#!/usr/bin/env python3 import hashlib import json import os import pathlib import sys import tempfile import bpy OBJECT_NAME = "WebGapAnimNlaSlotObject" ACTION_NAME = "WebGapAnimNlaSlotAction" STRIP_NAME = "WebGapNlaSlotStrip" SLOT_IDENTIFIER = "OBWebGapNlaSlot" def state_report(): obj = bpy.data.objects.get(OBJECT_NAME) if obj is None or obj.animation_data is None or not obj.animation_data.nla_tracks: raise RuntimeError("slot_unassign_from_nla_strip fixture NLA data is missing") strips = [strip for track in obj.animation_data.nla_tracks for strip in track.strips if strip.name == STRIP_NAME] if len(strips) != 1: raise RuntimeError("slot_unassign_from_nla_strip fixture strip is missing") strip = strips[0] return { "activeObject": bpy.context.view_layer.objects.active.name if bpy.context.view_layer.objects.active else None, "strip": strip.name, "action": strip.action.name if strip.action else None, "actionSlot": strip.action_slot.identifier if strip.action_slot else None, "actionSlotHandle": int(strip.action_slot_handle), "lastSlotIdentifier": strip.last_slot_identifier, "frameStart": float(strip.frame_start), "frameEnd": float(strip.frame_end), } def is_unassigned(state): return state["action"] == ACTION_NAME and state["actionSlot"] is None and state["actionSlotHandle"] == 0 and state["lastSlotIdentifier"] == SLOT_IDENTIFIER def run_operator(strip): 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, "nla_strip": strip} 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_nla_strip.poll()) if not poll: raise RuntimeError("ANIM_OT_slot_unassign_from_nla_strip poll failed") result = bpy.ops.anim.slot_unassign_from_nla_strip() if result != {"FINISHED"}: raise RuntimeError(f"ANIM_OT_slot_unassign_from_nla_strip 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-nla-strip-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) strip = next((strip for track in obj.animation_data.nla_tracks for strip in track.strips if strip.name == STRIP_NAME), None) if obj and obj.animation_data 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 or before["lastSlotIdentifier"] != SLOT_IDENTIFIER: raise RuntimeError(f"slot_unassign_from_nla_strip source state is wrong: {before}") poll, result = run_operator(strip) after = state_report() if not is_unassigned(after): raise RuntimeError(f"slot_unassign_from_nla_strip did not clear the assigned slot: {after}") descriptor, temporary = tempfile.mkstemp(prefix="m16-anim-slot-unassign-nla-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_nla_strip 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-00210", "operation": "ANIM_SLOT_UNASSIGN_FROM_NLA_STRIP_DESKTOP", "fixture": str(fixture), "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "before": before, "after": reopened, "poll": poll, "operatorStatus": "FINISHED", "mainMutation": "NLA_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-nla-strip-desktop-ok poll=true status=FINISHED mainMutation=nla_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-nla-strip-desktop-failed: {error}") raise SystemExit(1)