Files
settled-reach/tooling/scripts/blender/blender_author_joggers.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

511 lines
22 KiB
Python

"""
blender_author_joggers.py (T-1089 wave 2, joggers_modern + sweatpant family)
Authors JOGGERS/sweatpants as per-body offset shells: hips + full legs,
tapered toward the ankle into snug cuff bands, painted waist drawstring, and
a track SIDE STRIPE carried as its own tint region. Reuses
blender_author_offset_shell.py (via blender_author_denim_pants.py's module
instance) for scene build / join / solidify / export, and reuses the denim
companion's REQUIRED lower-body practices directly (not re-derived):
* boundary WELD of coincident segment-seam rings before offsetting
(denim.weld_boundaries) — un-welded rings offset apart along diverging
normals and open cracks at the waist/knee joins.
* open-rim FLATTENING (denim.flatten_open_rims) — the segment splitter
leaves 4.5-7.6 cm jagged teeth at the waist/ankle rims; boundary verts are
pulled onto clean planes pre-offset. The flattened ankle plane IS the
jogger cuff hem.
* waist flare + post-solidify residue clamp (denim.waist_flare,
denim.clamp_waist_residue) — deep-crouch waist-fold clip mitigation.
* per-body leg-axis landmarks (denim.LegLandmarks) — thigh_l/calf_l control
points drive azimuth math for the side stripe and the cuff/band rib paint,
so every parameter derives from the body's own bones (Q-060 per-body mode).
What this companion adds, as reusable parameters (not hacks):
* TAPERED variable offset — constant baggy standoff (--offset) above the
knee, smoothstep taper to --mid-offset at the cuff top, then a short
feathered step down to the snug --cuff-offset inside the cuff band: the
sweatpant silhouette (baggy thigh -> tapered shin -> gripping cuff).
* ankle CUFF band (--cuff-frac of the garment span) — its own mask region
(A channel) with painted knit RIB (azimuth-arc alternating shades, constant
metric rib width via the per-z leg axis).
* SIDE STRIPE (--stripe-width metres, scaled by hip width) — constant-width
azimuth band along the outer leg from waistband to cuff, painted bright
AND masked to its own region (B channel) for independent tinting.
* painted elastic waistband (subtle vertical rib) + centre-front DRAWSTRING:
two eyelets and two hanging cords with a slight outward slant (albedo
only; dark cords stay legible under the luma-scaled region tint).
Regions (RGBA mask, toon_garment.gdshader): waistband -> R (tint_0),
legs -> G (tint_1), side stripe -> B (tint_2), ankle cuffs -> A (tint_3).
A parked logo_uv TEXCOORD_1 layer is authored (denim.author_parked_uv2) —
not logo-capable, but the shader samples UV2 unconditionally.
Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r. Natural
boundaries give the waist opening and ankle hems for free. Per-body mode only
(offset shells author per body, Q-060).
Usage (joggers_modern reference invocation):
reach blender run blender_author_joggers \
client/assets/characters/bodies \
client/assets/characters/clothing/joggers_modern \
[--bodies average_m,child,...] [--offset 0.014] [--mid-offset 0.010] \
[--cuff-offset 0.0065] [--band-frac 0.055] [--cuff-frac 0.075] \
[--stripe-width 0.034] [--fabric 0.55,0.56,0.58] \
[--stripe-rgb 0.88,0.89,0.91] [--waist-flare 0.007] [--seed N] [--plain]
Writes per body: <out_dir>/<body>.glb (skinned, albedo embedded)
<out_dir>/<body>_mask.png (RGBA region mask, UV0)
<out_dir>/<body>_base_albedo.png
Plus: <out_dir>/base_albedo.png (average_m's, shared sidecar)
<out_dir>/reference_mask.png (average_m's, runtime fallback)
Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house
wardrobe), Q-060 (per-body offset shells).
"""
import importlib.util
import os
import shutil
import sys
import bmesh
import bpy
import numpy as np
# --------------------------------------------------------------------------
# Import the denim companion as a module — it carries the shared lower-body
# practices (weld, rim flatten, waist flare, clamp, landmarks, parked UV2) and
# its own instance of the base offset-shell library.
# --------------------------------------------------------------------------
_HERE = os.path.dirname(os.path.abspath(__file__))
_spec = importlib.util.spec_from_file_location(
"denim_pants", os.path.join(_HERE, "blender_author_denim_pants.py"))
denim = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(denim)
base = denim.base # single shared base-module instance
log = base.log
FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base)
# --------------------------------------------------------------------------
# Parameters (defaults = joggers_modern)
# --------------------------------------------------------------------------
COVERED_SEGMENTS = [
"seg_hips",
"seg_leg_upper_l", "seg_leg_upper_r",
"seg_leg_lower_l", "seg_leg_lower_r",
]
TEX_SIZE = 1024 # albedo + mask resolution (rib + cords need >512)
WAIST_FLARE_M = 0.007 # denim-proven deep-crouch waist-fold mitigation
# Tapered-offset profile (metres).
OFFSET_HI_M = 0.014 # hips/thigh standoff — baggy sweatpant volume
OFFSET_MID_M = 0.010 # taper target at the cuff top (shin)
OFFSET_CUFF_M = 0.0065 # snug cuff standoff (ankles barely deform)
CUFF_STEP_W_M = 0.010 # feather width of the taper->cuff step
# Vertical proportions — fractions of the garment span (waist_z - hem_z).
BAND_FRAC = 0.055 # elastic waistband height (R region)
CUFF_FRAC = 0.075 # knit cuff band height (A region) — taller than a hem
SEAM_W_FRAC = 0.008 # painted border-stitch line width
CORD_DROP_FRAC = 0.050 # drawstring cord length below the waistband
CORD_SLANT = 0.18 # outward cord slant (dx per unit hang depth)
# Horizontal / arc-metric proportions (scaled by hip half-width ratio lm.sh).
STRIPE_W_M = 0.034 # side stripe metric width (B region)
RIB_PERIOD_M = 0.009 # cuff knit-rib period (arc metres)
BAND_RIB_PERIOD_M = 0.006 # waistband elastic-rib period (arc metres)
CORD_X_M = 0.016 # drawstring eyelet/cord |x| at the band
CORD_W_M = 0.005 # cord width
EYELET_R_M = 0.0045 # eyelet dot radius
# Fleece style (sRGB floats; flat toon-friendly, texture carries identity).
FLEECE_RGB = (0.55, 0.56, 0.58) # heather grey, luma ~0.56 (tint-faithful)
STRIPE_RGB = (0.88, 0.89, 0.91) # near-white track stripe
CORD_RGB = (0.17, 0.17, 0.19) # charcoal drawstring
ALBEDO_NOISE = 0.020 # +/- fleece jitter
BAND_SHADE = 0.94 # waistband overall darkening
CUFF_SHADE = 0.96 # cuff overall darkening
RIB_LO = 0.86 # cuff rib dark-stripe factor
BAND_RIB_LO = 0.94 # waistband rib dark-stripe factor
STITCH_SHADE = 0.80 # band/cuff border-stitch darkening
PLAIN = False # --plain: skip drawstring/rib/stitch paint
NOISE_SEED = 2090 # distinct from denim (2089)
# --------------------------------------------------------------------------
# Joggers landmark finalization (denim.LegLandmarks carries the leg axis; the
# band/cuff fractions here are jogger parameters, not denim's)
# --------------------------------------------------------------------------
def finalize_jogger_landmarks(lm, waist_plane, ankle_plane,
band_frac, cuff_frac):
lm.waist_z = waist_plane
lm.hem_z = ankle_plane
lm.span = waist_plane - ankle_plane
lm.sh = lm.hip_x / denim._REF_HIP_X
lm.band_h = band_frac * lm.span
lm.cuff_h = cuff_frac * lm.span
lm.seam_w = SEAM_W_FRAC * lm.span
lm.band_z = waist_plane - lm.band_h
lm.cuff_top = ankle_plane + lm.cuff_h
log(f"landmarks: waist={lm.waist_z:.3f} hem={lm.hem_z:.3f} "
f"span={lm.span:.3f} hip_x={lm.hip_x:.3f} band_z={lm.band_z:.3f} "
f"cuff_top={lm.cuff_top:.3f} sh={lm.sh:.2f}")
# --------------------------------------------------------------------------
# Tapered offset — the jogger silhouette
# --------------------------------------------------------------------------
def _smoothstep(u):
u = min(max(u, 0.0), 1.0)
return u * u * (3.0 - 2.0 * u)
def tapered_offset(shell, knee_z, cuff_top, off_hi, off_mid, off_cuff,
step_w):
"""Per-vertex outward offset along smoothed normals: `off_hi` above the
knee, smoothstep taper to `off_mid` at the cuff top, then a feathered step
down to the snug `off_cuff` inside the cuff band. The step reads as the
cuff ledge; the A-region tint + painted rib carry the rest."""
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
bm.normal_update()
denom = max(knee_z - cuff_top, 1e-6)
lo = hi = None
for v in bm.verts:
z = v.co.z
t = off_hi + (off_mid - off_hi) * _smoothstep((knee_z - z) / denom)
s = _smoothstep((cuff_top + step_w - z) / step_w)
t = t + (off_cuff - t) * s
v.co += v.normal * t
lo = t if lo is None else min(lo, t)
hi = t if hi is None else max(hi, t)
bm.to_mesh(me)
bm.free()
me.update()
log(f"tapered offset: {hi * 1000:.1f} mm (thigh) -> {lo * 1000:.1f} mm "
f"(cuff); knee_z={knee_z:.3f} cuff_top={cuff_top:.3f}")
# --------------------------------------------------------------------------
# Jogger feature field (texel-level; drives albedo AND mask together)
# --------------------------------------------------------------------------
def _jogger_field(px, py, pz, lm):
"""Evaluate jogger features at texel 3D positions (numpy arrays).
Returns (front, in_band, in_cuff, stripe, arc): `stripe` feeds the mask B
channel, `in_band`/`in_cuff` feed R/A, `arc` is the azimuth arc-length
coordinate around the per-z leg axis used by the rib paint.
"""
front = py * FRONT_Y_SIGN > 0.004
in_band = pz >= lm.band_z
in_cuff = pz <= lm.cuff_top
mid = (~in_band) & (~in_cuff)
# Per-z leg axis, mirrored by x sign (denim.LegLandmarks control points).
side = np.where(px >= 0.0, 1.0, -1.0)
cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts)
cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts)
dx = px - cx
dy = py - cy
r = np.hypot(dx, dy) + 1e-9
# Side stripe: azimuth toward the outer (+/-x) direction; constant metric
# width via arc distance (same construction as the denim outseam).
arc_out = np.arccos(np.clip(dx * side / r, -1.0, 1.0)) * r
stripe = mid & (arc_out < 0.5 * STRIPE_W_M * lm.sh)
# Rib coordinate: signed azimuth arc length (0 at the outseam; the +/-pi
# wrap sits at the inner ankle / body centre where it cannot be seen).
arc = np.arctan2(dy, dx * side) * r
return front, in_band, in_cuff, stripe, arc
def _paint_texels(px, py, pz, noise, lm):
"""Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions."""
n = px.shape[0]
front, in_band, in_cuff, stripe, arc = _jogger_field(px, py, pz, lm)
# --- albedo ------------------------------------------------------------
alb = np.empty((n, 4), dtype=np.float32)
for c in range(3):
alb[:, c] = FLEECE_RGB[c] + noise
alb[:, 3] = 1.0
# Side stripe: bright, low-noise (crisp edge carries the sport read).
for c in range(3):
alb[stripe, c] = STRIPE_RGB[c] + 0.5 * noise[stripe]
if not PLAIN:
# Cuff knit rib: alternating shade stripes, constant metric width.
rib_dark = (np.floor(arc / (RIB_PERIOD_M * lm.sh)).astype(np.int64)
% 2) == 0
cuff_f = np.where(rib_dark, RIB_LO, 1.0) * CUFF_SHADE
alb[in_cuff, :3] *= cuff_f[in_cuff, None]
# Waistband elastic rib: same construction, subtler.
band_dark = (np.floor(arc / (BAND_RIB_PERIOD_M * lm.sh))
.astype(np.int64) % 2) == 0
band_f = np.where(band_dark, BAND_RIB_LO, 1.0) * BAND_SHADE
alb[in_band, :3] *= band_f[in_band, None]
# Border stitching: darker lines under the band and above the cuff.
w2 = lm.seam_w * 0.5
stitch = (np.abs(pz - lm.band_z) < w2) | (np.abs(pz - lm.cuff_top) < w2)
alb[stitch, :3] *= STITCH_SHADE
# Drawstring: two eyelets on the band + two slanted hanging cords.
ex = CORD_X_M * lm.sh
eyelet_z = lm.band_z + 0.35 * lm.band_h
eyelets = front & (
np.hypot(np.abs(px) - ex, pz - eyelet_z) < EYELET_R_M * lm.sh)
drop = CORD_DROP_FRAC * lm.span
hang = np.clip(eyelet_z - pz, 0.0, None)
cord_cx = ex + CORD_SLANT * hang
cords = front & (pz > eyelet_z - drop) & (pz <= eyelet_z) \
& (np.abs(np.abs(px) - cord_cx) < 0.5 * CORD_W_M * lm.sh)
for c in range(3):
alb[eyelets | cords, c] = CORD_RGB[c]
else:
alb[in_cuff, :3] *= CUFF_SHADE
alb[in_band, :3] *= BAND_SHADE
# --- region mask: band R / legs G / stripe B / cuffs A -------------------
mask = np.zeros((n, 4), dtype=np.float32)
is_b = stripe & ~in_band & ~in_cuff
is_g = ~(in_band | in_cuff | is_b)
mask[in_band, 0] = 1.0
mask[is_g, 1] = 1.0
mask[is_b, 2] = 1.0
mask[in_cuff, 3] = 1.0
return alb, mask
def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H):
"""Barycentric texel fill of one UV triangle: interpolate 3D positions,
evaluate the jogger field, write albedo + mask together (same rasterizer
construction as the denim companion; the paint field is what differs)."""
a, b, c = uvs
A, B, C = cos
ax, ay = a.x * (W - 1), a.y * (H - 1)
bx, by = b.x * (W - 1), b.y * (H - 1)
cx, cy = c.x * (W - 1), 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]
pxg = xs + 0.5
pyg = ys + 0.5
w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom
w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom
w2 = 1.0 - w0 - w1
inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4)
if not inside.any():
return
w0i, w1i, w2i = w0[inside], w1[inside], w2[inside]
px3 = w0i * A.x + w1i * B.x + w2i * C.x
py3 = w0i * A.y + w1i * B.y + w2i * C.y
pz3 = w0i * A.z + w1i * B.z + w2i * C.z
ysin = ys[inside]
xsin = xs[inside]
alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm)
alb_buf[ysin, xsin] = alb
mask_buf[ysin, xsin] = mask
def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body):
"""Rasterize all UV0 triangles once, producing the painted albedo and the
region mask from one shared feature-field evaluation per texel."""
W = H = TEX_SIZE
rng = np.random.default_rng(NOISE_SEED)
noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5)
* 2.0 * ALBEDO_NOISE)
alb_buf = np.empty((H, W, 4), dtype=np.float32)
for c in range(3):
alb_buf[:, :, c] = FLEECE_RGB[c] + noise_buf
alb_buf[:, :, 3] = 1.0
mask_buf = np.zeros((H, W, 4), dtype=np.float32)
mask_buf[:, :, 1] = 1.0 # background = legs green (bleed-safe)
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
bm.faces.ensure_lookup_table()
if not len(bm.loops.layers.uv):
raise RuntimeError("no UV layer for albedo/mask paint")
uv_layer = bm.loops.layers.uv[0]
tri_count = 0
for face in bm.faces:
loops = face.loops[:]
uvs = [loop[uv_layer].uv.copy() for loop in loops]
cos = [loop.vert.co.copy() for loop in loops]
for i in range(1, len(uvs) - 1):
_raster_tri_paint(
alb_buf, mask_buf, noise_buf,
(uvs[0], uvs[i], uvs[i + 1]),
(cos[0], cos[i], cos[i + 1]),
lm, W, H)
tri_count += 1
bm.free()
log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})")
def _save(buf, name, path):
img = bpy.data.images.new(name, W, H, alpha=True)
img.pixels.foreach_set(buf.reshape(-1))
img.update()
img.filepath_raw = path
img.file_format = 'PNG'
img.save()
return img
albedo_img = _save(alb_buf, f"jogger_albedo_{body}", albedo_path)
_save(mask_buf, f"jogger_mask_{body}", mask_path)
log(f"saved albedo -> {albedo_path}")
log(f"saved mask -> {mask_path}")
return albedo_img
# --------------------------------------------------------------------------
# Per-body authoring
# --------------------------------------------------------------------------
def author_jogger_shell(body_dir, out_dir, body):
crotch_z, _hips_top = denim.probe_hips_bounds(body_dir)
base.clear_scene()
base.COVERED_SEGMENTS = COVERED_SEGMENTS
shell, armature = base.build_covered_mesh(body_dir)
denim.weld_boundaries(shell)
zs = [v.co.z for v in shell.data.vertices]
waist_z, ankle_z = max(zs), min(zs)
lm = denim.LegLandmarks(armature, waist_z, ankle_z, crotch_z + 0.005)
# Full-length hem AT the ankle joint (calf tail); rim teeth flattened onto
# clean planes pre-offset (denim practice — no jagged silhouette).
ankle_plane = float(lm.leg_z_pts[0])
waist_plane, ankle_plane = denim.flatten_open_rims(shell, ankle_plane)
span = waist_plane - ankle_plane
knee_z = float(lm.leg_z_pts[1]) # calf head
cuff_top = ankle_plane + CUFF_FRAC * span
tapered_offset(shell, knee_z, cuff_top, OFFSET_HI_M, OFFSET_MID_M,
OFFSET_CUFF_M, CUFF_STEP_W_M)
denim.waist_flare(shell, waist_plane, BAND_FRAC * span, WAIST_FLARE_M)
base.solidify(shell, base.CLOTH_THICKNESS_M)
denim.clamp_waist_residue(shell, waist_plane)
# Landmarks reference the CLEAN rims for the paint/mask pass.
finalize_jogger_landmarks(lm, waist_plane, ankle_plane, BAND_FRAC,
CUFF_FRAC)
denim.author_parked_uv2(shell)
albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png")
mask_path = os.path.join(out_dir, f"{body}_mask.png")
albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body)
base.assign_fabric_material(shell, albedo_img)
base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb"))
def main():
global OFFSET_HI_M, OFFSET_MID_M, OFFSET_CUFF_M, BAND_FRAC, CUFF_FRAC
global STRIPE_W_M, FLEECE_RGB, STRIPE_RGB, WAIST_FLARE_M, NOISE_SEED, PLAIN
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] [--mid-offset M] [--cuff-offset M] "
"[--band-frac F] [--cuff-frac F] [--stripe-width M] "
"[--fabric r,g,b] [--stripe-rgb r,g,b] [--waist-flare M] "
"[--seed N] [--plain]")
sys.exit(1)
bodies_root = argv[0]
out_dir = argv[1]
if "--offset" in argv:
OFFSET_HI_M = float(argv[argv.index("--offset") + 1])
if "--mid-offset" in argv:
OFFSET_MID_M = float(argv[argv.index("--mid-offset") + 1])
if "--cuff-offset" in argv:
OFFSET_CUFF_M = float(argv[argv.index("--cuff-offset") + 1])
if "--band-frac" in argv:
BAND_FRAC = float(argv[argv.index("--band-frac") + 1])
if "--cuff-frac" in argv:
CUFF_FRAC = float(argv[argv.index("--cuff-frac") + 1])
if "--stripe-width" in argv:
STRIPE_W_M = float(argv[argv.index("--stripe-width") + 1])
if "--fabric" in argv:
FLEECE_RGB = tuple(
float(v) for v in argv[argv.index("--fabric") + 1].split(","))
if "--stripe-rgb" in argv:
STRIPE_RGB = tuple(
float(v) for v in argv[argv.index("--stripe-rgb") + 1].split(","))
if "--waist-flare" in argv:
WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1])
if "--seed" in argv:
NOISE_SEED = int(argv[argv.index("--seed") + 1])
if "--plain" in argv:
PLAIN = True
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)
log(f"joggers per-body mode: {len(bodies)} bodies, offset "
f"{OFFSET_HI_M * 1000:.0f}->{OFFSET_CUFF_M * 1000:.1f} mm, "
f"band_frac={BAND_FRAC} cuff_frac={CUFF_FRAC} "
f"stripe={STRIPE_W_M * 1000:.0f} mm, plain={PLAIN}")
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_jogger_shell(body_dir, out_dir, body)
results.append((body, "ok"))
except Exception as exc:
log(f"ERROR {body}: {exc}")
import traceback
traceback.print_exc()
results.append((body, f"error: {exc}"))
ref = base.REFERENCE_BODY
ref_mask = os.path.join(out_dir, f"{ref}_mask.png")
if os.path.isfile(ref_mask):
shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png"))
log(f"copied {ref}_mask.png -> reference_mask.png (fallback)")
ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png")
if os.path.isfile(ref_alb):
shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png"))
log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)")
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()