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>
138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
"""
|
|
Spike T-1089 / Synty Sidekick intake — step 1: rest-pose recon.
|
|
|
|
Imports the Sidekick garment FBX (with its armature) and our Quaternius
|
|
armature.glb + average_m body segments, then prints:
|
|
- world-space bone head/tail positions for the 12 vertex-group bones
|
|
in BOTH armatures + per-bone delta
|
|
- bounding boxes of the garment vs our torso-region body segments
|
|
- object transforms as-imported (scale factors etc.)
|
|
|
|
Run:
|
|
reach blender run \
|
|
spikes/synty-intake/scripts/01_recon_rest_pose.py
|
|
"""
|
|
|
|
import bpy
|
|
import json
|
|
import os
|
|
|
|
REPO = "/var/mnt/data/projects/settled-reach"
|
|
GARMENT_FBX = os.path.join(
|
|
REPO, "spikes/synty-intake/raw/Assets/Synty/SidekickCharacters/Resources",
|
|
"Meshes/Outfits/Starter/SK_SCFI_CIVL_09_10TORS_HU01.fbx")
|
|
OUR_ARMATURE_GLB = os.path.join(REPO, "client/assets/characters/skeleton/armature.glb")
|
|
BODY_DIR = os.path.join(REPO, "client/assets/characters/bodies/average_m")
|
|
BODY_SEGS = ["seg_torso", "seg_hips", "seg_neck", "seg_arm_upper_l", "seg_arm_upper_r"]
|
|
|
|
BOUND_BONES = ["pelvis", "thigh_l", "thigh_r", "spine_01", "spine_02", "spine_03",
|
|
"neck_01", "head", "clavicle_l", "upperarm_l", "clavicle_r",
|
|
"upperarm_r"]
|
|
|
|
OUT_JSON = os.path.join(REPO, "spikes/synty-intake/out/recon_rest_pose.json")
|
|
|
|
|
|
def clear_scene():
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.object.delete(use_global=False)
|
|
|
|
|
|
def import_new(op, path, **kw):
|
|
before = set(bpy.data.objects)
|
|
op(filepath=path, **kw)
|
|
return list(set(bpy.data.objects) - before)
|
|
|
|
|
|
def find_armature(objs):
|
|
for o in objs:
|
|
if o.type == 'ARMATURE':
|
|
return o
|
|
return None
|
|
|
|
|
|
def bone_world(arm_obj, name):
|
|
b = arm_obj.data.bones.get(name)
|
|
if b is None:
|
|
return None
|
|
mw = arm_obj.matrix_world
|
|
h = mw @ b.head_local
|
|
t = mw @ b.tail_local
|
|
return {"head": [round(v, 5) for v in h], "tail": [round(v, 5) for v in t]}
|
|
|
|
|
|
def mesh_world_bbox(obj):
|
|
import mathutils
|
|
pts = [obj.matrix_world @ mathutils.Vector(c) for c in obj.bound_box]
|
|
lo = [round(min(p[i] for p in pts), 5) for i in range(3)]
|
|
hi = [round(max(p[i] for p in pts), 5) for i in range(3)]
|
|
return {"min": lo, "max": hi,
|
|
"dims": [round(hi[i] - lo[i], 5) for i in range(3)]}
|
|
|
|
|
|
clear_scene()
|
|
|
|
report = {}
|
|
|
|
# --- garment FBX ---
|
|
g_objs = import_new(bpy.ops.import_scene.fbx, GARMENT_FBX)
|
|
g_arm = find_armature(g_objs)
|
|
g_meshes = [o for o in g_objs if o.type == 'MESH']
|
|
report["garment_objects"] = [
|
|
{"name": o.name, "type": o.type,
|
|
"scale": [round(s, 6) for s in o.scale],
|
|
"location": [round(v, 6) for v in o.location]} for o in g_objs]
|
|
report["sidekick_bones"] = {}
|
|
for bn in BOUND_BONES:
|
|
report["sidekick_bones"][bn] = bone_world(g_arm, bn)
|
|
report["garment_meshes"] = {}
|
|
for m in g_meshes:
|
|
report["garment_meshes"][m.name] = {
|
|
"verts": len(m.data.vertices),
|
|
"bbox": mesh_world_bbox(m),
|
|
"vgroups": [vg.name for vg in m.vertex_groups],
|
|
"shape_keys": ([kb.name for kb in m.data.shape_keys.key_blocks]
|
|
if m.data.shape_keys else []),
|
|
}
|
|
|
|
# --- our armature ---
|
|
o_objs = import_new(bpy.ops.import_scene.gltf, OUR_ARMATURE_GLB)
|
|
o_arm = find_armature(o_objs)
|
|
report["our_armature_bone_count"] = len(o_arm.data.bones)
|
|
report["our_bones"] = {}
|
|
deltas = {}
|
|
for bn in BOUND_BONES:
|
|
ours = bn if bn != "head" else "Head"
|
|
info = bone_world(o_arm, ours)
|
|
report["our_bones"][ours] = info
|
|
sk = report["sidekick_bones"][bn]
|
|
if info and sk:
|
|
d = [round(sk["head"][i] - info["head"][i], 5) for i in range(3)]
|
|
mag = round(sum(x * x for x in d) ** 0.5, 5)
|
|
deltas[bn] = {"delta": d, "magnitude_m": mag}
|
|
report["bone_head_deltas"] = deltas
|
|
|
|
# --- our body segments (proportion reference) ---
|
|
report["our_body_segments"] = {}
|
|
for seg in BODY_SEGS:
|
|
path = os.path.join(BODY_DIR, seg + ".glb")
|
|
objs = import_new(bpy.ops.import_scene.gltf, path)
|
|
for m in objs:
|
|
if m.type == 'MESH':
|
|
report["our_body_segments"][seg] = {
|
|
"verts": len(m.data.vertices),
|
|
"bbox": mesh_world_bbox(m),
|
|
"vgroups": [vg.name for vg in m.vertex_groups],
|
|
}
|
|
|
|
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=== RECON SUMMARY ===")
|
|
print(json.dumps(deltas, indent=2))
|
|
print("Garment bbox:", json.dumps(
|
|
{k: v["bbox"]["dims"] for k, v in report["garment_meshes"].items()}))
|
|
print("Body seg bbox:", json.dumps(
|
|
{k: v["bbox"]["dims"] for k, v in report["our_body_segments"].items()}))
|
|
print("Report written:", OUT_JSON)
|