A script scanned every tracked doc, rule, skill, agent, hook and source file for tooling/ paths that no longer exist, skipping historical records (sprints, discussions, workshops, governance, generated wiki pages). It found 62. The ones that tell a reader what to RUN now name the reach verb: - The atlas skill still sent agents to tooling/atlas, atlas-verify, atlas-update-field and atlas-commit-and-sync — about forty lines, all retired in T-1285. They now name the `reach atlas` verbs, and the skill records that commit-and-sync STAGES by default (--commit to commit) and takes --corridor as an option. - The clerk agent named tooling/clerk-review (now `reach dev clerk`). The Si and clerk briefings sent those agents to the retired tooling/db/decision and sqlite-query CLIs and to decisions/*.md paths that moved to governance/ in the pql migration. They now name pql. - The ticket-cli rule documented `pql decisions read`, which does not exist; `show` already includes the body. - The culture authoring guide and the RON sources name `reach validate ron`, with the same arguments as before. - The 41 Blender payloads' usage lines ran the retired tooling/blender wrapper, and the docstrings still cited pre-carve-out paths. They now read `reach blender run <payload>`. - Doc comments in server/, client/, wiki TOMLs and the domain modules. What is left is deliberate: "Formerly …" provenance, dated plans and findings docs, the retired-pipeline doc, and a build-artefact path. project.yaml 0.4.14 (mirrored to the client). Comment-only, but four touched files are in the canvas-version registry (trait_catalog_reader.rs, since T-1289, canvas_sources.py itself, and two client files). The gate is path-based and has no override. The previous push was rejected on exactly this. Three of the edits are stamped ledger sources, so systems.db is regenerated and the stamp is fresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
445 lines
18 KiB
Python
445 lines
18 KiB
Python
"""
|
|
blender_author_outerwear.py (T-1089, jacket_modern — open-front outerwear family)
|
|
|
|
Companion to blender_author_offset_shell.py: authors OUTERWEAR offset-shells
|
|
(jacket / track-jacket / parka family) per body. It imports the base module and
|
|
reuses its scene/join/offset/solidify/mask/logo/export machinery; everything
|
|
garment-specific lives in the PARAMS block below, so a new outerwear piece is a
|
|
parameter delta, not a new script.
|
|
|
|
What it adds over the base t-shirt shell:
|
|
|
|
* FULL SLEEVES — covers seg_arm_lower_l/r too; the sleeve cut moves from the
|
|
upper arm to a WRIST cut on the lowerarm bones (same bone-plane technique).
|
|
* 4th REGION (A = zip/trim) — a front zip placket strip running hem -> through
|
|
the collar, plus knit cuff bands on the sleeve ends. Region layout per spec:
|
|
collar=R, body=G, sleeves=B, zip/trim=A.
|
|
* POSITION-PAINTED ALBEDO — instead of the base script's flat-noise albedo,
|
|
each body gets an albedo baked in its own UV0 layout by rasterizing every
|
|
face and painting per-texel from interpolated body-local position: zip teeth
|
|
dashes + placket, panel seam lines (shoulder, collar, cuff, placket edges),
|
|
hem band. The toon_garment shader recolors by LUMA, so the painted detail is
|
|
authored as luma contrast and survives any tint choice.
|
|
* LEFT-BREAST LOGO PATCH — the UV2 chest box shifts off-centre (the zip owns
|
|
the centre line), scaled per body from the same bone-landmark ratios.
|
|
|
|
Per-body only (this family is the poster child for the Q-060 per-body route):
|
|
|
|
reach blender run blender_author_outerwear \
|
|
<bodies_root> <out_dir> \
|
|
[--bodies a,b,c] [--offset 0.022] [--cuff-keep 0.88]
|
|
|
|
Writes per body: <out_dir>/<body>.glb (painted albedo embedded), <body>_mask.png
|
|
Plus: <out_dir>/base_albedo.png (average_m's painted albedo)
|
|
<out_dir>/reference_mask.png (copy of average_m's, fallback)
|
|
|
|
The per-body albedo is embedded in the GLB under the image name "base_albedo"
|
|
(tshirt convention): Godot's importer extracts it as <body>_base_albedo.png.
|
|
average_m is deliberately authored LAST so the shared base_albedo.png sidecar
|
|
ends up holding the reference body's pixels.
|
|
|
|
Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060.
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
import bpy
|
|
import bmesh
|
|
import numpy as np
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Load the base offset-shell module (shared engine machinery).
|
|
# --------------------------------------------------------------------------
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py"))
|
|
base = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(base)
|
|
|
|
log = base.log
|
|
|
|
# --------------------------------------------------------------------------
|
|
# PARAMS — jacket_modern (casual zip jacket). A new outerwear garment should
|
|
# only need to touch this block (or override via CLI where exposed).
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Full-arm coverage: torso + torso_upper + both whole arms.
|
|
SEGMENTS = [
|
|
"seg_torso", "seg_torso_upper",
|
|
"seg_arm_upper_l", "seg_arm_upper_r",
|
|
"seg_arm_lower_l", "seg_arm_lower_r",
|
|
]
|
|
|
|
OFFSET_M = 0.022 # LARGEST standoff of the tops — worn over a shirt
|
|
# (tshirt_modern outer surface ~16 mm), must neither
|
|
# clip the body nor read skin-tight.
|
|
CLOTH_THICKNESS_M = 0.005 # beefier cloth than the 4 mm tee
|
|
|
|
CUFF_KEEP_FRAC = 0.88 # fraction of the lowerarm kept (wrist cut — hands show)
|
|
CUFF_BAND_FRAC = 0.22 # last fraction of the KEPT forearm = knit cuff trim (A)
|
|
|
|
# Region-shape ratios, anchored to the base module's average_m reference
|
|
# landmarks (shoulder |x| 0.1919, neck len 0.0793) so they scale per body.
|
|
ZIP_HALF_FRAC = 0.030 / 0.1919 # zip placket half-width as frac of shoulder |x|
|
|
COLLAR_DROP_NECK_FRAC = 0.55 # collar band starts this far below neck head
|
|
# (t-shirt band was 0.3846 — jacket collar is taller,
|
|
# but 0.80 caught upper-chest faces and rendered as
|
|
# jagged shoulder "wings"; 0.55 keeps a neat band)
|
|
COLLAR_X_MULT = 1.15 # jacket collar slightly wider than the tee band
|
|
|
|
# Left-breast logo patch (character-left = +X; centre line belongs to the zip).
|
|
# Fracs of shoulder |x| / spine span, from average_m absolutes (0.035..0.115 m,
|
|
# z 1.30..1.42 m).
|
|
CHEST_X_LO_FRAC = 0.035 / 0.1919
|
|
CHEST_X_HI_FRAC = 0.115 / 0.1919
|
|
CHEST_Z_LO_FRAC = (1.30 - 1.072) / (1.5205 - 1.072)
|
|
CHEST_Z_HI_FRAC = (1.42 - 1.072) / (1.5205 - 1.072)
|
|
|
|
# Painted-albedo luma palette (the shader tints by luma: tint * (luma*1.5+0.2)).
|
|
BASE_LUMA = 0.58
|
|
PLACKET_LUMA = 0.50 # slightly darker front placket band
|
|
SEAM_LUMA = 0.38 # panel seam / stitch lines
|
|
TEETH_LUMA = 0.88 # bright zip teeth dashes
|
|
TEETH_GAP_LUMA = 0.34 # dark tape between dashes
|
|
CUFF_LUMA = 0.48 # knit cuff band
|
|
HEM_LUMA = 0.50 # bottom hem band
|
|
ALBEDO_NOISE = 0.02 # woven jitter
|
|
SEAM_W = 0.005 # seam line half-width, metres
|
|
TEETH_HALF_W = 0.006 # zip teeth strip half-width, metres
|
|
TEETH_PERIOD = 0.024 # dash period along Z, metres
|
|
TEETH_DUTY = 0.014 # bright dash length within a period, metres
|
|
HEM_BAND_M = 0.020 # hem band height, metres
|
|
# Slate hue applied on top of luma (only visible if the region mask is missing).
|
|
FABRIC_HUE = np.array([0.93, 0.97, 1.06], dtype=np.float32)
|
|
|
|
ALBEDO_SIZE = 512
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Thresholds — base derivation + outerwear extras from the same armature
|
|
# --------------------------------------------------------------------------
|
|
|
|
def outerwear_thresholds(armature):
|
|
thr = base.derive_thresholds(armature)
|
|
bones = armature.data.bones
|
|
neck = bones.get("neck_01")
|
|
ua_l = bones.get("upperarm_l")
|
|
ua_r = bones.get("upperarm_r")
|
|
spine01 = bones.get("spine_01")
|
|
|
|
# Shoulder |x| (same landmark the base derivation uses).
|
|
if ua_l and ua_r:
|
|
shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0
|
|
else:
|
|
shoulder_x = 0.1919 # average_m fallback
|
|
|
|
# Taller, wider standing collar.
|
|
if neck:
|
|
neck_len = neck.tail_local.z - neck.head_local.z
|
|
thr["collar_z_min"] = neck.head_local.z - COLLAR_DROP_NECK_FRAC * neck_len
|
|
thr["collar_x_abs"] = thr["collar_x_abs"] * COLLAR_X_MULT
|
|
|
|
# Front zip placket.
|
|
thr["zip_half_x"] = shoulder_x * ZIP_HALF_FRAC
|
|
|
|
# Wrist cut planes + cuff trim band on the lowerarm bones.
|
|
cut_planes = []
|
|
cuff_abs = []
|
|
for bone_name, sign in [("lowerarm_l", +1), ("lowerarm_r", -1)]:
|
|
b = bones.get(bone_name)
|
|
if b is None:
|
|
log(f"WARNING: bone {bone_name} missing — sleeve left full-length")
|
|
continue
|
|
head_x = b.head_local.x
|
|
tail_x = b.tail_local.x
|
|
cut_x = head_x + CUFF_KEEP_FRAC * (tail_x - head_x)
|
|
band_x = head_x + (CUFF_KEEP_FRAC - CUFF_BAND_FRAC) * (tail_x - head_x)
|
|
cut_planes.append((sign, cut_x))
|
|
cuff_abs.append(abs(band_x))
|
|
log(f"wrist cut {bone_name}: keep |x| up to {cut_x:.3f} "
|
|
f"(elbow {head_x:.3f} -> wrist {tail_x:.3f}), cuff from {band_x:.3f}")
|
|
thr["wrist_cut_planes"] = cut_planes
|
|
thr["cuff_x_abs"] = min(cuff_abs) if cuff_abs else 1e9
|
|
|
|
# Left-breast logo patch (replaces the base full-chest box).
|
|
thr["chest_x"] = (shoulder_x * CHEST_X_LO_FRAC, shoulder_x * CHEST_X_HI_FRAC)
|
|
if neck and spine01:
|
|
spine_lo = spine01.head_local.z
|
|
span = neck.head_local.z - spine_lo
|
|
thr["chest_z"] = (spine_lo + CHEST_Z_LO_FRAC * span,
|
|
spine_lo + CHEST_Z_HI_FRAC * span)
|
|
log(f"outerwear thresholds: zip half {thr['zip_half_x']:.3f} "
|
|
f"collar z>={thr['collar_z_min']:.3f} |x|<{thr['collar_x_abs']:.3f} "
|
|
f"cuff |x|>={thr['cuff_x_abs']:.3f} "
|
|
f"chest x=({thr['chest_x'][0]:.3f},{thr['chest_x'][1]:.3f}) "
|
|
f"z=({thr['chest_z'][0]:.3f},{thr['chest_z'][1]:.3f})")
|
|
return thr
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Wrist cut (bone-plane, same technique as the base sleeve cut)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def wrist_cut(shell, thr):
|
|
"""Delete forearm verts beyond the wrist plane on each side."""
|
|
cut_planes = thr.get("wrist_cut_planes", [])
|
|
if not cut_planes:
|
|
log("WARNING: no wrist cut planes — sleeves stay full length")
|
|
return
|
|
bm = bmesh.new()
|
|
bm.from_mesh(shell.data)
|
|
bm.verts.ensure_lookup_table()
|
|
to_delete = []
|
|
for v in bm.verts:
|
|
for sign, thr_x in cut_planes:
|
|
if sign > 0 and v.co.x > thr_x:
|
|
to_delete.append(v)
|
|
break
|
|
if sign < 0 and v.co.x < thr_x:
|
|
to_delete.append(v)
|
|
break
|
|
bmesh.ops.delete(bm, geom=to_delete, context='VERTS')
|
|
bm.to_mesh(shell.data)
|
|
bm.free()
|
|
shell.data.update()
|
|
log(f"wrist cut removed {len(to_delete)} verts; "
|
|
f"{len(shell.data.vertices)} remain")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 4-region classifier (collar=R, body=G, sleeves=B, zip/trim=A)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def classify_region_outerwear(center, thr):
|
|
x, y, z = center.x, center.y, center.z
|
|
ax = abs(x)
|
|
front = y * base.FRONT_Y_SIGN > 0.0
|
|
# Zip placket first: runs hem -> THROUGH the collar (zip-through jacket).
|
|
if front and ax < thr["zip_half_x"]:
|
|
return (0.0, 0.0, 0.0, 1.0) # zip/trim -> A (tint[3])
|
|
if ax >= thr["cuff_x_abs"]:
|
|
return (0.0, 0.0, 0.0, 1.0) # knit cuff trim -> A (tint[3])
|
|
if ax >= thr["sleeve_x_abs"]:
|
|
return (0.0, 0.0, 1.0, 0.0) # sleeves -> B (tint[2])
|
|
if z >= thr["collar_z_min"] and ax < thr["collar_x_abs"]:
|
|
return (1.0, 0.0, 0.0, 0.0) # collar -> R (tint[0])
|
|
return (0.0, 1.0, 0.0, 0.0) # main body -> G (tint[1])
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Position-painted albedo (per-body UV0 bake)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _paint_luma(x, y, z, thr, z_min):
|
|
"""Vectorised luma paint from body-local position (arrays in, array out)."""
|
|
v = np.full(x.shape, BASE_LUMA, dtype=np.float32)
|
|
ax = np.abs(x)
|
|
front = (y * base.FRONT_Y_SIGN) > 0.0
|
|
|
|
# Hem band along the jacket bottom.
|
|
v = np.where(z < z_min + HEM_BAND_M, HEM_LUMA, v)
|
|
# Knit cuff bands.
|
|
v = np.where(ax >= thr["cuff_x_abs"], CUFF_LUMA, v)
|
|
# Panel seam lines: shoulder (sleeve boundary), cuff start, collar base.
|
|
v = np.where(np.abs(ax - thr["sleeve_x_abs"]) < SEAM_W, SEAM_LUMA, v)
|
|
v = np.where(np.abs(ax - thr["cuff_x_abs"]) < SEAM_W, SEAM_LUMA, v)
|
|
collar_seam = (np.abs(z - thr["collar_z_min"]) < SEAM_W) \
|
|
& (ax < thr["collar_x_abs"] * 1.2)
|
|
v = np.where(collar_seam, SEAM_LUMA, v)
|
|
|
|
# Front zip placket: darker band + stitch edges + dashed teeth line.
|
|
zh = thr["zip_half_x"]
|
|
v = np.where(front & (ax < zh), PLACKET_LUMA, v)
|
|
v = np.where(front & (np.abs(ax - zh) < SEAM_W * 0.6), SEAM_LUMA, v)
|
|
teeth = front & (ax < TEETH_HALF_W)
|
|
dash = np.mod(z, TEETH_PERIOD) < TEETH_DUTY
|
|
v = np.where(teeth & dash, TEETH_LUMA, v)
|
|
v = np.where(teeth & ~dash, TEETH_GAP_LUMA, v)
|
|
return v
|
|
|
|
|
|
def _raster_tri_painted(lum, uv_a, uv_b, uv_c, p_a, p_b, p_c, thr, z_min, W, H):
|
|
"""Barycentric fill of a UV triangle, painting per-texel from the
|
|
barycentrically interpolated body-local position."""
|
|
ax_, ay_ = uv_a.x * (W - 1), uv_a.y * (H - 1)
|
|
bx_, by_ = uv_b.x * (W - 1), uv_b.y * (H - 1)
|
|
cx_, cy_ = uv_c.x * (W - 1), uv_c.y * (H - 1)
|
|
minx = max(int(np.floor(min(ax_, bx_, cx_))), 0)
|
|
maxx = min(int(np.ceil(max(ax_, bx_, cx_))), W - 1)
|
|
miny = max(int(np.floor(min(ay_, by_, cy_))), 0)
|
|
maxy = min(int(np.ceil(max(ay_, by_, cy_))), H - 1)
|
|
if minx > maxx or miny > maxy:
|
|
return
|
|
denom = (by_ - cy_) * (ax_ - cx_) + (cx_ - bx_) * (ay_ - cy_)
|
|
if abs(denom) < 1e-9:
|
|
return
|
|
ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1]
|
|
px = xs + 0.5
|
|
py = ys + 0.5
|
|
w0 = ((by_ - cy_) * (px - cx_) + (cx_ - bx_) * (py - cy_)) / denom
|
|
w1 = ((cy_ - ay_) * (px - cx_) + (ax_ - cx_) * (py - cy_)) / denom
|
|
w2 = 1.0 - w0 - w1
|
|
inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4)
|
|
if not inside.any():
|
|
return
|
|
pos_x = w0 * p_a.x + w1 * p_b.x + w2 * p_c.x
|
|
pos_y = w0 * p_a.y + w1 * p_b.y + w2 * p_c.y
|
|
pos_z = w0 * p_a.z + w1 * p_b.z + w2 * p_c.z
|
|
vals = _paint_luma(pos_x, pos_y, pos_z, thr, z_min)
|
|
region = lum[miny:maxy + 1, minx:maxx + 1]
|
|
region[inside] = vals[inside]
|
|
|
|
|
|
def bake_painted_albedo(shell, thr, out_path, seed=1093):
|
|
"""Bake a per-body albedo: luma-painted detail in this body's UV0 layout."""
|
|
W = H = ALBEDO_SIZE
|
|
lum = np.full((H, W), BASE_LUMA, dtype=np.float32)
|
|
|
|
me = shell.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
bm.faces.ensure_lookup_table()
|
|
uv_layer = bm.loops.layers.uv.active
|
|
if uv_layer is None:
|
|
raise RuntimeError("no active UV layer for albedo bake")
|
|
|
|
z_min = min(v.co.z for v in bm.verts)
|
|
for face in bm.faces:
|
|
loops = face.loops[:]
|
|
uvs = [loop[uv_layer].uv.copy() for loop in loops]
|
|
pos = [loop.vert.co.copy() for loop in loops]
|
|
for i in range(1, len(uvs) - 1):
|
|
_raster_tri_painted(lum, uvs[0], uvs[i], uvs[i + 1],
|
|
pos[0], pos[i], pos[i + 1], thr, z_min, W, H)
|
|
bm.free()
|
|
|
|
rng = np.random.default_rng(seed)
|
|
lum += (rng.random((H, W), dtype=np.float32) - 0.5) * 2.0 * ALBEDO_NOISE
|
|
rgb = np.clip(lum[:, :, None] * FABRIC_HUE[None, None, :], 0.0, 1.0)
|
|
rgba = np.concatenate(
|
|
[rgb, np.ones((H, W, 1), dtype=np.float32)], axis=2).astype(np.float32)
|
|
|
|
img = bpy.data.images.new("garment_painted_albedo", W, H, alpha=False)
|
|
img.pixels.foreach_set(rgba.reshape(-1))
|
|
img.update()
|
|
img.filepath_raw = out_path
|
|
img.file_format = 'PNG'
|
|
img.save()
|
|
log(f"baked painted albedo -> {out_path}")
|
|
return img
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Per-body authoring
|
|
# --------------------------------------------------------------------------
|
|
|
|
def author_outerwear(body_dir, out_dir, body, offset):
|
|
base.clear_scene()
|
|
shell, armature = base.build_covered_mesh(body_dir)
|
|
thr = outerwear_thresholds(armature)
|
|
wrist_cut(shell, thr)
|
|
base.offset_outward(shell, offset)
|
|
base.solidify(shell, CLOTH_THICKNESS_M)
|
|
|
|
# Shared sidecar name on purpose: the exporter derives the embedded glTF
|
|
# image name from the filepath basename, and Godot extracts embedded
|
|
# textures as <glb>_<image>.png — so this yields <body>_base_albedo.png
|
|
# on import, matching the tshirt_modern convention (no doubled names).
|
|
albedo_path = os.path.join(out_dir, "base_albedo.png")
|
|
albedo_img = bake_painted_albedo(shell, thr, albedo_path)
|
|
base.assign_fabric_material(shell, albedo_img)
|
|
|
|
base.author_logo_uv(shell, thr) # UV2 before mask bake (mask uses UV0/active)
|
|
base.bake_region_mask(shell, os.path.join(out_dir, f"{body}_mask.png"), thr)
|
|
|
|
# Honest region tally (the base bake log can't see the A channel).
|
|
counts = {"collar": 0, "body": 0, "sleeve": 0, "zip/trim": 0}
|
|
bm = bmesh.new()
|
|
bm.from_mesh(shell.data)
|
|
for face in bm.faces:
|
|
c = classify_region_outerwear(face.calc_center_median(), thr)
|
|
if c[3] > 0.5:
|
|
counts["zip/trim"] += 1
|
|
elif c[2] > 0.5:
|
|
counts["sleeve"] += 1
|
|
elif c[0] > 0.5:
|
|
counts["collar"] += 1
|
|
else:
|
|
counts["body"] += 1
|
|
bm.free()
|
|
total = max(sum(counts.values()), 1)
|
|
log("outerwear regions: " + " ".join(
|
|
f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in counts.items()))
|
|
|
|
base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb"))
|
|
|
|
|
|
def main():
|
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
|
if len(argv) < 2:
|
|
print("Usage: -- <bodies_root> <out_dir> "
|
|
"[--bodies a,b,c] [--offset M] [--cuff-keep F]")
|
|
sys.exit(1)
|
|
bodies_root = argv[0]
|
|
out_dir = argv[1]
|
|
|
|
global CUFF_KEEP_FRAC
|
|
offset = OFFSET_M
|
|
if "--offset" in argv:
|
|
offset = float(argv[argv.index("--offset") + 1])
|
|
if "--cuff-keep" in argv:
|
|
CUFF_KEEP_FRAC = float(argv[argv.index("--cuff-keep") + 1])
|
|
bodies = base.BODY_TYPES
|
|
if "--bodies" in argv:
|
|
bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")]
|
|
# Author the reference body LAST so the shared base_albedo.png sidecar
|
|
# (rewritten per body before each export) ends as average_m's version.
|
|
if base.REFERENCE_BODY in bodies:
|
|
bodies = [b for b in bodies if b != base.REFERENCE_BODY] + [base.REFERENCE_BODY]
|
|
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
# Route the base mask bake through the 4-region outerwear classifier and
|
|
# the covered-segment import through the full-arm list.
|
|
base._classify_region = classify_region_outerwear
|
|
base.COVERED_SEGMENTS = SEGMENTS
|
|
|
|
log(f"outerwear per-body mode: {len(bodies)} bodies, "
|
|
f"offset {offset*1000:.0f} mm, cuff keep {CUFF_KEEP_FRAC:.2f}")
|
|
results = []
|
|
for body in bodies:
|
|
body_dir = os.path.join(bodies_root, body)
|
|
log(f"=== {body} ===")
|
|
if not os.path.isdir(body_dir):
|
|
results.append((body, "skipped: body dir missing"))
|
|
continue
|
|
try:
|
|
author_outerwear(body_dir, out_dir, body, offset)
|
|
results.append((body, "ok"))
|
|
except Exception as exc:
|
|
log(f"ERROR {body}: {exc}")
|
|
results.append((body, f"error: {exc}"))
|
|
|
|
# Runtime fallback: reference_mask.png mirrors average_m's mask.
|
|
# (base_albedo.png already holds average_m's albedo — it authored last.)
|
|
ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png")
|
|
if os.path.isfile(ref_mask):
|
|
shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png"))
|
|
log(f"copied {base.REFERENCE_BODY}_mask.png -> reference_mask.png")
|
|
|
|
log("=" * 50)
|
|
for body, status in results:
|
|
log(f" {body:12s} {status}")
|
|
ok = sum(1 for _, s in results if s == "ok")
|
|
log(f"OK={ok}/{len(results)}")
|
|
if ok != len(results):
|
|
sys.exit(1)
|
|
log("DONE")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|