Files
jpmschweitzerandClaude Opus 5 201dabd19b refactor(tooling): T-1273 — the Blender carve-out, and a guard that keeps it carved
35 payloads move to tooling/scripts/blender/ and stay outside package scope.
They run under Blender's bundled Python, which cannot see the repo venv, so
they physically cannot import tooling.core — holding them to the D-263 contract
would either fail the gate forever or force the contract to be weakened for
everyone, and the second is how a gate stops meaning anything.

Count verified by import rather than filename: 33 import bpy/bmesh directly,
and the two that do not are still payloads per their own usage lines.
garment-fit/make_logo.py is the one genuine non-payload and stays for T-1290.

The bash wrapper is retired rather than kept. Keeping it would have put the
install-resolution logic in two places, which is the duplication T-1286 had
just finished collapsing three copies of. domains/blender/service.py owns the
decisions — resolve_blender (native beats flatpak, ordering preserved),
resolve_payload, absolutise — and only run_payload performs. test_blender.py
pins all of them without launching Blender, which matters here more than
usual: the thing being launched is a 200 MB GUI application that writes GLBs.

`reach blender run` takes a registered payload name OR a path to any script,
because the wrapper served both — the spikes and the glb-gen skill hand it
one-off scripts of their own. An unknown name enumerates all 35 and exits 2.

The exclusion now defends itself. check_carve_out_stays_carved fails if
`scripts` is added to PACKAGE_ROOTS, if the payload directory empties (an empty
exclusion proves nothing), or if an __init__.py appears there (which would make
the payloads importable — the coupling the carve-out exists to prevent). All
three arms mutation-proved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:55:52 +02:00

512 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):
tooling/blender --background --python \
tooling/garment-fit/blender_author_joggers.py -- \
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()