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

105 lines
4.5 KiB
Python

"""
blender_rebuild_forks.py
Usage: tooling/blender --background --python tooling/blender_rebuild_forks.py -- <source_dir> <output_base_dir> [body_type ...]
Rebuilds ONLY the fork body types (thin_m/f, heavy_m/f, child) through the same
segmentation pipeline that produced the healthy six (blender_segment_body.py),
using the T-1090 fix: the fork scale is applied to the mesh AND the embedded
armature rest pose so each segment stays internally consistent with its own
skeleton (required by the shared-skeleton compositor in character_visual.gd).
Sources (Source-tier Godot - UE exports):
Regular_Male_FullBody.gltf + scale (0.82, 1.0, 0.88) -> thin_m
Regular_Female_FullBody.gltf + scale (0.82, 1.0, 0.88) -> thin_f
Regular_Male_FullBody.gltf + scale (1.20, 1.08, 1.0) -> heavy_m
Regular_Female_FullBody.gltf + scale (1.20, 1.08, 1.0) -> heavy_f
Teen_Male_FullBody.gltf + scale (0.75, 0.72, 0.75) -> child [gender-neutral]
The fork scale factors and source mapping are unchanged from
blender_process_bodies.py — this driver reuses the same table so the healthy six
are never touched. Output layout matches production: <out>/{body_type}/seg_*.glb.
"""
import sys
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)
from blender_segment_body import segment_body
# (gltf_filename, body_type_key, scale, source_label) — forks only.
FORK_MANIFEST = [
("Regular_Male_FullBody.gltf", "thin_m", (0.82, 1.0, 0.88), "Regular_Male_FullBody.gltf"),
("Regular_Female_FullBody.gltf", "thin_f", (0.82, 1.0, 0.88), "Regular_Female_FullBody.gltf"),
("Regular_Male_FullBody.gltf", "heavy_m", (1.20, 1.08, 1.0), "Regular_Male_FullBody.gltf"),
("Regular_Female_FullBody.gltf", "heavy_f", (1.20, 1.08, 1.0), "Regular_Female_FullBody.gltf"),
("Teen_Male_FullBody.gltf", "child", (0.75, 0.72, 0.75), "Teen_Male_FullBody.gltf"),
]
FORK_README = """\
# Fork body type — auto-generated from mesh + armature scaling (T-1090)
This directory contains **{body_type}** body segments, generated by applying a
proportional scale to the source body ({source}) mesh AND its armature rest pose
together, then segmenting:
Scale: ({sx:.2f}, {sy:.2f}, {sz:.2f})
The scale is applied to the mesh vertices and the embedded armature rest pose
with the SAME affine (T-1090 fix). This keeps each segment internally consistent
with its own skeleton, which the shared-skeleton compositor
(character_visual.gd) requires — a mesh-only scale detaches the head and
explodes the limbs on relocation.
Cross-sectional differentiation (thin = narrow, heavy = wide) survives the
composite; global height normalises to the shared skeleton (see T-1090 report /
Q-060 for the shared-skeleton scale-normalisation note).
Status: auto-generated (rebuilt T-1090), 18 segments incl. seg_hips
"""
def write_fork_readme(output_dir, body_type, source, scale):
sx, sy, sz = scale
with open(os.path.join(output_dir, "README.md"), "w") as f:
f.write(FORK_README.format(body_type=body_type, source=source, sx=sx, sy=sy, sz=sz))
if __name__ == "__main__":
argv = sys.argv
args = argv[argv.index("--") + 1:] if "--" in argv else []
if len(args) < 2:
print("Usage: -- <source_dir> <output_base_dir> [body_type ...]")
sys.exit(1)
source_dir = args[0]
output_base_dir = args[1]
only = set(args[2:]) # optional subset filter
manifest = [m for m in FORK_MANIFEST if not only or m[1] in only]
unique_sources = {gltf for (gltf, _, _, _) in manifest}
missing = [
os.path.join(source_dir, f)
for f in sorted(unique_sources)
if not os.path.exists(os.path.join(source_dir, f))
]
if missing:
print("\nERROR: Missing source GLTF files:")
for p in missing:
print(f" {p}")
sys.exit(1)
results = []
for (gltf_filename, body_type, scale, source_label) in manifest:
gltf_path = os.path.join(source_dir, gltf_filename)
output_dir = os.path.join(output_base_dir, body_type)
print(f"\n{'='*60}\n Fork: {body_type} scale={scale}\n Source: {gltf_filename}")
exported, skipped = segment_body(gltf_path, output_dir, scale)
write_fork_readme(output_dir, body_type, source_label, scale)
results.append((body_type, exported, skipped))
print(f"\n{'='*60}\n=== Fork rebuild complete ===")
for body_type, exported, skipped in results:
print(f" {body_type}: {len(exported)} exported, {len(skipped)} skipped")