Files
settled-reach/tooling/scripts/blender/blender_author_offset_shell_legs.py
T
jpmschweitzerandClaude Opus 5.5 c597ec9131 docs(tooling): T-1253 — sweep the live references to retired tool paths
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>
2026-09-23 20:13:58 +02:00

372 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
blender_author_offset_shell_legs.py (T-1089, lower-body offset-shell garments)
Companion to blender_author_offset_shell.py (imported as a module — scene
helpers, offset/solidify, albedo, rasterizer and export are reused, not
copied). Where the base script is calibrated for TORSO garments (sleeve cut,
collar/sleeve regions, chest logo UV), this one authors LOWER-BODY garments
from seg_hips + leg segments with parameterized cuts/regions, so one script
serves shorts, jeans, joggers, formal pants, swim trunks:
--coverage thigh|full thigh: seg_hips + seg_leg_upper_l/r (shorts)
full: + seg_leg_lower_l/r (jeans/joggers/formal)
--hem-frac F fraction of the hem bone's length KEPT below its
head (bone-plane cut). Hem bone = thigh for
--coverage thigh, calf for full. 0.78 on the thigh
= casual shorts hem ~9 cm above the knee.
--waistband-frac F waistband band height as a fraction of
(waist rim z thigh head z). The band is
classified R in the region mask; everything else
is G. (B/A unused — 2-region garment.)
--fabric R,G,B flat toon-friendly base tone (luma ~0.6 keeps the
toon_garment.gdshader luma-recolor faithful).
--seed N albedo noise seed (deterministic output).
Cut/region thresholds derive PER BODY from that body's own landmarks, same
philosophy as the base script: the hem plane comes from the thigh/calf bone
(head→tail, identical 65-bone rig on all 11 bodies), the waist rim is the
natural top boundary of seg_hips (its own mesh z-max), so proportions match
across bodies. The waistline itself comes free as a segment boundary — no
waist join geometry needed.
Regions (RGBA mask, toon_garment.gdshader): waistband → R (tint_0),
body → G (tint_1). A parked logo_uv TEXCOORD_1 layer is authored for channel
consistency with torso garments (all UVs outside [0,1] → shader draws nothing).
Modes mirror the base script:
PER-BODY (preferred for offset shells, Q-060):
reach blender run blender_author_offset_shell_legs \
<bodies_dir> <out_dir> --per-body \
[--bodies a,b,c] [--offset 0.012] [--hem-frac 0.78] [--waistband-frac 0.35]
Writes <out_dir>/<body>.glb + <out_dir>/<body>_mask.png per body, plus
shared base_albedo.png and reference_mask.png (= average_m's mask, runtime
fallback for the compositor).
SINGLE-REFERENCE:
reach blender run blender_author_offset_shell_legs \
<bodies_dir>/average_m <out_dir> [--offset 0.020] [...]
Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house wardrobe).
"""
import sys
import os
import shutil
import importlib.util
import bpy # noqa: F401 (Blender runtime)
import bmesh
import numpy as np
# --------------------------------------------------------------------------
# Import the base authoring module (shared helpers; main() is __main__-guarded)
# --------------------------------------------------------------------------
_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
# --------------------------------------------------------------------------
# Parameters (defaults = shorts_modern)
# --------------------------------------------------------------------------
COVERAGE_SEGMENTS = {
"thigh": ["seg_hips", "seg_leg_upper_l", "seg_leg_upper_r"],
"full": ["seg_hips", "seg_leg_upper_l", "seg_leg_upper_r",
"seg_leg_lower_l", "seg_leg_lower_r"],
}
HEM_BONES = {"thigh": ("thigh_l", "thigh_r"), "full": ("calf_l", "calf_r")}
HEM_FRAC = 0.78 # keep 78% of the thigh → hem above the knee
WAISTBAND_FRAC = 0.35 # top 35% of waist-rim→thigh-head span = waistband (R)
FABRIC_RGB = (0.63, 0.60, 0.53) # warm stone khaki, luma ~0.60 (toon-friendly)
ALBEDO_SEED = 20890 # deterministic albedo noise, distinct from the tshirt
# --------------------------------------------------------------------------
# Per-body threshold derivation
# --------------------------------------------------------------------------
def derive_leg_thresholds(armature, coverage, hem_frac):
"""Hem plane from this body's own hem bone; thigh head z for the waistband
span. All 11 bodies share the 65-bone rig, so the landmarks always exist."""
bones = armature.data.bones
hem_l, hem_r = HEM_BONES[coverage]
zs = []
thigh_heads = []
for name in (hem_l, hem_r):
b = bones.get(name)
if b is None:
raise RuntimeError(f"hem bone {name} missing on armature")
zs.append(b.head_local.z + hem_frac * (b.tail_local.z - b.head_local.z))
for name in ("thigh_l", "thigh_r"):
b = bones.get(name)
if b is None:
raise RuntimeError(f"landmark bone {name} missing on armature")
thigh_heads.append(b.head_local.z)
thr = {
"hem_z": sum(zs) / len(zs),
"thigh_head_z": sum(thigh_heads) / len(thigh_heads),
}
log(f"thresholds: hem z>={thr['hem_z']:.3f} "
f"(hem bone {hem_l}/{hem_r}, keep {hem_frac:.2f}) "
f"thigh head z={thr['thigh_head_z']:.3f}")
return thr
# --------------------------------------------------------------------------
# Seam weld — the body segment meshes are assembled from patches whose seam
# vertices are coincident but DUPLICATED. Each copy's normal averages only its
# own patch's faces, so the outward offset pulls seams apart (visible slit at
# the front-centre of seg_hips, V-notches on the waist rim). Welding before
# the offset merges the copies (weights/UVs are identical on coincident verts)
# and gives one smooth normal per seam vertex.
# --------------------------------------------------------------------------
def weld_seams(shell, epsilon=1e-4):
bm = bmesh.new()
bm.from_mesh(shell.data)
before = len(bm.verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=epsilon)
bm.to_mesh(shell.data)
bm.free()
shell.data.update()
after = len(shell.data.vertices)
log(f"seam weld merged {before - after} duplicate verts; {after} remain")
# --------------------------------------------------------------------------
# Bone-plane hem cut (z threshold — legs hang along -Z in rest pose)
# --------------------------------------------------------------------------
def hem_cut(shell, hem_z):
"""Delete every vert below the hem plane. A single global z threshold is
safe: the hips segment bottoms out far above any sensible hem."""
bm = bmesh.new()
bm.from_mesh(shell.data)
bm.verts.ensure_lookup_table()
to_delete = [v for v in bm.verts if v.co.z < hem_z]
bmesh.ops.delete(bm, geom=to_delete, context='VERTS')
bm.to_mesh(shell.data)
bm.free()
shell.data.update()
log(f"hem cut removed {len(to_delete)} verts below z={hem_z:.3f}; "
f"{len(shell.data.vertices)} remain")
# --------------------------------------------------------------------------
# Region mask: waistband (R) / body (G)
# --------------------------------------------------------------------------
def bake_region_mask_legs(shell, out_path, thr, waistband_frac):
"""Rasterize UV0 faces: waistband → R, everything else → G.
MUST run PRE-solidify (open boundaries still present). The waistband is
boundary-anchored: the waist rim is the top open boundary loop of seg_hips
(the rim DIPS ~5 cm at the navel, so a global z-max test misses the front
row), so a face is waistband when it touches a rim-boundary vertex — that
keys the full top face row on every tessellation — or when its centre lies
inside the proportional band. Solidify afterwards duplicates the UV loops
unchanged, so the baked texels serve outer shell, inner shell and rim caps
alike."""
W = H = base.MASK_SIZE
buf = np.zeros((H, W, 4), dtype=np.float32)
buf[:, :, 1] = 1.0 # green background = body (bilinear-bleed safe)
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
bm.faces.ensure_lookup_table()
bm.verts.ensure_lookup_table()
uv_layer = bm.loops.layers.uv.active
if uv_layer is None:
raise RuntimeError("no active UV layer for region mask bake")
waist_rim_z = max(v.co.z for v in bm.verts)
band = waistband_frac * (waist_rim_z - thr["thigh_head_z"])
wb_z_min = waist_rim_z - band
# Waist-rim boundary verts: on an open boundary AND above the thigh head
# (the only other boundaries are the leg hems, far below).
rim_verts = set()
for e in bm.edges:
if e.is_boundary:
for v in e.verts:
if v.co.z >= thr["thigh_head_z"]:
rim_verts.add(v.index)
log(f"waistband: rim z={waist_rim_z:.3f} band {band*100:.1f} cm "
f"→ R for z>={wb_z_min:.3f} or touching {len(rim_verts)} rim verts")
counts = {"waistband": 0, "body": 0}
for face in bm.faces:
in_band = face.calc_center_median().z >= wb_z_min
touches_rim = any(v.index in rim_verts for v in face.verts)
if in_band or touches_rim:
color = (1.0, 0.0, 0.0, 0.0)
counts["waistband"] += 1
else:
color = (0.0, 1.0, 0.0, 0.0)
counts["body"] += 1
for a, b, c in base._tris_from_face(face, uv_layer):
base._raster_tri(buf, a, b, c, color, W, H)
bm.free()
total = max(sum(counts.values()), 1)
log("region faces: " + " ".join(
f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in counts.items()))
img = bpy.data.images.new("garment_region_mask", W, H, alpha=True)
img.pixels.foreach_set(buf.reshape(-1))
img.update()
img.filepath_raw = out_path
img.file_format = 'PNG'
img.save()
log(f"baked region mask -> {out_path}")
def park_logo_uv(shell):
"""Author a TEXCOORD_1 layer with every loop parked outside [0,1] — keeps
the UV-channel layout identical to torso garments; the shader's in-box
guard means nothing ever draws."""
me = shell.data
while len(me.uv_layers) > 1:
me.uv_layers.remove(me.uv_layers[-1])
me.uv_layers.new(name="logo_uv")
me.uv_layers.active = me.uv_layers[0]
bm = bmesh.new()
bm.from_mesh(me)
uvl = bm.loops.layers.uv.get("logo_uv")
for face in bm.faces:
for loop in face.loops:
loop[uvl].uv = (2.0, 2.0)
bm.to_mesh(me)
bm.free()
me.update()
log("logo UV2 parked (no logo region on this garment)")
# --------------------------------------------------------------------------
# Author one body
# --------------------------------------------------------------------------
def author_legs_shell(body_dir, out_dir, glb_name, mask_name, offset,
coverage, hem_frac, waistband_frac, seed):
base.clear_scene()
base.COVERED_SEGMENTS = COVERAGE_SEGMENTS[coverage]
shell, armature = base.build_covered_mesh(body_dir)
weld_seams(shell)
thr = derive_leg_thresholds(armature, coverage, hem_frac)
hem_cut(shell, thr["hem_z"])
base.offset_outward(shell, offset)
# Region mask + parked logo UV are authored PRE-solidify: the boundary-
# anchored waistband needs the open waist rim, and solidify duplicates all
# UV loops unchanged so the baked texels stay valid for the final mesh.
park_logo_uv(shell)
bake_region_mask_legs(shell, os.path.join(out_dir, mask_name), thr,
waistband_frac)
base.solidify(shell, base.CLOTH_THICKNESS_M)
base.FABRIC_RGB = FABRIC_RGB
albedo_img = base.make_base_albedo_image(seed=seed)
base.assign_fabric_material(shell, albedo_img)
base.save_albedo_sidecar(albedo_img, os.path.join(out_dir, "base_albedo.png"))
base.export_reference(shell, armature, os.path.join(out_dir, glb_name))
# --------------------------------------------------------------------------
# Entry
# --------------------------------------------------------------------------
def main():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
if len(argv) < 2:
print("Usage: -- <bodies_dir>/average_m <out_dir> [--offset M] "
"[--coverage thigh|full] [--hem-frac F] [--waistband-frac F] "
"[--fabric R,G,B] [--seed N]\n"
" or: -- <bodies_dir> <out_dir> --per-body [--bodies a,b,c] "
"[same flags]")
sys.exit(1)
in_dir = argv[0]
out_dir = argv[1]
per_body = "--per-body" in argv
global FABRIC_RGB
offset = None
coverage = "thigh"
hem_frac = HEM_FRAC
waistband_frac = WAISTBAND_FRAC
seed = ALBEDO_SEED
if "--offset" in argv:
offset = float(argv[argv.index("--offset") + 1])
if "--coverage" in argv:
coverage = argv[argv.index("--coverage") + 1]
if coverage not in COVERAGE_SEGMENTS:
print(f"unknown --coverage {coverage}")
sys.exit(1)
if "--hem-frac" in argv:
hem_frac = float(argv[argv.index("--hem-frac") + 1])
if "--waistband-frac" in argv:
waistband_frac = float(argv[argv.index("--waistband-frac") + 1])
if "--fabric" in argv:
FABRIC_RGB = tuple(
float(c) for c in argv[argv.index("--fabric") + 1].split(","))
if "--seed" in argv:
seed = int(argv[argv.index("--seed") + 1])
bodies = base.BODY_TYPES
if "--bodies" in argv:
bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")]
os.makedirs(out_dir, exist_ok=True)
if not per_body:
offset = base.OFFSET_M if offset is None else offset
body = os.path.basename(os.path.normpath(in_dir))
author_legs_shell(in_dir, out_dir, f"{body}.glb", "reference_mask.png",
offset, coverage, hem_frac, waistband_frac, seed)
log("DONE")
return
offset = base.PER_BODY_OFFSET_M if offset is None else offset
log(f"per-body mode: {len(bodies)} bodies, offset {offset*1000:.0f} mm, "
f"coverage={coverage} hem_frac={hem_frac} waistband_frac={waistband_frac}")
results = []
for body in bodies:
body_dir = os.path.join(in_dir, body)
log(f"=== {body} ===")
if not os.path.isdir(body_dir):
results.append((body, "skipped: body dir missing"))
continue
try:
author_legs_shell(body_dir, out_dir, f"{body}.glb",
f"{body}_mask.png", offset, coverage, hem_frac,
waistband_frac, seed)
results.append((body, "ok"))
except Exception as exc:
log(f"ERROR {body}: {exc}")
results.append((body, f"error: {exc}"))
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 (fallback)")
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()