#!/usr/bin/env python3 import hashlib import json import os import pathlib import sys import tempfile import bpy OBJECT_NAME = "WebGapAnimViewCurveGraphEditorObject" ACTION_NAME = "WebGapAnimViewCurveGraphEditorAction" PROPERTY_NAME = "curve_target" def view_report(area): region = next((candidate for candidate in area.regions if candidate.type == "WINDOW"), None) if region is None: raise RuntimeError("Graph Editor window region is missing") view = region.view2d xmin, ymin = view.region_to_view(0, 0) xmax, ymax = view.region_to_view(region.width - 1, region.height - 1) return { "areaType": area.type, "mode": area.spaces.active.mode, "view2d": { "cur": {"xmin": round(float(xmin), 6), "xmax": round(float(xmax), 6), "ymin": round(float(ymin), 6), "ymax": round(float(ymax), 6)}, "mask": {"xmin": 0, "xmax": int(region.width - 1), "ymin": 0, "ymax": int(region.height - 1)}, }, } def action_report(obj): action = obj.animation_data.action if obj.animation_data else None if action is None: raise RuntimeError("Graph Editor Action is missing") channels = [] for layer in action.layers: for strip in layer.strips: for bag in strip.channelbags: for curve in bag.fcurves: channels.append({"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], "selected": bool(curve.select)}) channels.sort(key=lambda value: (value["path"], value["index"])) return {"name": action.name, "channels": channels} def state_report(): obj = bpy.data.objects.get(OBJECT_NAME) if obj is None or obj.animation_data is None: raise RuntimeError("view_curve_in_graph_editor fixture object is missing") area = next((candidate for screen in bpy.data.screens for candidate in screen.areas if candidate.type == "GRAPH_EDITOR"), None) if area is None: raise RuntimeError("Graph Editor area is missing") return {"selected": bool(obj.select_get()), "active": bpy.context.view_layer.objects.active == obj, "value": round(float(obj[PROPERTY_NAME]), 6), "action": action_report(obj), "view": view_report(area)} class ViewCurvePanel(bpy.types.Panel): bl_label = "View Curve Fixture" bl_idname = "WEBGAP_PT_view_curve_in_graph_editor" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" def draw(self, context): if context.object is not None: self.layout.prop(context.object, f'["{PROPERTY_NAME}"]', text=PROPERTY_NAME) def main(): arguments = sys.argv[sys.argv.index("--") + 1 :] if len(arguments) != 2: raise SystemExit("usage: blender --factory-startup --python check-action-view-curve-in-graph-editor-desktop.py -- FIXTURE REPORT") fixture, output = (pathlib.Path(value).resolve() for value in arguments) bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=True) before = state_report() expected_channel = {"path": f'["{PROPERTY_NAME}"]', "index": 0, "frames": [1.0, 10.0], "values": [-3.0, 7.0], "selected": True} if before["action"]["name"] != ACTION_NAME or before["action"]["channels"] != [expected_channel]: raise RuntimeError(f"unexpected source action: {before}") def finish_report(before_state, after_state, poll, operator_status, main_mutation): descriptor, temporary = tempfile.mkstemp(prefix="m16-anim-view-curve-graph-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=True) reopened = state_report() if reopened != after_state: raise RuntimeError(f"view_curve_in_graph_editor save/reopen drift: {after_state} != {reopened}") output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps({"schemaVersion": 1, "task": "M16-GAP-00214", "operation": "ANIM_VIEW_CURVE_IN_GRAPH_EDITOR_DESKTOP", "fixture": str(fixture), "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "before": before_state, "after": reopened, "poll": poll, "operatorStatus": operator_status, "mainMutation": main_mutation, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(f"anim-view-curve-in-graph-editor-desktop-ok poll={str(poll).lower()} status={operator_status} mainMutation={main_mutation.lower()} saveReopen=exact") finally: temporary_path.unlink(missing_ok=True) bpy.utils.register_class(ViewCurvePanel) if bpy.app.background: try: screen = bpy.context.screen properties_area = next(candidate for candidate in screen.areas if candidate.type == "PROPERTIES") properties_region = next(candidate for candidate in properties_area.regions if candidate.type == "WINDOW") with bpy.context.temp_override(window=bpy.context.window, screen=screen, area=properties_area, region=properties_region): poll = bool(bpy.ops.anim.view_curve_in_graph_editor.poll()) result = bpy.ops.anim.view_curve_in_graph_editor(all=False, isolate=False) if poll else set() operator_status = "FINISHED" if "FINISHED" in result else "CANCELLED" finish_report(before, state_report(), poll, operator_status, "GRAPH_VIEW_FRAMED" if operator_status == "FINISHED" else "NONE") finally: bpy.utils.unregister_class(ViewCurvePanel) return state = {"started": False, "done": False} def blender_window(): import subprocess windows = subprocess.check_output(["xdotool", "search", "--name", "Blender"], text=True).split() if not windows: raise RuntimeError("Blender window was not found") return windows[0] def activate_property_button(window_id): import subprocess sequence = ("sleep 2; " f"xdotool mousemove --sync --window {window_id} 1170 746; " "xdotool click --repeat 10 --delay 60 5; " "sleep 0.5; " f"xdotool mousemove --sync --window {window_id} 1185 645; " "xdotool click 1") subprocess.Popen(["sh", "-c", sequence], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def poll_result(): if state["done"]: return None screen = bpy.context.screen properties_area = next((candidate for candidate in screen.areas if candidate.type == "PROPERTIES"), None) if properties_area is None: return 0.25 properties_region = next(candidate for candidate in properties_area.regions if candidate.type == "WINDOW") with bpy.context.temp_override(window=bpy.context.window, screen=screen, area=properties_area, region=properties_region): poll = bool(bpy.ops.anim.view_curve_in_graph_editor.poll()) result = bpy.ops.anim.view_curve_in_graph_editor(all=False, isolate=False) if poll else set() if "FINISHED" not in result: return 0.25 state["done"] = True after = state_report() finish_report(before, after, poll, "FINISHED", "GRAPH_VIEW_FRAMED") bpy.utils.unregister_class(ViewCurvePanel) bpy.ops.wm.quit_blender() return None def drive(): if state["started"]: return 0.25 state["started"] = True activate_property_button(blender_window()) bpy.app.timers.register(poll_result, first_interval=2.5) return None bpy.app.timers.register(drive, first_interval=1.5) bpy.app.timers.register(lambda: None if state["done"] else (_ for _ in ()).throw(RuntimeError("UI Graph Editor operator timed out")), first_interval=30.0) if __name__ == "__main__": try: main() except Exception as error: print(f"anim-view-curve-in-graph-editor-desktop-failed: {error}") raise SystemExit(1)