#!/usr/bin/env python3 import hashlib import json import os import pathlib import sys import tempfile import bpy ARMATURE_NAME = "WebGapVersionBoneHideArmature" OBJECT_NAME = "WebGapVersionBoneHideObject" BONE_NAME = "WebGapVersionBoneHideBone" ARMATURE_ACTION_NAME = "WebGapVersionBoneHideArmatureAction" OBJECT_ACTION_NAME = "WebGapVersionBoneHideObjectAction" OLD_PATH = f'bones["{BONE_NAME}"].hide' NEW_PATH = f'pose.bones["{BONE_NAME}"].hide' def action_fcurves(id_block): action = id_block.animation_data.action if id_block.animation_data else None if action is None: raise RuntimeError(f"{id_block.name} animation action is missing") curves = [] for layer in action.layers: for strip in layer.strips: for channelbag in strip.channelbags: curves.extend(channelbag.fcurves) return curves def curve_report(curves): return [ { "path": curve.data_path, "index": int(curve.array_index), "frames": [float(key.co.x) for key in curve.keyframe_points], "values": [float(key.co.y) for key in curve.keyframe_points], } for curve in curves ] def state_report(): armature = bpy.data.armatures.get(ARMATURE_NAME) obj = bpy.data.objects.get(OBJECT_NAME) if armature is None or obj is None or obj.type != "ARMATURE" or obj.data != armature: raise RuntimeError("version_bone_hide_property armature fixture is missing") return { "selected": obj.select_get(), "active": bpy.context.view_layer.objects.active.name if bpy.context.view_layer.objects.active else None, "armatureAction": armature.animation_data.action.name if armature.animation_data and armature.animation_data.action else None, "armatureChannels": curve_report(action_fcurves(armature)), "objectAction": obj.animation_data.action.name if obj.animation_data and obj.animation_data.action else None, "objectChannels": curve_report(action_fcurves(obj)), } def run_operator(): poll = bool(bpy.ops.anim.version_bone_hide_property.poll()) if not poll: raise RuntimeError("ANIM_OT_version_bone_hide_property poll failed") result = bpy.ops.anim.version_bone_hide_property() if result != {"FINISHED"}: raise RuntimeError(f"ANIM_OT_version_bone_hide_property returned {result}") return poll, result def main(): arguments = sys.argv[sys.argv.index("--") + 1 :] if len(arguments) != 2: raise SystemExit("usage: blender -b --factory-startup --python check-action-version-bone-hide-property-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 = state_report() if before["armatureAction"] != ARMATURE_ACTION_NAME or before["objectAction"] != OBJECT_ACTION_NAME: raise RuntimeError(f"unexpected version_bone_hide_property source actions: {before}") if len(before["armatureChannels"]) != 1 or before["armatureChannels"][0]["path"] != OLD_PATH: raise RuntimeError(f"unexpected armature hide channel: {before}") object_has_new = any(channel["path"] == NEW_PATH for channel in before["objectChannels"]) poll = True if not object_has_new: poll, _result = run_operator() after = state_report() if not after["selected"] or after["active"] != OBJECT_NAME: raise RuntimeError(f"version_bone_hide_property lost armature selection: {after}") if len(after["armatureChannels"]) != 1 or after["armatureChannels"][0]["path"] != OLD_PATH: raise RuntimeError(f"version_bone_hide_property changed source action: {after}") copied = [channel for channel in after["objectChannels"] if channel["path"] == NEW_PATH] if len(copied) != 1 or copied[0]["frames"] != [1.0, 10.0] or copied[0]["values"] != [0.0, 1.0]: raise RuntimeError(f"version_bone_hide_property did not copy the hide channel: {after}") descriptor, temporary = tempfile.mkstemp(prefix="m16-anim-version-bone-hide-reopen-", suffix=".blend") os.close(descriptor) temporary_path = pathlib.Path(temporary) try: bpy.ops.wm.save_as_mainfile(filepath=str(temporary_path), check_existing=False, compress=True) bpy.ops.wm.open_mainfile(filepath=str(temporary_path), load_ui=False) reopened = state_report() if reopened != after: raise RuntimeError(f"anim.version_bone_hide_property save/reopen drift: {after} != {reopened}") if not object_has_new: bpy.ops.wm.save_as_mainfile(filepath=str(fixture), check_existing=False, compress=True) if not output.exists(): report = { "schemaVersion": 1, "task": "M16-GAP-00213", "operation": "ANIM_VERSION_BONE_HIDE_PROPERTY_DESKTOP", "fixture": str(fixture), "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "before": before, "after": reopened, "poll": poll, "operatorStatus": "FINISHED", "mainMutation": "BONE_HIDE_FCURVE_MOVED_TO_OBJECT_ACTION", "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string, } output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") print("anim-version-bone-hide-property-desktop-ok poll=true status=FINISHED mainMutation=bone_hide_fcurve_moved saveReopen=exact") finally: temporary_path.unlink(missing_ok=True) if __name__ == "__main__": try: main() except Exception as error: print(f"anim-version-bone-hide-property-desktop-failed: {error}") raise SystemExit(1)