#!/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 matrix(value): return [rounded(component) for row in value for component in row] def report(): mesh_object = bpy.data.objects.get("WBV2P3003SkinnedMeshObject") armature_object = bpy.data.objects.get("WBV2P3003ArmatureObject") if mesh_object is None or armature_object is None: raise RuntimeError("RIG_INVALID: required objects are missing") armature = armature_object.data bones = [] for bone in armature.bones: pose_bone = armature_object.pose.bones[bone.name] bones.append({ "name": bone.name, "parent": bone.parent.name if bone.parent else None, "head": [rounded(value) for value in bone.head_local], "tail": [rounded(value) for value in bone.tail_local], "poseMatrix": matrix(pose_bone.matrix), "constraints": [{ "name": constraint.name, "type": constraint.type, "influence": rounded(constraint.influence), } for constraint in pose_bone.constraints], }) groups = [group.name for group in mesh_object.vertex_groups] weights = [] for vertex in mesh_object.data.vertices: weights.append({ group.name: rounded(next((item.weight for item in vertex.groups if item.group == group.index), 0.0)) for group in mesh_object.vertex_groups }) shape_keys = [] if mesh_object.data.shape_keys: for key in mesh_object.data.shape_keys.key_blocks: shape_keys.append({ "name": key.name, "value": rounded(key.value), "positions": [rounded(component) for point in key.data for component in point.co], }) modifier = next((value for value in mesh_object.modifiers if value.type == "ARMATURE"), None) if modifier is None or modifier.object != armature_object: raise RuntimeError("RIG_INVALID: armature modifier is missing") object_constraints = [{ "name": constraint.name, "type": constraint.type, "influence": rounded(constraint.influence), } for constraint in mesh_object.constraints] return { "armature": { "id": "armature:" + armature.name, "objectId": "object:" + armature_object.name, "name": armature.name, "bones": bones, }, "mesh": { "id": "mesh:" + mesh_object.data.name, "objectId": "object:" + mesh_object.name, "groups": groups, "weights": weights, "shapeKeys": shape_keys, "armatureModifier": {"name": modifier.name, "objectId": "object:" + armature_object.name}, "constraints": object_constraints, }, "unsupportedConstraint": { "name": "WBV2P3003UnsupportedLimitRotation", "type": "LIMIT_ROTATION", "status": "EXPLICIT_UNSUPPORTED_BY_WASM_DEPSGRAPH", }, } def main(): arguments = sys.argv[sys.argv.index("--") + 1:] if len(arguments) != 2: raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p3-003-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 = report() descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p3-003-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 = report() finally: pathlib.Path(temporary).unlink(missing_ok=True) if before != after: raise RuntimeError(f"RIG_INVALID: desktop save/reopen drift: {before} != {after}") value = { "schemaVersion": 1, "task": "WBV2-P3-003", "operation": "RIG_POSE_SKIN_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "rig": 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-003-desktop-ok armature=%s bones=%d groups=%d shapeKeys=%d unsupported=explicit saveReopen=exact" % (after["armature"]["id"], len(after["armature"]["bones"]), len(after["mesh"]["groups"]), len(after["mesh"]["shapeKeys"]))) if __name__ == "__main__": main()