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>
59 lines
1.6 KiB
Python
59 lines
1.6 KiB
Python
"""
|
|
blender_compare_bones.py
|
|
Usage: tooling/blender --background --python tooling/blender_compare_bones.py -- <file_a.glb> <file_b.glb>
|
|
|
|
Compares bone names between two GLB/GLTF files. Reports missing or extra bones.
|
|
"""
|
|
|
|
import sys
|
|
import bpy
|
|
|
|
|
|
def get_bones(path: str) -> set:
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
bpy.ops.import_scene.gltf(filepath=path)
|
|
for obj in bpy.context.scene.objects:
|
|
if obj.type == 'ARMATURE':
|
|
return {b.name for b in obj.data.bones}
|
|
return set()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
argv = sys.argv
|
|
if "--" not in argv:
|
|
print("Usage: tooling/blender --background --python tooling/blender_compare_bones.py -- <a.glb> <b.glb>")
|
|
sys.exit(1)
|
|
|
|
args = argv[argv.index("--") + 1:]
|
|
if len(args) < 2:
|
|
print("ERROR: Need two GLB/GLTF paths.")
|
|
sys.exit(1)
|
|
|
|
path_a, path_b = args[0], args[1]
|
|
|
|
print(f"Loading A: {path_a}")
|
|
bones_a = get_bones(path_a)
|
|
print(f"A has {len(bones_a)} bones")
|
|
|
|
print(f"Loading B: {path_b}")
|
|
bones_b = get_bones(path_b)
|
|
print(f"B has {len(bones_b)} bones")
|
|
|
|
only_in_a = bones_a - bones_b
|
|
only_in_b = bones_b - bones_a
|
|
shared = bones_a & bones_b
|
|
|
|
print(f"Shared: {len(shared)}")
|
|
if only_in_a:
|
|
print(f"Only in A ({len(only_in_a)}):")
|
|
for name in sorted(only_in_a):
|
|
print(f" - {name}")
|
|
if only_in_b:
|
|
print(f"Only in B ({len(only_in_b)}):")
|
|
for name in sorted(only_in_b):
|
|
print(f" + {name}")
|
|
if not only_in_a and not only_in_b:
|
|
print("BONE_MATCH=OK")
|
|
else:
|
|
print("BONE_MATCH=MISMATCH")
|