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>
117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
"""
|
|
Spike T-1089 / Synty Sidekick intake — step 3 recon: the 11 body types.
|
|
|
|
Questions this answers (feeds 04_batch_fit_bodies.py):
|
|
1. Do the garment-adjacent segments (torso/hips/neck/upper arms/upper legs)
|
|
share topology (vert/poly counts) across all 11 body types? If yes, the
|
|
Surface-Deform refit can be driven by direct vertex correspondence
|
|
(bind SD on average_m, write target coords into the reference mesh).
|
|
2. Do the per-segment embedded armatures differ across bodies (rest-pose
|
|
bake), or is it the shared skeleton everywhere?
|
|
|
|
Run:
|
|
reach blender run \
|
|
spikes/synty-intake/scripts/03_recon_bodies.py
|
|
"""
|
|
|
|
import bpy
|
|
import json
|
|
import os
|
|
|
|
REPO = "/var/mnt/data/projects/settled-reach"
|
|
BODIES_DIR = os.path.join(REPO, "client/assets/characters/bodies")
|
|
OUT_JSON = os.path.join(REPO, "spikes/synty-intake/out/bodies_recon.json")
|
|
|
|
BODY_TYPES = ["average_f", "average_m", "child", "heavy_f", "heavy_m",
|
|
"muscular_f", "muscular_m", "teen_f", "teen_m",
|
|
"thin_f", "thin_m"]
|
|
|
|
SEGS = ["seg_torso", "seg_hips", "seg_neck",
|
|
"seg_arm_upper_l", "seg_arm_upper_r",
|
|
"seg_leg_upper_l", "seg_leg_upper_r"]
|
|
|
|
LANDMARK_BONES = ["pelvis", "spine_01", "spine_02", "spine_03",
|
|
"clavicle_l", "upperarm_l", "thigh_l", "neck_01", "Head"]
|
|
|
|
|
|
def clear_scene():
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.object.delete(use_global=False)
|
|
for block_list in (bpy.data.meshes, bpy.data.armatures, bpy.data.images,
|
|
bpy.data.materials):
|
|
for block in list(block_list):
|
|
if block.users == 0:
|
|
block_list.remove(block)
|
|
|
|
|
|
def import_new(path):
|
|
before = set(bpy.data.objects)
|
|
bpy.ops.import_scene.gltf(filepath=path)
|
|
return list(set(bpy.data.objects) - before)
|
|
|
|
|
|
report = {}
|
|
|
|
for body in BODY_TYPES:
|
|
body_dir = os.path.join(BODIES_DIR, body)
|
|
entry = {"segments": {}, "armature": None}
|
|
for seg in SEGS:
|
|
path = os.path.join(body_dir, seg + ".glb")
|
|
if not os.path.exists(path):
|
|
entry["segments"][seg] = None
|
|
continue
|
|
clear_scene()
|
|
objs = import_new(path)
|
|
mesh = None
|
|
for o in objs:
|
|
if o.type == 'MESH' and len(o.vertex_groups) > 0:
|
|
mesh = o
|
|
arm = None
|
|
for o in objs:
|
|
if o.type == 'ARMATURE':
|
|
arm = o
|
|
seg_info = {}
|
|
if mesh is not None:
|
|
seg_info["verts"] = len(mesh.data.vertices)
|
|
seg_info["polys"] = len(mesh.data.polygons)
|
|
seg_info["vgroups"] = len(mesh.vertex_groups)
|
|
entry["segments"][seg] = seg_info
|
|
# record armature landmarks once per body (from seg_torso)
|
|
if seg == "seg_torso" and arm is not None:
|
|
lm = {}
|
|
for bn in LANDMARK_BONES:
|
|
b = arm.data.bones.get(bn)
|
|
if b is None:
|
|
b = arm.data.bones.get(bn.lower())
|
|
if b is not None:
|
|
w = arm.matrix_world @ b.head_local
|
|
lm[bn] = [round(v, 5) for v in w]
|
|
entry["armature"] = {
|
|
"bones": len(arm.data.bones),
|
|
"landmarks": lm,
|
|
}
|
|
report[body] = entry
|
|
|
|
# cross-body topology comparison vs average_m
|
|
ref = report["average_m"]["segments"]
|
|
topo = {}
|
|
for body in BODY_TYPES:
|
|
diffs = []
|
|
for seg in SEGS:
|
|
a = ref.get(seg)
|
|
b = report[body]["segments"].get(seg)
|
|
if not a or not b:
|
|
diffs.append(f"{seg}: missing")
|
|
elif (a["verts"], a["polys"]) != (b["verts"], b["polys"]):
|
|
diffs.append(f"{seg}: {b['verts']}v/{b['polys']}p vs "
|
|
f"ref {a['verts']}v/{a['polys']}p")
|
|
topo[body] = diffs if diffs else "MATCH"
|
|
report["_topology_vs_average_m"] = topo
|
|
|
|
os.makedirs(os.path.dirname(OUT_JSON), exist_ok=True)
|
|
with open(OUT_JSON, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
|
|
print("\n=== BODIES RECON ===")
|
|
print(json.dumps(report, indent=2))
|