""" 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))