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

472 lines
20 KiB
Python

"""
blender_author_swim_trunks.py (T-1089 wave 2, swim set — swim_trunks)
Authors bright SWIM TRUNKS as per-body offset shells: hips + upper legs with
the hem clearly ABOVE the knee (shorter than shorts_modern's 0.78 thigh
fraction), a painted centre-front drawstring (knot + two hanging cords) and a
lateral SIDE PANEL colour block down each leg.
Regions (RGBA mask, toon_garment.gdshader):
waistband -> R (tint_0), body -> G (tint_1), side panel -> B (tint_2)
Companion to blender_author_offset_shell.py (imported as a library — scene
build/join, offset, solidify, GLB export). The bottoms-specific practices are
REUSED from the proven wave-1 companions, not rediscovered:
* denim (blender_author_denim_pants.py): boundary WELD of coincident
segment-seam rings before offsetting (un-welded rings offset apart along
diverging normals -> cracks), open-rim FLATTENING onto clean planes (the
segment splitter leaves 4.5-7.6 cm jagged teeth at the waist/hem rims),
the feathered --waist-flare deep-crouch mitigation, the post-solidify
waist residue clamp, and the parked logo TEXCOORD_1 layer (trunks are not
logo-capable, but toon_garment.gdshader samples UV2 unconditionally).
* legs (blender_author_offset_shell_legs.py): the bone-plane hem cut.
Like the denim script, albedo and region mask are painted TEXEL-level from
ONE analytic feature-field evaluation per texel (UV0 triangles rasterized
with barycentric-interpolated 3D positions), so the painted drawstring/panel
and the recolor regions always agree:
- albedo: bright saturated default (coral body, teal side panel), painted
drawstring knot + hanging cords (centre front), waistband border
+ hem border stitches, side-panel edge piping — flat and
toon-friendly, identity carried by the texture (style pin:
modern only).
- mask: waistband R, body G, side panel B.
All cut/mask/paint parameters derive PER BODY from that body's own bone
landmarks (thigh_l/r) and measured mesh extents (waist rim valley, hem
plane), scaled by the body's garment span and hip half-width — the same
proportional-ratio philosophy as base.derive_thresholds. Per-body mode only
(offset shells author per body, Q-060).
Usage (swim_trunks reference invocation):
reach blender run blender_author_swim_trunks \
client/assets/characters/bodies \
client/assets/characters/clothing/swim_trunks \
[--bodies average_m,child,...] [--offset 0.012] [--hem-frac 0.60] \
[--band-frac 0.14] [--panel-frac 0.42] [--waist-flare 0.007] \
[--base-rgb 0.95,0.45,0.35] [--panel-rgb 0.13,0.68,0.64] [--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 wave-1 modules (shared machinery — reused, not copied)
# --------------------------------------------------------------------------
_HERE = os.path.dirname(os.path.abspath(__file__))
def _load(mod_name, file_name):
spec = importlib.util.spec_from_file_location(
mod_name, os.path.join(_HERE, file_name))
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", "blender_author_denim_pants.py")
legs = _load("offset_shell_legs", "blender_author_offset_shell_legs.py")
log = base.log
FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base)
# --------------------------------------------------------------------------
# Parameters (defaults = swim_trunks)
# --------------------------------------------------------------------------
COVERED_SEGMENTS = ["seg_hips", "seg_leg_upper_l", "seg_leg_upper_r"]
HEM_FRAC = 0.60 # fraction of the thigh bone KEPT below its head.
# 0.60 puts the hem ~17 cm above the knee on
# average_m — clearly shorter than shorts_modern's
# 0.78 (~9 cm above the knee), per the swim spec.
TEX_SIZE = 1024 # albedo + mask resolution (painted details need >512)
WAIST_FLARE_M = 0.007 # feathered radial stand-off at the waistband rim
# (deep-crouch waist-fold mitigation — QA evidence
# from the peasant + jeans sets, reused from denim)
# Vertical proportions — fractions of the garment span (waist_z - hem_z).
BAND_FRAC = 0.14 # waistband height (R region) (~4.4 cm on average_m)
SEAM_W_FRAC = 0.019 # painted stitch/piping line width (~6 mm on average_m)
HEM_STITCH_FRAC = 0.028 # hem border stitch centre height above the hem
CORD_LEN_FRAC = 0.20 # drawstring cord drop below the knot (~6.3 cm)
# Horizontal proportions — fractions of the hip half-width (|thigh head x|).
PANEL_HW_FRAC = 0.42 # side-panel half ARC width (~3.8 cm -> 7.6 cm panel)
# Drawstring metrics in metres on average_m, scaled by the body's hip ratio.
KNOT_R_M = 0.009 # knot blob radius
CORD_W_M = 0.0055 # cord line width
CORD_X0_M = 0.011 # cord |x| offset just below the knot
CORD_SLANT = 0.28 # outward drift of the cords per metre of drop
# Swim style (sRGB floats; saved as-is — matches the proven base pipeline).
# Bright saturated default per spec: coral body / teal side panel. Luma of
# both stays near the toon_garment.gdshader recolor sweet spot (~0.5-0.6).
CORAL_RGB = (0.95, 0.45, 0.35) # body + waistband base (luma ~0.59)
TEAL_RGB = (0.13, 0.68, 0.64) # side panel (luma ~0.51)
TRIM_RGB = (0.97, 0.95, 0.90) # drawstring + stitches/piping (cream)
BAND_SHADE = 0.93 # waistband albedo darkening (reads untinted)
ALBEDO_NOISE = 0.020 # +/- woven jitter
PLAIN = False # --plain: skip drawstring/stitch/piping paint
NOISE_SEED = 3089
# Reference proportions (average_m) the fractions were calibrated against:
# waist rim valley 1.0312, hem(0.60) 0.7139 -> span 0.3173; |thigh head x|.
_REF_HIP_X = 0.0906
# --------------------------------------------------------------------------
# Per-body landmarks
# --------------------------------------------------------------------------
class TrunkLandmarks:
"""Cut/mask/paint parameters derived from one body's bones + mesh."""
def __init__(self, armature):
bones = armature.data.bones
thigh_l = bones.get("thigh_l")
thigh_r = bones.get("thigh_r")
if thigh_l is None or thigh_r is None:
raise RuntimeError("thigh_l/thigh_r missing — not the 65-bone rig?")
self.hip_x = (abs(thigh_l.head_local.x) + abs(thigh_r.head_local.x)) / 2.0
# Hem plane: fraction of the thigh bone kept below its head (averaged
# over both sides — they are symmetric on every shipped body).
self.hem_z = (
thigh_l.head_local.z + HEM_FRAC * (thigh_l.tail_local.z - thigh_l.head_local.z)
+ thigh_r.head_local.z + HEM_FRAC * (thigh_r.tail_local.z - thigh_r.head_local.z)
) / 2.0
# Leg axis control points (z-increasing: knee -> hip) for the azimuth
# side-panel placement. x values are the +x (left) leg; the right leg
# mirrors via sign.
self.leg_z_pts = np.array([thigh_l.tail_local.z, thigh_l.head_local.z])
self.leg_x_pts = np.array(
[abs(thigh_l.tail_local.x), abs(thigh_l.head_local.x)])
self.leg_y_pts = np.array([thigh_l.tail_local.y, thigh_l.head_local.y])
log(f"landmarks: hem plane z={self.hem_z:.3f} "
f"(thigh head {thigh_l.head_local.z:.3f} -> knee "
f"{thigh_l.tail_local.z:.3f}, keep {HEM_FRAC:.2f}) "
f"hip_x={self.hip_x:.3f}")
def finalize(self, waist_plane, hem_plane):
"""Derive paint metrics from the CLEAN (flattened) rims."""
self.waist_z = waist_plane
self.hem_plane = hem_plane
self.span = waist_plane - hem_plane
self.sh = self.hip_x / _REF_HIP_X
self.band_h = BAND_FRAC * self.span
self.band_z = waist_plane - self.band_h
self.seam_w = SEAM_W_FRAC * self.span
self.panel_hw = PANEL_HW_FRAC * self.hip_x
self.hem_stitch_z = hem_plane + HEM_STITCH_FRAC * self.span
self.knot_z = self.band_z + 0.55 * self.band_h
self.knot_r = KNOT_R_M * self.sh
self.cord_len = CORD_LEN_FRAC * self.span
self.cord_w = CORD_W_M * self.sh
self.cord_x0 = CORD_X0_M * self.sh
log(f"finalized: waist={self.waist_z:.3f} hem={self.hem_plane:.3f} "
f"span={self.span:.3f} band_z={self.band_z:.3f} "
f"panel_hw={self.panel_hw * 100:.1f}cm "
f"seam_w={self.seam_w * 1000:.1f}mm knot_z={self.knot_z:.3f}")
# --------------------------------------------------------------------------
# Swim feature field (texel-level; drives albedo AND mask together)
# --------------------------------------------------------------------------
def _trunk_field(px, py, pz, lm):
"""Evaluate swim-trunk features at texel 3D positions (numpy arrays).
Returns bool arrays (in_band, panel, trim):
`in_band`/`panel` feed the mask R/B channels; `trim` is every painted
cream detail (drawstring, border stitches, panel edge piping).
"""
w2 = lm.seam_w * 0.5
front = py * FRONT_Y_SIGN > 0.004
in_band = pz >= lm.band_z
mid = ~in_band
# Per-z leg axis, mirrored by x sign (clamps to the hip values above the
# thigh head, so the panel runs straight up through the hip to the band).
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 panel: 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
panel = mid & (arc_out < lm.panel_hw)
# Painted trim: panel edge piping, waistband border, hem border.
piping = mid & (np.abs(arc_out - lm.panel_hw) < w2 * 0.8)
wstitch = np.abs(pz - lm.band_z) < w2 * 0.7
hstitch = np.abs(pz - lm.hem_stitch_z) < w2 * 0.7
# Drawstring: knot blob + two cords hanging from it, centre front,
# drifting slightly outward as they drop.
knot = front & (np.hypot(px, pz - lm.knot_z) < lm.knot_r)
drop = np.clip(lm.knot_z - pz, 0.0, None)
cord_sep = lm.cord_x0 + CORD_SLANT * drop
cords = front & (pz < lm.knot_z) & (pz > lm.knot_z - lm.cord_len) \
& (np.abs(np.abs(px) - cord_sep) < lm.cord_w * 0.5)
trim = piping | wstitch | hstitch | knot | cords
return in_band, panel, trim
def _paint_texels(px, py, pz, noise, lm):
"""Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions."""
n = px.shape[0]
in_band, panel, trim = _trunk_field(px, py, pz, lm)
# --- albedo: coral base, shaded band, teal panel, cream trim ------------
alb = np.empty((n, 4), dtype=np.float32)
for c in range(3):
alb[:, c] = CORAL_RGB[c] + noise
alb[:, 3] = 1.0
alb[in_band, :3] *= BAND_SHADE
for c in range(3):
alb[panel, c] = TEAL_RGB[c] + noise[panel]
if not PLAIN:
alb[trim, 0] = TRIM_RGB[0]
alb[trim, 1] = TRIM_RGB[1]
alb[trim, 2] = TRIM_RGB[2]
np.clip(alb, 0.0, 1.0, out=alb)
# --- region mask: waistband R / body G / side panel B --------------------
mask = np.zeros((n, 4), dtype=np.float32)
mask[in_band, 0] = 1.0
mask[panel, 2] = 1.0
mask[~(in_band | panel), 1] = 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 swim field, write albedo + mask together (same rasterizer
contract as the denim companion, driving this garment's field)."""
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] = CORAL_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 = body green (bilinear-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()
total = float(W * H)
r_pct = 100.0 * float((mask_buf[:, :, 0] > 0.5).sum()) / total
b_pct = 100.0 * float((mask_buf[:, :, 2] > 0.5).sum()) / total
log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H}); "
f"mask texels: R(waistband)={r_pct:.1f}% B(panel)={b_pct:.1f}% "
f"(rest G/background)")
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"swim_albedo_{body}", albedo_path)
_save(mask_buf, f"swim_mask_{body}", mask_path)
log(f"saved albedo -> {albedo_path}")
log(f"saved mask -> {mask_path}")
return albedo_img
# --------------------------------------------------------------------------
# Per-body authoring
# --------------------------------------------------------------------------
def author_trunks_shell(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)
lm = TrunkLandmarks(armature)
legs.hem_cut(shell, lm.hem_z)
waist_plane, hem_plane = denim.flatten_open_rims(shell, lm.hem_z)
base.offset_outward(shell, offset)
denim.waist_flare(shell, waist_plane,
BAND_FRAC * (waist_plane - hem_plane), WAIST_FLARE_M)
base.solidify(shell, base.CLOTH_THICKNESS_M)
denim.clamp_waist_residue(shell, waist_plane)
# Paint metrics reference the CLEAN rims (band under the flattened waist
# edge, hem stitch above the flattened hem).
lm.finalize(waist_plane, hem_plane)
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 HEM_FRAC, BAND_FRAC, PANEL_HW_FRAC, WAIST_FLARE_M
global CORAL_RGB, TEAL_RGB, 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] [--hem-frac F] [--band-frac F] [--panel-frac F] "
"[--waist-flare M] [--base-rgb r,g,b] [--panel-rgb r,g,b] "
"[--plain]")
sys.exit(1)
bodies_root = argv[0]
out_dir = argv[1]
offset = base.PER_BODY_OFFSET_M
if "--offset" in argv:
offset = float(argv[argv.index("--offset") + 1])
if "--hem-frac" in argv:
HEM_FRAC = float(argv[argv.index("--hem-frac") + 1])
if "--band-frac" in argv:
BAND_FRAC = float(argv[argv.index("--band-frac") + 1])
if "--panel-frac" in argv:
PANEL_HW_FRAC = float(argv[argv.index("--panel-frac") + 1])
if "--waist-flare" in argv:
WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1])
if "--base-rgb" in argv:
CORAL_RGB = tuple(
float(v) for v in argv[argv.index("--base-rgb") + 1].split(","))
if "--panel-rgb" in argv:
TEAL_RGB = tuple(
float(v) for v in argv[argv.index("--panel-rgb") + 1].split(","))
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"swim-trunks per-body mode: {len(bodies)} bodies, offset "
f"{offset * 1000:.0f} mm, hem-frac {HEM_FRAC}, band-frac {BAND_FRAC}, "
f"panel-frac {PANEL_HW_FRAC}, 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_trunks_shell(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()