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>
502 lines
20 KiB
Python
502 lines
20 KiB
Python
"""
|
|
blender_author_cargo_pants.py (T-1089 wave 2, cargo_pants)
|
|
|
|
Authors full-length CARGO pants as per-body offset shells. Reuses
|
|
blender_author_offset_shell.py as the base library (scene build, join,
|
|
offset, solidify, GLB export) and blender_author_denim_pants.py as the
|
|
proven bottoms companion (boundary WELD of coincident segment-seam rings,
|
|
open-rim FLATTENING onto clean planes, per-body LegLandmarks, waist flare,
|
|
hem cut, parked logo UV2) — the denim script's utilities are imported as a
|
|
module, not re-derived.
|
|
|
|
What cargo adds, as reusable parameters:
|
|
|
|
* --straight: STRAIGHT-FIT boost — extra radial stand-off away from the
|
|
per-z leg axis, ramping 0 at the knee to the full value at the ankle, so
|
|
the lower leg does not taper with the calf (straight silhouette; also
|
|
buys lower-leg clip clearance). Applied post-offset, pre-solidify.
|
|
* TEXEL-level cargo feature painting (same one-field-drives-both design as
|
|
denim: painted albedo and region mask always agree):
|
|
- albedo: large outer-thigh CARGO POCKETS with button-down FLAPS
|
|
(filled panels, border stitching, centre pleat, two flap
|
|
buttons), outseam/inseam side seams, centre-front fly
|
|
stitch, belt loops + waist button, waistband and hem border
|
|
stitching — flat tone-on-tone utility look, identity
|
|
carried by the texture (style pin: modern only).
|
|
- mask: waistband -> R, legs -> G, pockets + flaps -> B
|
|
(spec: waistband=R, legs=G, pockets+flaps=B).
|
|
A SIGNED azimuth arc around the per-z leg axis (0 at the outer side,
|
|
positive toward the front) places the pockets slightly forward-of-side
|
|
and the flap buttons symmetrically — the same analytic-field approach as
|
|
the denim script, generalised from unsigned to signed arc.
|
|
* pocket proportions anchor to each body's own THIGH span (crotch ->
|
|
knee) and hip half-width, so the pockets stay proportional across all
|
|
11 bodies (child included) — the base.derive_thresholds philosophy.
|
|
|
|
Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r; natural
|
|
boundaries give the waist opening and ankle hems. Regions are painted, not
|
|
modelled — no 3D pocket geometry (texture carries identity).
|
|
|
|
Usage (cargo_pants reference invocation):
|
|
reach blender run blender_author_cargo_pants \
|
|
client/assets/characters/bodies \
|
|
client/assets/characters/clothing/cargo_pants \
|
|
[--bodies average_m,child,...] [--offset 0.013] [--hem-frac 1.0] \
|
|
[--waist-flare 0.007] [--straight 0.010] [--base-rgb 0.36,0.37,0.29] \
|
|
[--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 base offset-shell module + the denim bottoms companion
|
|
# (weld / rim-flatten / flare / landmarks utilities live there)
|
|
# --------------------------------------------------------------------------
|
|
_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
|
|
FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base)
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Parameters
|
|
# --------------------------------------------------------------------------
|
|
COVERED_SEGMENTS = [
|
|
"seg_hips",
|
|
"seg_leg_upper_l", "seg_leg_upper_r",
|
|
"seg_leg_lower_l", "seg_leg_lower_r",
|
|
]
|
|
|
|
HEM_FRAC = 1.0 # 1.0 = full leg to the ankle
|
|
OFFSET_M = 0.013 # cargo sits a touch looser than jeans (0.012)
|
|
STRAIGHT_M = 0.010 # straight-fit radial boost at the ankle (0 at knee)
|
|
WAIST_FLARE_M = 0.007 # waistband rim stand-off (deep-crouch mitigation,
|
|
# proven on jeans)
|
|
TEX_SIZE = 1024 # painted pockets/seams need >512
|
|
|
|
# Vertical proportions — fractions of the garment span (waist_z - hem_z),
|
|
# via denim.LegLandmarks.finalize (BAND/CUFF/SEAM fractions proven on jeans).
|
|
# Pocket proportions — fractions of the THIGH span (crotch_z - knee_z).
|
|
FLAP_TOP_FRAC = 0.18 # flap top below the crotch
|
|
FLAP_H_FRAC = 0.14 # flap height
|
|
POCKET_BOT_FRAC = 0.74 # pocket bottom below the crotch
|
|
|
|
# Horizontal proportions — signed azimuth ARC around the per-z leg axis
|
|
# (metres on the reference body, scaled by the body's hip ratio lm.sh).
|
|
POCKET_HALF_ARC_M = 0.058 # pocket half-width
|
|
FLAP_EXTRA_ARC_M = 0.007 # flap overhangs the pocket by this much per side
|
|
POCKET_FWD_BIAS_M = 0.010 # pocket centre sits slightly forward of the outseam
|
|
BUTTON_R_M = 0.0075 # flap button radius
|
|
LOOP_X_FRACS = (0.55, 1.30) # belt-loop |x| positions (fractions of hip_x)
|
|
LOOP_W_M = 0.016 # belt loop width (m, scaled by hip ratio)
|
|
|
|
# Utility olive/grey style (sRGB floats; flat toon-friendly, tone-on-tone).
|
|
CARGO_RGB = (0.360, 0.370, 0.290) # utility olive-grey
|
|
THREAD_RGB = (0.225, 0.235, 0.185) # tone-on-tone darker stitching
|
|
BUTTON_RGB = (0.170, 0.160, 0.140) # matte button
|
|
ALBEDO_NOISE = 0.020 # +/- woven jitter
|
|
POCKET_SHADE = 0.93 # pocket panel albedo darkening
|
|
FLAP_SHADE = 0.85 # flap panel albedo darkening
|
|
LOOP_SHADE = 0.80 # belt-loop albedo darkening
|
|
PLAIN = False # --plain: skip pockets/stitch/button paint
|
|
NOISE_SEED = 3089
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Geometry: straight-fit boost (cargo's reusable fit parameter)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def straight_boost(shell, lm, boost):
|
|
"""Extra RADIAL stand-off away from the per-z leg axis below the knee,
|
|
ramping linearly from 0 at the knee to `boost` at the ankle. The body
|
|
calf tapers; pushing the shell out progressively keeps the pant leg
|
|
straight (cargo silhouette) instead of hugging the calf. Applied
|
|
post-offset, pre-solidify; weights/UVs are untouched."""
|
|
if boost <= 0.0:
|
|
return
|
|
knee_z = float(lm.leg_z_pts[1])
|
|
ankle_z = float(lm.leg_z_pts[0])
|
|
span = max(knee_z - ankle_z, 1e-6)
|
|
bm = bmesh.new()
|
|
bm.from_mesh(shell.data)
|
|
n = 0
|
|
for v in bm.verts:
|
|
z = v.co.z
|
|
if z >= knee_z:
|
|
continue
|
|
t = min((knee_z - z) / span, 1.0)
|
|
side = 1.0 if v.co.x >= 0.0 else -1.0
|
|
cx = side * float(np.interp(z, lm.leg_z_pts, lm.leg_x_pts))
|
|
cy = float(np.interp(z, lm.leg_z_pts, lm.leg_y_pts))
|
|
dx = v.co.x - cx
|
|
dy = v.co.y - cy
|
|
r = (dx * dx + dy * dy) ** 0.5
|
|
if r > 1e-6:
|
|
v.co.x += boost * t * dx / r
|
|
v.co.y += boost * t * dy / r
|
|
n += 1
|
|
bm.to_mesh(shell.data)
|
|
bm.free()
|
|
shell.data.update()
|
|
log(f"straight-fit boost: {n} verts, +{boost * 1000:.1f} mm radial at "
|
|
f"ankle (ramped from knee z={knee_z:.3f})")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Cargo feature field (texel-level; drives albedo AND mask together)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _cargo_field(px, py, pz, lm):
|
|
"""Evaluate cargo features at texel 3D positions (numpy arrays).
|
|
|
|
Returns a dict of bool arrays. `pocket`/`flap` feed the mask B channel;
|
|
the stitch/fill features feed the albedo only.
|
|
"""
|
|
w2 = lm.seam_w * 0.5
|
|
front = py * FRONT_Y_SIGN > 0.004
|
|
in_band = pz >= lm.band_z
|
|
mid = (~in_band) & (pz > lm.hem_z + 0.5 * lm.cuff_h)
|
|
|
|
# Per-z leg axis, mirrored by x sign; SIGNED azimuth arc around it
|
|
# (0 at the outer side, positive toward the front, +/-pi*r at the inseam).
|
|
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
|
|
theta = np.arctan2(dy * FRONT_Y_SIGN / r, np.clip(dx * side / r, -1.0, 1.0))
|
|
sarc = theta * r # signed arc distance from the outer direction
|
|
|
|
# Side seams: outseam on the outer azimuth; inseam opposite, below crotch.
|
|
outseam = mid & (np.abs(sarc) < w2)
|
|
arc_in = (np.pi - np.abs(theta)) * r
|
|
inseam = mid & (arc_in < w2) \
|
|
& (pz < lm.crotch_z - 0.01 * lm.span / denim._REF_SPAN)
|
|
|
|
# Centre-front fly stitch (slightly off-centre, like the jeans J-front).
|
|
fly = front & mid & (np.abs(px - 0.012 * lm.sh) < w2) \
|
|
& (pz > lm.crotch_z + 0.015 * lm.span / denim._REF_SPAN)
|
|
|
|
# --- cargo pocket + flap (outer thigh, slightly forward-of-side) --------
|
|
knee_z = float(lm.leg_z_pts[1])
|
|
thigh = max(lm.crotch_z - knee_z, 1e-6)
|
|
flap_top = lm.crotch_z - FLAP_TOP_FRAC * thigh
|
|
flap_bot = flap_top - FLAP_H_FRAC * thigh
|
|
pocket_bot = lm.crotch_z - POCKET_BOT_FRAC * thigh
|
|
p_arc = POCKET_HALF_ARC_M * lm.sh
|
|
f_arc = p_arc + FLAP_EXTRA_ARC_M * lm.sh
|
|
a = sarc - POCKET_FWD_BIAS_M * lm.sh # pocket-centred signed arc
|
|
|
|
pocket = (np.abs(a) < p_arc) & (pz <= flap_top) & (pz >= pocket_bot)
|
|
flap = (np.abs(a) < f_arc) & (pz <= flap_top) & (pz >= flap_bot)
|
|
|
|
# Pocket border stitching (sides + bottom), centre pleat, flap edge.
|
|
p_side = (np.abs(np.abs(a) - p_arc) < w2) & (pz <= flap_top) \
|
|
& (pz >= pocket_bot)
|
|
p_bottom = (np.abs(pz - pocket_bot) < w2) & (np.abs(a) < p_arc)
|
|
pleat = (np.abs(a) < w2) & (pz < flap_bot - 2.0 * w2) & (pz >= pocket_bot)
|
|
f_edge = flap & ((np.abs(np.abs(a) - f_arc) < w2)
|
|
| (np.abs(pz - flap_bot) < w2))
|
|
pkt_stitch = p_side | p_bottom | pleat | f_edge
|
|
|
|
# Two flap buttons, symmetric about the pocket centre.
|
|
btn_r = BUTTON_R_M * lm.sh
|
|
btn_z = flap_bot + 0.30 * (flap_top - flap_bot)
|
|
buttons = np.zeros_like(front)
|
|
for bx in (-0.45 * f_arc, 0.45 * f_arc):
|
|
buttons |= np.hypot(a - bx, pz - btn_z) < btn_r
|
|
|
|
# Border stitching: waistband seam + hem stitch.
|
|
wstitch = np.abs(pz - lm.band_z) < w2
|
|
hemstitch = np.abs(pz - lm.cuff_top) < w2
|
|
|
|
return {
|
|
"front": front, "in_band": in_band,
|
|
"seams": outseam | inseam | fly,
|
|
"wstitch": wstitch, "hemstitch": hemstitch,
|
|
"pocket": pocket, "flap": flap,
|
|
"pkt_stitch": pkt_stitch, "buttons": buttons,
|
|
}
|
|
|
|
|
|
def _paint_texels(px, py, pz, noise, lm):
|
|
"""Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions."""
|
|
n = px.shape[0]
|
|
f = _cargo_field(px, py, pz, lm)
|
|
|
|
# --- albedo -------------------------------------------------------------
|
|
alb = np.empty((n, 4), dtype=np.float32)
|
|
for c in range(3):
|
|
alb[:, c] = CARGO_RGB[c] + noise
|
|
alb[:, 3] = 1.0
|
|
|
|
if not PLAIN:
|
|
# Belt loops: darkened bands on the waistband.
|
|
loop_w = LOOP_W_M * lm.sh
|
|
loops = np.zeros(n, dtype=bool)
|
|
for fx in LOOP_X_FRACS:
|
|
loops |= np.abs(np.abs(px) - fx * lm.hip_x) < loop_w * 0.5
|
|
loops |= (~f["front"]) & (np.abs(px) < loop_w * 0.5) # centre-back
|
|
loops &= f["in_band"]
|
|
alb[loops, :3] *= LOOP_SHADE
|
|
|
|
# Panel fills first, stitch lines on top.
|
|
alb[f["pocket"] & ~f["flap"], :3] *= POCKET_SHADE
|
|
alb[f["flap"], :3] *= FLAP_SHADE
|
|
|
|
thread = f["seams"] | f["wstitch"] | f["hemstitch"] | f["pkt_stitch"]
|
|
alb[thread, 0] = THREAD_RGB[0]
|
|
alb[thread, 1] = THREAD_RGB[1]
|
|
alb[thread, 2] = THREAD_RGB[2]
|
|
|
|
# Flap buttons + waist button.
|
|
btn = f["buttons"] | (f["front"] & (
|
|
np.hypot(px, pz - (lm.band_z + 0.5 * lm.band_h)) < 0.009 * lm.sh))
|
|
alb[btn, 0] = BUTTON_RGB[0]
|
|
alb[btn, 1] = BUTTON_RGB[1]
|
|
alb[btn, 2] = BUTTON_RGB[2]
|
|
|
|
# --- region mask: waistband R / legs G / pockets+flaps B -----------------
|
|
mask = np.zeros((n, 4), dtype=np.float32)
|
|
is_b = (f["pocket"] | f["flap"]) & ~f["in_band"]
|
|
is_g = ~(f["in_band"] | is_b)
|
|
mask[f["in_band"], 0] = 1.0
|
|
mask[is_b, 2] = 1.0
|
|
mask[is_g, 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 cargo field, write albedo + mask together (same rasterizer
|
|
contract as the denim companion, pointed at the cargo painter)."""
|
|
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] = CARGO_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"cargo_albedo_{body}", albedo_path)
|
|
_save(mask_buf, f"cargo_mask_{body}", mask_path)
|
|
log(f"saved albedo -> {albedo_path}")
|
|
log(f"saved mask -> {mask_path}")
|
|
return albedo_img
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Per-body authoring
|
|
# --------------------------------------------------------------------------
|
|
|
|
def author_cargo_shell(body_dir, out_dir, body, offset, hem_frac):
|
|
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)
|
|
|
|
hem_z = denim.hem_cut(shell, lm, hem_frac)
|
|
ankle_plane = float(lm.leg_z_pts[0]) if hem_frac >= 0.999 else hem_z
|
|
waist_plane, ankle_plane = denim.flatten_open_rims(shell, ankle_plane)
|
|
|
|
base.offset_outward(shell, offset)
|
|
straight_boost(shell, lm, STRAIGHT_M)
|
|
denim.waist_flare(shell, waist_plane,
|
|
denim.BAND_FRAC * (waist_plane - ankle_plane),
|
|
WAIST_FLARE_M)
|
|
base.solidify(shell, base.CLOTH_THICKNESS_M)
|
|
denim.clamp_waist_residue(shell, waist_plane)
|
|
|
|
# Landmarks reference the CLEAN rims (band under the flattened waist edge,
|
|
# hem stitch above the flattened ankle rim).
|
|
lm.waist_z = waist_plane
|
|
lm.finalize(ankle_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, CARGO_RGB, PLAIN, WAIST_FLARE_M, STRAIGHT_M
|
|
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] [--waist-flare M] [--straight M] "
|
|
"[--base-rgb r,g,b] [--plain]")
|
|
sys.exit(1)
|
|
bodies_root = argv[0]
|
|
out_dir = argv[1]
|
|
offset = 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 "--waist-flare" in argv:
|
|
WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1])
|
|
if "--straight" in argv:
|
|
STRAIGHT_M = float(argv[argv.index("--straight") + 1])
|
|
if "--base-rgb" in argv:
|
|
CARGO_RGB = tuple(
|
|
float(v) for v in argv[argv.index("--base-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"cargo per-body mode: {len(bodies)} bodies, offset "
|
|
f"{offset * 1000:.0f} mm, straight {STRAIGHT_M * 1000:.0f} mm, "
|
|
f"hem-frac {HEM_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_cargo_shell(body_dir, out_dir, body, offset, HEM_FRAC)
|
|
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()
|