diff blender/time_sync/time_from_graph.py @ 2453:b23afde50bc2

blender addons get thier own pdm setup for now. fix time_from_graph startup race
author drewp@bigasterisk.com
date Sun, 18 May 2025 20:08:35 -0700
parents src/light9/blender/time_sync/time_from_graph.py@e683b449506b
children 405abed9a45c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/blender/time_sync/time_from_graph.py	Sun May 18 20:08:35 2025 -0700
@@ -0,0 +1,96 @@
+import threading
+import time
+
+import bpy  # type: ignore
+from bpy.app.handlers import persistent  # type: ignore
+from light9_sync.asyncio_thread import startLoopInThread
+from rdfdb.syncedgraph.syncedgraph import SyncedGraph
+
+from light9 import networking
+from light9.namespaces import L9
+from light9.run_local import log
+from light9.typedgraph import typedValue
+
+
+def clamp(lo, hi, x):
+    return max(lo, min(hi, x))
+
+
+UPDATE_PERIOD = 1 / 20
+
+
+class Sync:
+    lock = threading.Lock()
+    duration: float = 1
+    wallStartTime: float | None = None
+    pausedSongTime: float | None = None
+    playing = False
+    latestTime: float
+
+    def __init__(self):
+        # main thread
+        self.lastSetFrame = -1
+        self.lastGraphFrame = -1
+        startLoopInThread(self.task())
+        # need persistent because `blender --addons ...` seems to start addon
+        # before loading scene.
+        bpy.app.timers.register(self.update, persistent=True)
+        bpy.app.handlers.frame_change_post.append(self.on_frame_change_post)
+
+    ## updates from graph -> self
+
+    async def task(self):
+        # bg thread with asyncio loop
+        self.graph = SyncedGraph(networking.rdfdb.url, "time_sync")
+        self.graph.addHandler(self.syncFromGraph)
+
+    def syncFromGraph(self):
+        # bg thread
+        with self.lock:
+            asco = L9['ascoltami']
+            self.wallStartTime = typedValue(float | None, self.graph, asco, L9['wallStartTime'])
+            self.pausedSongTime = typedValue(float | None, self.graph, asco, L9['pausedSongTime'])
+            self.duration = typedValue(float | None, self.graph, asco, L9['duration']) or 1.0
+            self.playing = typedValue(bool | None, self.graph, asco, L9['playing']) or False
+
+    def currentTime(self) -> float | None:
+        if self.wallStartTime is not None:
+            return time.time() - self.wallStartTime
+        if self.pausedSongTime is not None:
+            return self.pausedSongTime
+        log.warn('no time data')
+        return None
+
+    ## graph time -> blender time
+
+    # @persistent
+    def update(self):
+        # main thread? wherever blender runs timers
+        if self.playing:
+            with self.lock:
+                t = self.currentTime()
+            if t is not None:
+                self.setBlenderTime(t, self.duration)
+        return UPDATE_PERIOD
+
+    def setBlenderTime(self, t: float, duration: float):
+        scene = bpy.context.scene
+        fps = scene.render.fps
+        scene.frame_start = 0
+        scene.frame_end = int(duration * fps)
+        fr = int(clamp(t, 0, duration) * fps)
+        self.lastSetFrame = fr
+        scene.frame_set(fr)
+
+    ## blender time -> graph time
+
+    @persistent
+    def on_frame_change_post(self, scene, deps):
+        if scene.frame_current != self.lastSetFrame:
+            # self.setGraphTime(scene.frame_current / scene.render.fps, self.duration)
+            self.lastSetFrame = scene.frame_current
+            t = scene.frame_current / scene.render.fps
+            self.setInGraph(t)
+
+    def setInGraph(self, t: float):
+        log.warning(f'todo: set graph to {t:.2f}')