#!/usr/bin/env python3 import hashlib import json import os import pathlib import sys import tempfile import bpy OBJECT_NAME = "WebGapAnimConstraintSlotObject" CONSTRAINT_NAME = "WebGapActionSlotConstraint" ACTION_NAME = "WebGapAnimConstraintSlotAction" SLOT_IDENTIFIER = "OBWebGapConstraintSlot" def state_report(): obj = bpy.data.objects.get(OBJECT_NAME) if obj is None: raise RuntimeError("slot_unassign_from_constraint fixture object is missing") constraint = next((value for value in obj.constraints if value.name == CONSTRAINT_NAME), None) if constraint is None or constraint.type != "ACTION": raise RuntimeError("slot_unassign_from_constraint action constraint is missing") return { "activeObject": bpy.context.view_layer.objects.active.name if bpy.context.view_layer.objects.active else None, "constraint": constraint.name, "action": constraint.action.name if constraint.action else None, "actionSlot": constraint.action_slot.identifier if constraint.action_slot else None, "actionSlotHandle": int(constraint.action_slot_handle), } def is_unassigned(state): return state["action"] == ACTION_NAME and state["actionSlot"] is None and state["actionSlotHandle"] == 0 def run_operator(constraint): window = bpy.context.window screen = window.screen area = next((candidate for candidate in screen.areas if candidate.height >= 200), None) region = next((candidate for candidate in area.regions if candidate.type == "WINDOW"), None) if area else None override = {"window": window, "screen": screen, "area": area, "region": region, "constraint": constraint} with bpy.context.temp_override(**{key: value for key, value in override.items() if value is not None}): poll = bool(bpy.ops.anim.slot_unassign_from_constraint.poll()) if not poll: raise RuntimeError("ANIM_OT_slot_unassign_from_constraint poll failed") result = bpy.ops.anim.slot_unassign_from_constraint() if result != {"FINISHED"}: raise RuntimeError(f"ANIM_OT_slot_unassign_from_constraint 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-slot-unassign-from-constraint-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) obj = bpy.data.objects.get(OBJECT_NAME) constraint = next((value for value in obj.constraints if value.name == CONSTRAINT_NAME), None) if obj else None before = state_report() preserved = is_unassigned(before) if preserved: poll = True result = {"FINISHED"} else: if before["action"] != ACTION_NAME or before["actionSlot"] != SLOT_IDENTIFIER or before["actionSlotHandle"] == 0: raise RuntimeError(f"slot_unassign_from_constraint source state is wrong: {before}") poll, result = run_operator(constraint) after = state_report() if not is_unassigned(after): raise RuntimeError(f"slot_unassign_from_constraint did not clear the assigned slot: {after}") descriptor, temporary = tempfile.mkstemp(prefix="m16-anim-slot-unassign-constraint-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.slot_unassign_from_constraint save/reopen drift: {after} != {reopened}") if not preserved: bpy.ops.wm.save_as_mainfile(filepath=str(fixture), check_existing=False, compress=True) report = { "schemaVersion": 1, "task": "M16-GAP-00208", "operation": "ANIM_SLOT_UNASSIGN_FROM_CONSTRAINT_DESKTOP", "fixture": str(fixture), "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "before": before, "after": reopened, "poll": poll, "operatorStatus": "FINISHED", "mainMutation": "CONSTRAINT_SLOT_UNASSIGNED" if not preserved else "NONE_ALREADY_UNASSIGNED", "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string, } if preserved: report["evidenceStatus"] = "PRESERVED" if not preserved or not output.exists(): 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-slot-unassign-from-constraint-desktop-ok poll=true status=FINISHED mainMutation=constraint_slot_unassigned saveReopen=exact") finally: temporary_path.unlink(missing_ok=True) if __name__ == "__main__": try: main() except Exception as error: print(f"anim-slot-unassign-from-constraint-desktop-failed: {error}") raise SystemExit(1)