diff blender/time_sync/time_from_graph.py @ 2455:2d454737a916

split blender code to new file
author drewp@bigasterisk.com
date Tue, 20 May 2025 09:24:35 -0700
parents 405abed9a45c
children d94480bfb179
line wrap: on
line diff
--- a/blender/time_sync/time_from_graph.py	Mon May 19 21:25:32 2025 -0700
+++ b/blender/time_sync/time_from_graph.py	Tue May 20 09:24:35 2025 -0700
@@ -1,60 +1,70 @@
 import asyncio
 import threading
 import time
-from typing import Coroutine, cast
+from typing import Coroutine
 
-import bpy  # type: ignore
-from bpy.app.handlers import persistent  # type: ignore
-from ..asyncio_thread import startLoopInThread
+from rdfdb.patch import Patch
 from rdfdb.syncedgraph.syncedgraph import SyncedGraph
-from rdflib import URIRef
-from rdflib.graph import _ContextType
+from rdflib import Literal
 
 from light9 import networking, showconfig
+from light9.ascoltami.graph_context import ascoltamiContext
 from light9.namespaces import L9
 from light9.newtypes import decimalLiteral
 from light9.run_local import log
 from light9.typedgraph import typedValue
 
-
-def clamp(lo, hi, x):
-    return max(lo, min(hi, x))
+from ..asyncio_thread import startLoopInThread
+from .blender_time import (
+    BlenderTime,
+    PausedGotoTime,
+    PlayingGotoTime,
+    SceneLoaded,
+    _TimeEvent,
+)
 
 
-UPDATE_PERIOD = 1 / 20
+
 
 
 class Sync:
+    """asco is the authority on playback status. Sync maintains a copy of the state"""
     lock = threading.Lock()
+
+    # these are written ONLY by bg thread
     duration: float = 1
     wallStartTime: float | None = None
     pausedSongTime: float | None = None
+    latestSongTime: float
     playing = False
-    latestTime: float
 
     def __init__(self):
         # main thread
-        self.lastSetFrame = -1
-        self.lastGraphFrame = -1
-        show = showconfig.showUri()
-        self.ctx = cast(_ContextType, (URIRef(show + '/ascoltami')))
+        self.blenderTime = BlenderTime(self.onEvent)
+        self.blenderTime.start()
+
+        self.ctx = ascoltamiContext(showconfig.showUri())
         log.debug('🚋3 startLoopInThread')
-        self._loop = 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)
+        self._loop = startLoopInThread(self.connectGraph())
         log.info('🚋10 Sync initd')
 
-    ## updates from graph -> self
+    def onEvent(self, event: _TimeEvent):
+        # main thread
+        match event:
+            case SceneLoaded():
+                if hasattr(self, "latestSongTime"):
+                    self.blenderTime.setBlenderTime(self.latestSongTime, self.duration)
+            case PausedGotoTime(t):
+                self.runInBackgroundLoop(self.setInGraph(t, False))
+            case PlayingGotoTime(t):
+                self.runInBackgroundLoop(self.setInGraph(t, True))
 
     def runInBackgroundLoop(self, f: Coroutine):
+        # main thread
         asyncio.run_coroutine_threadsafe(f, self._loop)
 
-
-    async def task(self):
-        # bg thread with asyncio loop
+    async def connectGraph(self):
+        # bg thread
         log.info('🚋11 start SyncedGraph')
         self.graph = SyncedGraph(networking.rdfdb.url, "time_sync")
         self.graph.addHandler(self.syncFromGraph)
@@ -76,44 +86,25 @@
         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.runInBackgroundLoop(self.setInGraph(t, bpy.context.screen.is_animation_playing))
+    async def setGraphPlaying(self, isBlenderPlaying: bool):
+        # bg thread
+        log.info(f'set graph playing to {isBlenderPlaying}')
+        self.graph.patchObject(self.ctx, L9['ascoltami'], L9['playing'], Literal(isBlenderPlaying))
 
     async def setInGraph(self, t: float, isBlenderPlaying: bool):
-        log.info(f'set graph time to {t:.2f}')
+        # bg thread
+        log.info(f'set graph time to {t:.2f} {isBlenderPlaying=}')
+        p = Patch()
         if isBlenderPlaying:
-            log.info('  playing mode')
-            p = self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], decimalLiteral(t))
+            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['playing'], Literal(True)))
+            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], decimalLiteral(round(time.time() - t, 1))))
+            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], None))
         else:
-            log.info('  paused mode')
-            p = self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], decimalLiteral(t))
+            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['playing'], Literal(False)))
+            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], None))
+            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], decimalLiteral(round(t, 1))))
 
+        if p.isEmpty():
+            return
+        log.info(f'setInGraph {p.shortSummary()}')
         await self.graph.patch(p)