#!/usr/bin/env python3 import hashlib import json import os import pathlib import sys import tempfile import bpy def rounded(value): return round(float(value), 6) def canonical(): obj = bpy.data.objects.get("WBV2P3004RoundTripMeshObject") armature = bpy.data.objects.get("WBV2P3004ArmatureObject") if obj is None or armature is None or obj.type != "MESH" or armature.type != "ARMATURE": raise RuntimeError("GLB_INVALID: roundtrip fixture objects are missing") material = obj.data.materials[0] if obj.data.materials else None principled = material.node_tree.nodes.get("Principled BSDF") if material and material.node_tree else None shape_keys = [] if obj.data.shape_keys: for key in obj.data.shape_keys.key_blocks[1:]: shape_keys.append({"name": key.name, "value": rounded(key.value), "vertexCount": len(key.data)}) weights = [] for vertex in obj.data.vertices: weights.append(sorted(rounded(group.weight) for group in vertex.groups)) action = obj.animation_data.action if obj.animation_data else None channels = [] if action: for layer in action.layers: for strip in layer.strips: for bag in strip.channelbags: channels.extend(curve.data_path for curve in bag.fcurves) return { "mesh": {"name": obj.data.name, "vertexCount": len(obj.data.vertices), "triangleCount": len(obj.data.loop_triangles), "uvLayers": [layer.name for layer in obj.data.uv_layers], "shapeKeys": shape_keys}, "material": {"name": material.name if material else None, "baseColor": [rounded(value) for value in principled.inputs["Base Color"].default_value] if principled else [], "metallic": rounded(principled.inputs["Metallic"].default_value) if principled else None, "roughness": rounded(principled.inputs["Roughness"].default_value) if principled else None, "imagePacked": bool(material and any(node.type == "TEX_IMAGE" and node.image and node.image.packed_file for node in material.node_tree.nodes))}, "skin": {"bones": [bone.name for bone in armature.data.bones], "parents": {bone.name: bone.parent.name if bone.parent else None for bone in armature.data.bones}, "vertexGroups": [group.name for group in obj.vertex_groups], "weightSums": weights}, "animation": {"actionName": action.name if action else None, "channelCount": len(channels), "frameRange": [bpy.context.scene.frame_start, bpy.context.scene.frame_end]}, } def main(): arguments = sys.argv[sys.argv.index("--") + 1:] if len(arguments) != 2: raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p3-004-desktop.py -- FIXTURE REPORT") fixture, output = (pathlib.Path(value).resolve() for value in arguments) bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False) before = canonical() descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p3-004-reopen-", suffix=".blend", dir=fixture.parent) os.close(descriptor) try: bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True) bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False) after = canonical() finally: pathlib.Path(temporary).unlink(missing_ok=True) if before != after: raise RuntimeError(f"GLB_INVALID: desktop save/reopen drift: {before} != {after}") value = {"schemaVersion": 1, "task": "WBV2-P3-004", "operation": "IO_GLB_ROUNDTRIP_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "canonical": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string} output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") print("wbv2-p3-004-desktop-ok vertices=%d triangles=%d bones=%d morphs=%d channels=%d texture=packed saveReopen=exact" % (after["mesh"]["vertexCount"], after["mesh"]["triangleCount"], len(after["skin"]["bones"]), len(after["mesh"]["shapeKeys"]), after["animation"]["channelCount"])) if __name__ == "__main__": main()