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

141 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
blender_process_bodies.py
Usage: tooling/blender --background --python tooling/blender_process_bodies.py -- <source_dir> <output_base_dir>
Drives the full body segmentation pipeline for all 11 body types.
Calls blender_segment_body.py logic inline (same Blender session, one pass per body type).
Source GLTF files (pre-exported from Source tier, Godot - UE format):
<source_dir>/Regular_Male_FullBody.gltf -> average_m (18 segs)
<source_dir>/Regular_Female_FullBody.gltf -> average_f (18 segs)
<source_dir>/Superhero_Male_FullBody.gltf -> muscular_m (18 segs)
<source_dir>/Superhero_Female_FullBody.gltf -> muscular_f (18 segs)
<source_dir>/Teen_Male_FullBody.gltf -> teen_m (18 segs)
<source_dir>/Teen_Female_FullBody.gltf -> teen_f (18 segs)
Fork body types (vertex-level scale applied before segmentation):
Regular_Male + scale (0.82, 1.0, 0.88) -> thin_m
Regular_Female + scale (0.82, 1.0, 0.88) -> thin_f
Regular_Male + scale (1.20, 1.08, 1.0) -> heavy_m
Regular_Female + scale (1.20, 1.08, 1.0) -> heavy_f
Teen_Male + scale (0.75, 0.72, 0.75) -> child [gender-neutral]
Fork body types are approximate (auto-generated from mesh scaling). They produce
visually distinct silhouettes but may need artist refinement for final quality.
A README is placed in each fork directory marking this status.
Output: <output_base_dir>/{body_type}/seg_{name}.glb (18 files × 11 types = 198 GLBs)
Decisions: D-159 (11 body types), D-160 (18 segments), D-164 (Source .blends as starting point)
"""
import sys
import os
# We import the segmentation logic from blender_segment_body.py which must be
# in the same directory as this script.
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, is_fork)
BODY_MANIFEST = [
# --- Direct body types (segment as-is) ---
("Regular_Male_FullBody.gltf", "average_m", (1.0, 1.0, 1.0), False),
("Regular_Female_FullBody.gltf", "average_f", (1.0, 1.0, 1.0), False),
("Superhero_Male_FullBody.gltf", "muscular_m", (1.0, 1.0, 1.0), False),
("Superhero_Female_FullBody.gltf","muscular_f", (1.0, 1.0, 1.0), False),
("Teen_Male_FullBody.gltf", "teen_m", (1.0, 1.0, 1.0), False),
("Teen_Female_FullBody.gltf", "teen_f", (1.0, 1.0, 1.0), False),
# --- Fork body types (auto-scaled from source) ---
# thin: narrow/ectomorph — narrower X, slightly shorter Z
("Regular_Male_FullBody.gltf", "thin_m", (0.82, 1.0, 0.88), True),
("Regular_Female_FullBody.gltf", "thin_f", (0.82, 1.0, 0.88), True),
# heavy: wide/endomorph — wider X, slightly taller Y
("Regular_Male_FullBody.gltf", "heavy_m", (1.20, 1.08, 1.0), True),
("Regular_Female_FullBody.gltf", "heavy_f", (1.20, 1.08, 1.0), True),
# child: gender-neutral, forked from Teen Male, scaled down
("Teen_Male_FullBody.gltf", "child", (0.75, 0.72, 0.75), True),
]
FORK_README = """\
# Fork body type — auto-generated from mesh scaling
This directory contains **{body_type}** body segments, generated by applying
a proportional mesh scale to the source body ({source}):
Scale: ({sx:.2f}, {sy:.2f}, {sz:.2f})
These segments are APPROXIMATE. They produce a visually distinct silhouette
but are not artist-authored from scratch. Review the proportions and refine
the base mesh manually if the auto-scale result is not satisfactory.
Status: auto-generated, needs artist review
Sprint 28 — visual team
"""
def write_fork_readme(output_dir, body_type, source, scale):
sx, sy, sz = scale
readme_path = os.path.join(output_dir, "README.md")
with open(readme_path, '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
if "--" not in argv:
print("Usage: tooling/blender --background --python tooling/blender_process_bodies.py -- <source_dir> <output_base_dir>")
sys.exit(1)
args = argv[argv.index("--") + 1:]
if len(args) < 2:
print("ERROR: Provide <source_dir> and <output_base_dir>")
sys.exit(1)
source_dir = args[0]
output_base_dir = args[1]
# Preflight: check ALL source files exist before starting any processing,
# so all missing files are reported at once rather than failing mid-loop.
unique_sources = {gltf_filename for (gltf_filename, _, _, _) in BODY_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 path in missing:
print(f" {path}")
sys.exit(1)
results = []
for (gltf_filename, body_type, scale, is_fork) in BODY_MANIFEST:
gltf_path = os.path.join(source_dir, gltf_filename)
output_dir = os.path.join(output_base_dir, body_type)
print(f"\n{'='*60}")
print(f" Body type: {body_type} {'[FORK]' if is_fork else '[DIRECT]'}")
print(f" Source: {gltf_filename}")
if is_fork:
print(f" Scale: {scale}")
exported, skipped = segment_body(gltf_path, output_dir, scale)
if is_fork:
write_fork_readme(output_dir, body_type, gltf_filename, scale)
results.append((body_type, exported, skipped, is_fork))
# Summary
print(f"\n{'='*60}")
print("=== Body type segmentation complete ===")
total_exported = 0
for body_type, exported, skipped, is_fork in results:
flag = " [FORK]" if is_fork else ""
print(f" {body_type}{flag}: {len(exported)} exported, {len(skipped)} skipped")
total_exported += len(exported)
print(f" Total GLBs: {total_exported}")