"""Import each M12 GLB with Blender 5.2 and freeze a Main persistence baseline.""" import hashlib import json import os import sys from pathlib import Path import bpy FIXTURE_IDS = ("mesh", "pbr", "uv", "skin", "animation") def sha256_file(path): digest = hashlib.sha256() with open(path, "rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def rounded(value): return round(float(value), 6) def graph_report(): objects = [] for obj in sorted(bpy.data.objects, key=lambda item: item.name): objects.append( { "name": obj.name, "type": obj.type, "data": obj.data.name if obj.data is not None else None, "parent": obj.parent.name if obj.parent is not None else None, "children": sorted(child.name for child in obj.children), "location": [rounded(value) for value in obj.location], } ) meshes = [] for mesh in sorted(bpy.data.meshes, key=lambda item: item.name): mesh.calc_loop_triangles() meshes.append( { "name": mesh.name, "vertexCount": len(mesh.vertices), "polygonCount": len(mesh.polygons), "triangleCount": len(mesh.loop_triangles), "uvLayers": sorted(layer.name for layer in mesh.uv_layers), "materials": [material.name if material else None for material in mesh.materials], } ) materials = [] for material in sorted(bpy.data.materials, key=lambda item: item.name): materials.append( { "name": material.name, "nodeNames": sorted(node.name for node in material.node_tree.nodes) if material.node_tree else [], } ) images = [] for image in sorted(bpy.data.images, key=lambda item: item.name): images.append( { "name": image.name, "size": list(image.size), "packed": image.packed_file is not None, "mimeType": image.file_format, } ) armatures = [] for armature in sorted(bpy.data.armatures, key=lambda item: item.name): armatures.append( { "name": armature.name, "bones": [ {"name": bone.name, "parent": bone.parent.name if bone.parent else None} for bone in sorted(armature.bones, key=lambda item: item.name) ], } ) actions = [] action_stable_ids = [] for action in sorted(bpy.data.actions, key=lambda item: item.name): fcurves = [] for layer in action.layers: for strip in layer.strips: for channelbag in strip.channelbags: fcurves.extend((curve.data_path, curve.array_index) for curve in channelbag.fcurves) actions.append( { "name": action.name, "frameRange": [rounded(action.frame_range[0]), rounded(action.frame_range[1])], "fcurves": sorted(fcurves), } ) owners = sorted( object_.name for object_ in bpy.data.objects if object_.animation_data is not None and object_.animation_data.action == action ) action_stable_ids.extend( "action:" + action.name + ":object:" + owner for owner in owners ) if not owners: action_stable_ids.append("action:" + action.name) return { "objects": objects, "meshes": meshes, "materials": materials, "images": images, "armatures": armatures, "actions": actions, "stableIds": { "objects": ["object:" + item["name"] for item in objects], "meshes": ["mesh:" + item["name"] for item in meshes], "materials": ["material:" + item["name"] for item in materials], "images": ["image:" + item["name"] for item in images], "armatures": ["armature:" + item["name"] for item in armatures], "actions": sorted(action_stable_ids), }, } def import_and_save(glb_path, blend_path): bpy.ops.wm.read_factory_settings(use_empty=True) result = bpy.ops.import_scene.gltf(filepath=str(glb_path)) if "FINISHED" not in result: raise RuntimeError("Blender GLB import failed: %s" % (result,)) scene = bpy.context.scene scene.frame_start = 1 scene.frame_end = 25 before = graph_report() bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), check_existing=False) bpy.ops.wm.open_mainfile(filepath=str(blend_path), load_ui=False) reopened = graph_report() if before != reopened: raise RuntimeError("desktop import save/reopen semantic drift: %s" % blend_path) return reopened def main(glb_root, output_root, report_path): glb_root = Path(glb_root).resolve() output_root = Path(output_root).resolve() report_path = Path(report_path).resolve() output_root.mkdir(parents=True, exist_ok=True) fixtures = [] for fixture_id in FIXTURE_IDS: glb_path = glb_root / (fixture_id + ".glb") blend_path = output_root / (fixture_id + ".blend") graph = import_and_save(glb_path, blend_path) fixtures.append( { "id": fixture_id, "glb": {"file": glb_path.name, "sha256": sha256_file(glb_path)}, "blend": {"file": blend_path.name, "byteLength": blend_path.stat().st_size, "sha256": sha256_file(blend_path)}, "graph": graph, } ) report = { "schemaVersion": 1, "task": "M12-06C", "operation": "DESKTOP_GLB_IMPORT_MAIN_PERSISTENCE_BASELINE", "blenderVersion": bpy.app.version_string, "fixtureCount": len(fixtures), "fixtures": fixtures, "nextTask": "M12-06D", } report_path.parent.mkdir(parents=True, exist_ok=True) report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") print("glb-main-persistence-fixtures-generated fixtures=%s next=%s" % (len(fixtures), report["nextTask"])) if __name__ == "__main__": args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] if len(args) != 3: raise SystemExit("usage: blender --background --python generate-glb-main-persistence-fixtures.py -- GLB_ROOT OUTPUT_ROOT REPORT") main(args[0], args[1], args[2])