158 lines
8.0 KiB
Python
158 lines
8.0 KiB
Python
#!/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)
|