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

677 lines
28 KiB
Python

"""
blender_author_slides.py (T-1089 wave 2, slides — open swim footwear)
Authors SLIDES (open sandal: flat sole slab + one broad strap band across the
midfoot) as per-body offset shells, reusing blender_author_offset_shell.py as
a library (scene build/join, offset, solidify, GLB export) and the denim
companion's required bottoms/footwear practices (boundary WELD of coincident
segment-seam verts before offsetting; the parked logo UV2 layer).
First offset-shell FOOTWEAR: follows the peasant_shoes both-feet convention
(one garment covers seg_foot_l + seg_foot_r), and adds what feet need that no
torso/leg companion provides, as reusable parameters:
* STRAP band cut — the foot shell is cut down to just the midfoot band
between two Y planes derived from that body's own foot bones
(fractions of the ball_l/r head -> foot_l/r head span, identical 65-bone
rig on all 11 bodies). Probe evidence: the seg_foot ankle rim (the
weight-threshold splitter's jagged boundary, 3.3-8.3 cm teeth post-weld)
stays ABOVE y-fraction ~0.62 of that span on every body, so a band cut at
<= 0.58 removes the entire jagged rim by construction. The band keeps the
full cross-section ring (including under-foot skin) so the strap-to-sole
join is gap-free by construction (the coverall waist-join pattern); the
hidden under-foot part is swallowed by the sole slab.
* clean strap rims by PLANE BISECT — the wave-1 delete-then-flatten rim
practice is too crude for the foot's large instep triangles (it notches
the strap crest); the band is instead cut with two exact bisect planes,
which yields the same clean-plane end state the denim flatten was after,
by construction (bmesh interpolates weights/UVs on the new edge verts).
A rim check verifies every open-edge vert sits on a cut plane.
* SOLE slab construction — per foot, the 2D convex hull (monotone chain) of
that foot's full footprint, expanded radially by a margin, extruded into
a prism from below the skin's lowest point to just above it (the foot
visually rests IN the footbed). Sole verts receive skin weights by
nearest-vertex transfer from the pre-cut foot snapshot, so the sole bends
with the foot/ball bones during Walk/Sprint toe-off. A FLEX CREASE ring
is bisected into each prism at the ball-joint line so the slab hinges
where the foot hinges (QA evidence: without it, toe skin dips through
the linearly-interpolated top face in deep-crouch toe-off).
* under-sole clamp — strap ring verts that offset/solidify pushed below the
sole interior are clamped onto a plane inside the slab (invisible), so the
strap never pokes out of the sole bottom.
* deterministic region UVs — the garment is fully re-UV'd (the foot's body
atlas layout is useless for garment paint): strap faces pack into the left
half of UV space, sole faces into the right half, each planar-projected by
dominant normal axis into three stacked tiles. Faces of one region may
overlap in UV (they share one flat colour), but strap and sole texels are
DISJOINT by construction — no cross-region contamination, no operator
(bpy.ops.uv.*) dependency in background mode.
* albedo + region mask painted together per face: sole -> R (tint_0),
strap -> G (tint_1); bright default albedo (spec: swim family, bright).
Painted texels are dilated outward so bilinear/mip bleed at tile edges
never lands on an untinted texel.
Usage (per-body only — offset shells author per body, Q-060):
tooling/blender --background --python \
tooling/garment-fit/blender_author_slides.py -- \
client/assets/characters/bodies \
client/assets/characters/clothing/slides \
[--bodies average_m,child,...] [--offset 0.005] \
[--strap-y0 0.10] [--strap-y1 0.58] \
[--sole-margin 0.007] [--sole-embed 0.009] [--sole-drop 0.010] \
[--strap-rgb 0.93,0.35,0.20] [--sole-rgb 0.82,0.83,0.85] [--seed N]
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 base offset-shell module + the denim companion (shared machinery)
# --------------------------------------------------------------------------
_HERE = os.path.dirname(os.path.abspath(__file__))
def _load(name, fname):
spec = importlib.util.spec_from_file_location(name, os.path.join(_HERE, fname))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
base = _load("offset_shell_base", "blender_author_offset_shell.py")
denim = _load("denim_pants_lib", "blender_author_denim_pants.py")
log = base.log
# --------------------------------------------------------------------------
# Parameters
# --------------------------------------------------------------------------
COVERED_SEGMENTS = ["seg_foot_l", "seg_foot_r"]
STRAP_OFFSET_M = 0.005 # strap standoff from skin (slides hug the foot)
STRAP_THICKNESS_M = 0.005 # Solidify thickness (chunky foam strap)
# Strap band window as fractions of the ball-head -> foot(ankle)-head Y span,
# measured from the ball. Probe: the jagged seg_foot ankle rim starts at
# fraction ~0.62 on every body, so y1 <= 0.58 excludes it by construction.
STRAP_Y0_FRAC = 0.10
STRAP_Y1_FRAC = 0.58
# Sole slab (metres on average_m; scaled by each body's foot-length ratio).
SOLE_MARGIN_M = 0.007 # radial footprint expansion beyond the skin hull
SOLE_EMBED_M = 0.009 # slab top above the foot's lowest skin point
SOLE_DROP_M = 0.010 # slab bottom below the foot's lowest skin point
CLAMP_INSET_M = 0.003 # strap under-foot verts clamped this far above
# the slab bottom (kept inside the sole)
# Bright default (spec: swim family). Texture carries identity; the runtime
# toon_garment.gdshader recolors per region via luma, so these tones ARE the
# default look and default_tints should match them.
STRAP_RGB = (0.93, 0.35, 0.20) # bright coral strap (G region, tint_1)
SOLE_RGB = (0.82, 0.83, 0.85) # off-white foam sole (R region, tint_0)
ALBEDO_NOISE = 0.018 # +/- jitter, subtle foam/EVA feel
NOISE_SEED = 3089
TEX_SIZE = 512 # slides are small; 512 is plenty
DILATE_PX = 6 # painted-texel dilation into the background
_REF_FOOT_SPAN = 0.2211 # average_m: foot_l head y (0.0875) - ball_l tail y (-0.1336)
# --------------------------------------------------------------------------
# Per-body landmarks
# --------------------------------------------------------------------------
class FootLandmarks:
"""Per-side cut/sole parameters from one body's own foot bones + mesh."""
def __init__(self, armature):
bones = armature.data.bones
self.sides = {}
spans = []
for side, sign in (("l", +1), ("r", -1)):
foot = bones.get(f"foot_{side}")
ball = bones.get(f"ball_{side}")
if foot is None or ball is None:
raise RuntimeError(
f"foot_{side}/ball_{side} missing — not the 65-bone rig?")
ankle_y = foot.head_local.y
ball_y = ball.head_local.y
toe_y = ball.tail_local.y
span = ankle_y - toe_y
spans.append(span)
self.sides[sign] = {
"y0": ball_y + STRAP_Y0_FRAC * (ankle_y - ball_y),
"y1": ball_y + STRAP_Y1_FRAC * (ankle_y - ball_y),
"ball_y": ball_y,
}
self.scale = (sum(spans) / len(spans)) / _REF_FOOT_SPAN
for sign in (+1, -1):
s = self.sides[sign]
log(f"landmarks side {'L' if sign > 0 else 'R'}: "
f"strap y[{s['y0']:.4f},{s['y1']:.4f}]")
log(f"foot scale vs average_m: {self.scale:.3f}")
# --------------------------------------------------------------------------
# Geometry: snapshot, strap cut, rim flatten, sole build
# --------------------------------------------------------------------------
def snapshot_skin(shell):
"""Record post-weld skin verts: positions + per-vertex group weights.
Used later for the sole's nearest-vertex weight transfer, after the strap
cut has thrown most of the foot away.
"""
me = shell.data
pos = np.array([(v.co.x, v.co.y, v.co.z) for v in me.vertices],
dtype=np.float64)
weights = [
[(g.group, g.weight) for g in v.groups if g.weight > 0.0]
for v in me.vertices
]
return pos, weights
def strap_cut(shell, lm):
"""Cut the foot shell down to the strap band with two exact plane bisects.
The wave-1 bottoms practice (delete-then-flatten) is too crude here: the
foot mesh's instep triangles are large relative to the 5-7 cm band, so
vertex deletion notches the strap crest and rim-snapping folds it. Bisect
planes give clean straight rims BY CONSTRUCTION (bmesh interpolates
weights/UVs on the new edge verts), which is the same end state the denim
flatten was after. The rig's feet are mirrored, so one Y window (side
average) serves both feet.
"""
y0 = (lm.sides[+1]["y0"] + lm.sides[-1]["y0"]) / 2.0
y1 = (lm.sides[+1]["y1"] + lm.sides[-1]["y1"]) / 2.0
bm = bmesh.new()
bm.from_mesh(shell.data)
before = len(bm.verts)
bmesh.ops.bisect_plane(
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
plane_co=(0.0, y0, 0.0), plane_no=(0.0, 1.0, 0.0),
clear_inner=True) # drop y < y0 (toe side)
bmesh.ops.bisect_plane(
bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
plane_co=(0.0, y1, 0.0), plane_no=(0.0, 1.0, 0.0),
clear_outer=True) # drop y > y1 (ankle side, incl. the jagged rim)
bm.to_mesh(shell.data)
bm.free()
shell.data.update()
log(f"strap band bisect y[{y0:.4f},{y1:.4f}]: {before} -> "
f"{len(shell.data.vertices)} verts")
if len(shell.data.vertices) == 0:
raise RuntimeError("strap cut removed everything — window wrong?")
def check_strap_rims(shell, lm):
"""Verify the band's open edges sit ON the two cut planes (bisect gives
this by construction; residue means ankle-rim leakage into the window)."""
y0 = (lm.sides[+1]["y0"] + lm.sides[-1]["y0"]) / 2.0
y1 = (lm.sides[+1]["y1"] + lm.sides[-1]["y1"]) / 2.0
bm = bmesh.new()
bm.from_mesh(shell.data)
bm.verts.ensure_lookup_table()
boundary = set()
for e in bm.edges:
if len(e.link_faces) == 1:
boundary.update(v.index for v in e.verts)
off = [i for i in boundary
if min(abs(bm.verts[i].co.y - y0), abs(bm.verts[i].co.y - y1)) > 1e-4]
bm.free()
log(f"strap rims: {len(boundary)} boundary verts, {len(off)} off-plane "
f"(expected 0)")
if off:
log("WARNING: off-plane rim verts — ankle-rim residue inside the "
"band window; lower STRAP_Y1_FRAC")
def _convex_hull_2d(points):
"""Andrew's monotone chain; returns CCW hull points (numpy (H,2))."""
pts = np.unique(np.round(points, 6), axis=0)
order = np.lexsort((pts[:, 1], pts[:, 0]))
pts = pts[order]
if len(pts) < 3:
raise RuntimeError("degenerate footprint for convex hull")
def cross(o, a, b):
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
lower = []
for p in pts:
while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
lower.pop()
lower.append(p)
upper = []
for p in pts[::-1]:
while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
upper.pop()
upper.append(p)
return np.array(lower[:-1] + upper[:-1], dtype=np.float64)
def build_soles(shell, skin_pos, lm, margin, embed, drop):
"""Append one sole prism per foot; returns (sole_start_index, sole_info).
Footprint = expanded convex hull of that foot's FULL skin xy (pre-cut
snapshot); prism spans z in [skin_min - drop, skin_min + embed].
FLEX CREASE: each prism is bisected (no material removed) by a Y plane at
that side's ball-joint line. Without it the long heel->toe top face
interpolates skinning linearly across the whole span while the foot
creases sharply at the ball during Sprint/Crouch toe-off — the slab sags
below the bend crest and toe skin dips through (QA evidence, wave-2
slides). The crease ring picks up ball-blended weights from the
nearest-vertex transfer, so the slab hinges where the foot hinges.
"""
me = shell.data
sole_start = len(me.vertices)
bm = bmesh.new()
bm.from_mesh(me)
info = {}
for sign in (+1, -1):
if sign > 0:
side_pos = skin_pos[skin_pos[:, 0] >= 0.0]
else:
side_pos = skin_pos[skin_pos[:, 0] < 0.0]
if len(side_pos) == 0:
raise RuntimeError("no skin verts on one side for sole build")
hull = _convex_hull_2d(side_pos[:, :2])
centroid = hull.mean(axis=0)
d = hull - centroid
n = d / np.linalg.norm(d, axis=1, keepdims=True)
hull = hull + n * margin
z_min = float(side_pos[:, 2].min())
z_bot, z_top = z_min - drop, z_min + embed
info[sign] = {"z_bot": z_bot, "z_top": z_top}
bot = [bm.verts.new((p[0], p[1], z_bot)) for p in hull]
top = [bm.verts.new((p[0], p[1], z_top)) for p in hull]
new_faces = []
h = len(hull)
for i in range(h):
j = (i + 1) % h
new_faces.append(bm.faces.new((bot[i], bot[j], top[j], top[i])))
new_faces.append(bm.faces.new(top))
new_faces.append(bm.faces.new(tuple(reversed(bot))))
bmesh.ops.recalc_face_normals(bm, faces=new_faces)
# Flex crease at the ball line (cut only, keep both sides).
crease_verts = set()
for f in new_faces:
crease_verts.update(f.verts)
crease_edges = {e for v in crease_verts for e in v.link_edges}
res = bmesh.ops.bisect_plane(
bm,
geom=list(crease_verts) + list(crease_edges) + new_faces,
plane_co=(0.0, lm.sides[sign]["ball_y"], 0.0),
plane_no=(0.0, 1.0, 0.0),
clear_inner=False, clear_outer=False)
cut = sum(1 for g in res["geom_cut"] if isinstance(g, bmesh.types.BMVert))
log(f"sole {'L' if sign > 0 else 'R'}: hull {h} pts, "
f"z[{z_bot:.4f},{z_top:.4f}], ball crease "
f"y={lm.sides[sign]['ball_y']:.4f} ({cut} crease verts)")
bm.to_mesh(me)
bm.free()
me.update()
log(f"soles appended: verts {sole_start} -> {len(me.vertices)}")
return sole_start, info
def transfer_sole_weights(shell, skin_pos, skin_weights, sole_start):
"""Nearest-vertex weight transfer (same-side skin snapshot) for sole verts."""
me = shell.data
left = skin_pos[:, 0] >= 0.0
idx_by_side = {+1: np.where(left)[0], -1: np.where(~left)[0]}
transferred = 0
for vi in range(sole_start, len(me.vertices)):
co = me.vertices[vi].co
side = +1 if co.x >= 0.0 else -1
cand = idx_by_side[side]
d2 = ((skin_pos[cand] - np.array([co.x, co.y, co.z])) ** 2).sum(axis=1)
src = int(cand[int(np.argmin(d2))])
for gi, w in skin_weights[src]:
shell.vertex_groups[gi].add([vi], w, 'REPLACE')
transferred += 1
log(f"sole weights transferred: {transferred} verts (nearest skin vert)")
def clamp_strap_under_sole(shell, sole_start, sole_info):
"""Clamp strap verts that dipped below the sole interior back inside it."""
me = shell.data
n = 0
for vi in range(sole_start):
v = me.vertices[vi]
side = +1 if v.co.x >= 0.0 else -1
floor_z = sole_info[side]["z_bot"] + CLAMP_INSET_M
if v.co.z < floor_z:
v.co.z = floor_z
n += 1
me.update()
if n:
log(f"clamped {n} strap verts above the sole bottom (hidden in slab)")
# --------------------------------------------------------------------------
# Deterministic region UVs (no bpy.ops dependency)
# --------------------------------------------------------------------------
def _dominant_axis(normal):
a = (abs(normal.x), abs(normal.y), abs(normal.z))
return a.index(max(a))
def author_region_uvs(shell, sole_start):
"""Re-UV the garment: strap faces -> left half, sole faces -> right half,
each split into three stacked tiles by dominant normal axis (planar
projection). Faces within one (region, axis) tile may overlap — harmless,
they share one flat colour — but strap/sole texels never mix."""
me = shell.data
while len(me.uv_layers) > 1:
me.uv_layers.remove(me.uv_layers[-1])
if len(me.uv_layers) == 0:
me.uv_layers.new(name="UVMap")
bm = bmesh.new()
bm.from_mesh(me)
bm.faces.ensure_lookup_table()
bm.normal_update()
uvl = bm.loops.layers.uv[0]
# region 0 = strap (u 0.02..0.48), region 1 = sole (u 0.52..0.98)
u_ranges = {0: (0.02, 0.48), 1: (0.52, 0.98)}
proj = {0: (1, 2), 1: (0, 2), 2: (0, 1)} # axis -> (coord_a, coord_b)
buckets = {}
for face in bm.faces:
region = 1 if all(v.index >= sole_start for v in face.verts) else 0
axis = _dominant_axis(face.normal)
buckets.setdefault((region, axis), []).append(face)
for (region, axis), faces in buckets.items():
ca, cb = proj[axis]
pts = []
for f in faces:
for lo in f.loops:
pts.append((lo.vert.co[ca], lo.vert.co[cb]))
pts = np.array(pts)
lo_a, hi_a = float(pts[:, 0].min()), float(pts[:, 0].max())
lo_b, hi_b = float(pts[:, 1].min()), float(pts[:, 1].max())
da = max(hi_a - lo_a, 1e-6)
db = max(hi_b - lo_b, 1e-6)
u0, u1 = u_ranges[region]
v0 = 0.02 + axis * (1.0 / 3.0)
v1 = v0 + (1.0 / 3.0) - 0.04
for f in faces:
for lo in f.loops:
a = (lo.vert.co[ca] - lo_a) / da
b = (lo.vert.co[cb] - lo_b) / db
lo[uvl].uv = (u0 + a * (u1 - u0), v0 + b * (v1 - v0))
bm.to_mesh(me)
bm.free()
me.update()
log(f"region UVs authored: {len(buckets)} (region,axis) tiles")
# --------------------------------------------------------------------------
# Paint albedo + mask (per-face flat colours, shared rasterization)
# --------------------------------------------------------------------------
def _raster_tri_multi(bufs_colors, a, b, c, W, H):
"""Barycentric fill of one UV triangle into several (buf, color) pairs."""
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]
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
for buf, color in bufs_colors:
region = buf[miny:maxy + 1, minx:maxx + 1]
region[inside] = np.array(color, dtype=buf.dtype)
def _dilate_painted(alb, mask, flag, iters):
"""Grow painted texels into the background so bilinear/mip bleed at tile
edges picks up real region colours, not the background fill."""
H, W = flag.shape
for _ in range(iters):
grew = np.zeros_like(flag)
for dy, dx in ((1, 0), (-1, 0), (0, 1), (0, -1)):
src = np.zeros_like(flag)
sy0, sy1 = max(dy, 0), H + min(dy, 0)
ty0, ty1 = max(-dy, 0), H + min(-dy, 0)
sx0, sx1 = max(dx, 0), W + min(dx, 0)
tx0, tx1 = max(-dx, 0), W + min(-dx, 0)
src[ty0:ty1, tx0:tx1] = flag[sy0:sy1, sx0:sx1]
fill = (~flag) & (~grew) & src
if not fill.any():
continue
alb_src = np.zeros_like(alb)
alb_src[ty0:ty1, tx0:tx1] = alb[sy0:sy1, sx0:sx1]
mask_src = np.zeros_like(mask)
mask_src[ty0:ty1, tx0:tx1] = mask[sy0:sy1, sx0:sx1]
alb[fill] = alb_src[fill]
mask[fill] = mask_src[fill]
grew |= fill
flag |= grew
def paint_albedo_and_mask(shell, sole_start, albedo_path, mask_path, body):
"""Rasterize all UV0 triangles once: sole -> R + sole tone, strap -> G +
strap tone. One classification drives both outputs (denim practice)."""
W = H = TEX_SIZE
alb_buf = np.empty((H, W, 4), dtype=np.float32)
for c in range(3):
alb_buf[:, :, c] = STRAP_RGB[c]
alb_buf[:, :, 3] = 1.0
mask_buf = np.zeros((H, W, 4), dtype=np.float32)
mask_buf[:, :, 1] = 1.0 # background = strap green (bleed-safe default)
flag_buf = np.zeros((H, W), dtype=bool)
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
bm.faces.ensure_lookup_table()
uvl = bm.loops.layers.uv[0]
counts = {"sole": 0, "strap": 0}
for face in bm.faces:
is_sole = all(v.index >= sole_start for v in face.verts)
counts["sole" if is_sole else "strap"] += 1
alb_rgb = SOLE_RGB if is_sole else STRAP_RGB
mask_rgba = (1.0, 0.0, 0.0, 0.0) if is_sole else (0.0, 1.0, 0.0, 0.0)
alb_rgba = (alb_rgb[0], alb_rgb[1], alb_rgb[2], 1.0)
uvs = [lo[uvl].uv.copy() for lo in face.loops]
flag_view = flag_buf[:, :, None] # view — writes reach flag_buf
for i in range(1, len(uvs) - 1):
_raster_tri_multi(
[(alb_buf, alb_rgba), (mask_buf, mask_rgba),
(flag_view, True)],
uvs[0], uvs[i], uvs[i + 1], 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()))
_dilate_painted(alb_buf, mask_buf, flag_buf, DILATE_PX)
rng = np.random.default_rng(NOISE_SEED)
noise = ((rng.random((H, W, 1), dtype=np.float32) - 0.5)
* 2.0 * ALBEDO_NOISE)
alb_buf[:, :, :3] = np.clip(alb_buf[:, :, :3] + noise, 0.0, 1.0)
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"slides_albedo_{body}", albedo_path)
_save(mask_buf, f"slides_mask_{body}", mask_path)
log(f"saved albedo -> {albedo_path}")
log(f"saved mask -> {mask_path}")
# The glTF exporter names the embedded image after the file basename, and
# the Godot import EXTRACTS it as <glb>_<imagename>.png. Point the image at
# the shared base_albedo.png so the extraction lands exactly on the sidecar
# saved above (<body>_base_albedo.png, same pixels — hoodie/tshirt
# convention), instead of a doubled <body>_<body>_base_albedo.png whose
# deletion would break the imported scene. The shared file is re-pointed
# to the reference body's paint at the end of the run (main()).
albedo_img.filepath_raw = os.path.join(os.path.dirname(albedo_path),
"base_albedo.png")
albedo_img.save()
return albedo_img
# --------------------------------------------------------------------------
# Per-body authoring
# --------------------------------------------------------------------------
def author_slides(body_dir, out_dir, body, offset):
base.clear_scene()
base.COVERED_SEGMENTS = COVERED_SEGMENTS
shell, armature = base.build_covered_mesh(body_dir)
denim.weld_boundaries(shell) # merge UV-seam/segment duplicate verts
lm = FootLandmarks(armature)
skin_pos, skin_weights = snapshot_skin(shell)
strap_cut(shell, lm)
check_strap_rims(shell, lm)
base.offset_outward(shell, offset)
base.solidify(shell, STRAP_THICKNESS_M)
sole_start, sole_info = build_soles(
shell, skin_pos, lm,
SOLE_MARGIN_M * lm.scale, SOLE_EMBED_M * lm.scale,
SOLE_DROP_M * lm.scale)
transfer_sole_weights(shell, skin_pos, skin_weights, sole_start)
clamp_strap_under_sole(shell, sole_start, sole_info)
author_region_uvs(shell, sole_start)
denim.author_parked_uv2(shell) # slides are not logo-capable
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, sole_start, 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 STRAP_Y0_FRAC, STRAP_Y1_FRAC, STRAP_RGB, SOLE_RGB
global SOLE_MARGIN_M, SOLE_EMBED_M, SOLE_DROP_M, NOISE_SEED
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] [--strap-y0 F] [--strap-y1 F] [--sole-margin M] "
"[--sole-embed M] [--sole-drop M] [--strap-rgb r,g,b] "
"[--sole-rgb r,g,b] [--seed N]")
sys.exit(1)
bodies_root = argv[0]
out_dir = argv[1]
def _f(flag, default):
return float(argv[argv.index(flag) + 1]) if flag in argv else default
def _rgb(flag, default):
if flag not in argv:
return default
return tuple(float(v) for v in argv[argv.index(flag) + 1].split(","))
offset = _f("--offset", STRAP_OFFSET_M)
STRAP_Y0_FRAC = _f("--strap-y0", STRAP_Y0_FRAC)
STRAP_Y1_FRAC = _f("--strap-y1", STRAP_Y1_FRAC)
SOLE_MARGIN_M = _f("--sole-margin", SOLE_MARGIN_M)
SOLE_EMBED_M = _f("--sole-embed", SOLE_EMBED_M)
SOLE_DROP_M = _f("--sole-drop", SOLE_DROP_M)
NOISE_SEED = int(_f("--seed", NOISE_SEED))
STRAP_RGB = _rgb("--strap-rgb", STRAP_RGB)
SOLE_RGB = _rgb("--sole-rgb", SOLE_RGB)
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"slides per-body mode: {len(bodies)} bodies, strap offset "
f"{offset * 1000:.0f} mm, band frac [{STRAP_Y0_FRAC},{STRAP_Y1_FRAC}]")
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_slides(body_dir, out_dir, body, offset)
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()