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>
105 lines
4.5 KiB
Python
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")
|