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>
473 lines
20 KiB
Python
473 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):
|
|
tooling/blender --background --python \
|
|
tooling/garment-fit/blender_author_swim_trunks.py -- \
|
|
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()
|