Mercurial > code > home > repos > light9
view blender/time_sync/time_from_graph.py @ 2454:405abed9a45c default tip
fix up asyncio-in-bg-thread sorcery
author | drewp@bigasterisk.com |
---|---|
date | Mon, 19 May 2025 21:25:32 -0700 |
parents | b23afde50bc2 |
children |
line wrap: on
line source
import asyncio import threading import time from typing import Coroutine, cast import bpy # type: ignore from bpy.app.handlers import persistent # type: ignore from ..asyncio_thread import startLoopInThread from rdfdb.syncedgraph.syncedgraph import SyncedGraph from rdflib import URIRef from rdflib.graph import _ContextType from light9 import networking, showconfig 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)) 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 show = showconfig.showUri() self.ctx = cast(_ContextType, (URIRef(show + '/ascoltami'))) 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) log.info('🚋10 Sync initd') ## updates from graph -> self def runInBackgroundLoop(self, f: Coroutine): asyncio.run_coroutine_threadsafe(f, self._loop) async def task(self): # bg thread with asyncio loop log.info('🚋11 start SyncedGraph') 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.runInBackgroundLoop(self.setInGraph(t, bpy.context.screen.is_animation_playing)) async def setInGraph(self, t: float, isBlenderPlaying: bool): log.info(f'set graph time to {t:.2f}') if isBlenderPlaying: log.info(' playing mode') p = self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], decimalLiteral(t)) else: log.info(' paused mode') p = self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], decimalLiteral(t)) await self.graph.patch(p)