Files
jpmschweitzerandClaude Opus 5 201dabd19b refactor(tooling): T-1273 — the Blender carve-out, and a guard that keeps it carved
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>
2026-09-02 20:55:52 +02:00

94 lines
2.9 KiB
Python

"""
blender_extract_armature.py
Usage: tooling/blender --background --python tooling/blender_extract_armature.py -- <input.gltf> <output.glb>
Loads a Quaternius GLTF file, strips all mesh objects and animation data,
and exports only the armature as a GLB skeleton file.
Requirements:
- 65-bone hierarchy preserved
- No mesh geometry
- No animation data
- Y-up, -Z forward (glTF default)
- 1 unit = 1 meter (Quaternius convention)
"""
import sys
import bpy
def extract_armature(input_path: str, output_path: str) -> None:
# Clear the default scene
bpy.ops.wm.read_factory_settings(use_empty=True)
# Import the GLTF/GLB
print(f"Loading: {input_path}")
bpy.ops.import_scene.gltf(filepath=input_path)
# Report what was imported
all_objects = list(bpy.context.scene.objects)
print(f"Imported {len(all_objects)} objects:")
for obj in all_objects:
print(f" {obj.name} ({obj.type})")
# Find armature objects
armatures = [obj for obj in all_objects if obj.type == 'ARMATURE']
if not armatures:
print("ERROR: No armature found in the imported file.")
sys.exit(1)
armature = armatures[0]
print(f"Armature: {armature.name}{len(armature.data.bones)} bones")
if len(armature.data.bones) < 60:
print(f"WARNING: Expected ~65 bones, found {len(armature.data.bones)}. Check compatibility.")
# Remove all non-armature objects (meshes, lights, cameras, empties)
bpy.ops.object.select_all(action='DESELECT')
for obj in all_objects:
if obj.type != 'ARMATURE':
obj.select_set(True)
bpy.ops.object.delete()
# Remove all animation data (skeleton only, no poses)
if armature.animation_data:
armature.animation_data_clear()
for action in list(bpy.data.actions):
bpy.data.actions.remove(action)
# Select only the armature for export
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
bpy.context.view_layer.objects.active = armature
# Export as GLB — skeleton only, no animations, no meshes
print(f"Exporting to: {output_path}")
bpy.ops.export_scene.gltf(
filepath=output_path,
use_selection=True,
export_format='GLB',
export_animations=False,
export_skins=True,
export_yup=True,
)
# Verify: report bone count and names
bones = sorted(armature.data.bones, key=lambda b: b.name)
print(f"Export complete. {len(bones)} bones:")
for bone in bones:
print(f" {bone.name}")
if __name__ == "__main__":
argv = sys.argv
if "--" not in argv:
print("Usage: tooling/blender --background --python tooling/blender_extract_armature.py -- <input.gltf> <output.glb>")
sys.exit(1)
args = argv[argv.index("--") + 1:]
if len(args) < 2:
print("ERROR: Provide input GLTF/GLB path and output GLB path.")
sys.exit(1)
extract_armature(args[0], args[1])