#!/usr/bin/env python3 import hashlib import json import os import pathlib import sys import tempfile import bpy OBJECT_NAME = "WebGapAnimatedTransformConstraintObject" ACTION_NAME = "WebGapAnimatedTransformConstraintAction" CONSTRAINT_NAME = "WebGapAnimatedTransformConstraint" OLD_PATH = f'constraints["{CONSTRAINT_NAME}"].from_min_x' NEW_PATH = f'constraints["{CONSTRAINT_NAME}"].from_min_x_rot' def action_fcurves(obj): action = obj.animation_data.action if obj.animation_data else None if action is None: raise RuntimeError("update_animated_transform_constraints 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 state_report(): obj = bpy.data.objects.get(OBJECT_NAME) if obj is None or obj.animation_data is None: raise RuntimeError("update_animated_transform_constraints object animation is missing") constraint = obj.constraints.get(CONSTRAINT_NAME) if constraint is None: raise RuntimeError("update_animated_transform_constraints Transform constraint is missing") return { "object": obj.name, "action": obj.animation_data.action.name if obj.animation_data.action else None, "constraint": constraint.name, "mapFrom": constraint.map_from, "channels": [ { "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 action_fcurves(obj) ], } def run_operator(): poll = bool(bpy.ops.anim.update_animated_transform_constraints.poll()) if not poll: raise RuntimeError("ANIM_OT_update_animated_transform_constraints poll failed") result = bpy.ops.anim.update_animated_transform_constraints(use_convert_to_radians=True) if result != {"FINISHED"}: raise RuntimeError(f"ANIM_OT_update_animated_transform_constraints 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-update-animated-transform-constraints-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["action"] != ACTION_NAME or len(before["channels"]) != 1 or before["channels"][0]["path"] not in {OLD_PATH, NEW_PATH}: raise RuntimeError(f"unexpected update_animated_transform_constraints source state: {before}") poll, result = run_operator() after = state_report() if after["action"] != ACTION_NAME or after["mapFrom"] != "ROTATION" or len(after["channels"]) != 1: raise RuntimeError(f"update_animated_transform_constraints produced unexpected state: {after}") channel = after["channels"][0] if channel["path"] != NEW_PATH or channel["frames"] != [1.0, 10.0] or channel["values"] != [-30.0, 60.0]: raise RuntimeError(f"update_animated_transform_constraints path/value drift: {after}") descriptor, temporary = tempfile.mkstemp(prefix="m16-anim-update-transform-constraints-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.update_animated_transform_constraints save/reopen drift: {after} != {reopened}") if before["channels"][0]["path"] == OLD_PATH: bpy.ops.wm.save_as_mainfile(filepath=str(fixture), check_existing=False, compress=True) if not output.exists(): report = { "schemaVersion": 1, "task": "M16-GAP-00212", "operation": "ANIM_UPDATE_ANIMATED_TRANSFORM_CONSTRAINTS_DESKTOP", "fixture": str(fixture), "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "before": before, "after": reopened, "useConvertToRadians": True, "poll": poll, "operatorStatus": "FINISHED", "mainMutation": "TRANSFORM_CONSTRAINT_PATHS_UPDATED", "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-update-animated-transform-constraints-desktop-ok poll=true status=FINISHED mainMutation=transform_constraint_paths_updated saveReopen=exact") finally: temporary_path.unlink(missing_ok=True) if __name__ == "__main__": try: main() except Exception as error: print(f"anim-update-animated-transform-constraints-desktop-failed: {error}") raise SystemExit(1)