Mercurial > code > home > repos > light9
changeset 2455:2d454737a916
split blender code to new file
author | drewp@bigasterisk.com |
---|---|
date | Tue, 20 May 2025 09:24:35 -0700 |
parents | 405abed9a45c |
children | 917bc2eaf4f4 |
files | blender/light_control/send_to_collector.py blender/time_sync/blender_time.py blender/time_sync/time_from_graph.py |
diffstat | 3 files changed, 159 insertions(+), 61 deletions(-) [+] |
line wrap: on
line diff
--- a/blender/light_control/send_to_collector.py Mon May 19 21:25:32 2025 -0700 +++ b/blender/light_control/send_to_collector.py Tue May 20 09:24:35 2025 -0700 @@ -5,8 +5,8 @@ from typing import cast import bpy # type: ignore -from light9_sync.time_sync.time_from_graph import clamp from light9.effect.effecteval import literalColor +from light9_sync.time_sync.blender_time import clamp from rdfdb.patch import Patch from rdfdb.syncedgraph.syncedgraph import SyncedGraph from rdflib import RDF, ConjunctiveGraph, Literal, URIRef
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/time_sync/blender_time.py Tue May 20 09:24:35 2025 -0700 @@ -0,0 +1,107 @@ +from dataclasses import dataclass +from typing import Callable + +import bpy +from bpy.app.handlers import persistent + +from light9.run_local import log + +UPDATE_PERIOD = 1 / 20 + + +def clamp(lo, hi, x): + return max(lo, min(hi, x)) + + +class _TimeEvent(): + pass + + +class SceneLoaded(_TimeEvent): + pass + + +@dataclass +class PausedGotoTime(_TimeEvent): + t: float + + +@dataclass +class PlayingGotoTime(_TimeEvent): + t: float + + +class BlenderTime: + """all methods to run in main thread""" + + def __init__(self, onEvent: Callable[[_TimeEvent], None]): + self._onEvent = onEvent + self.lastSetFrame = -1 + + def start(self): + bpy.app.handlers.load_post.append(self._on_load_post) + bpy.app.handlers.frame_change_post.append(self._on_frame_change_post) + + # need persistent because `blender --addons ...` seems to start addon + # before loading scene. + bpy.app.timers.register(self.update, persistent=True) + + self.emitEvent(SceneLoaded()) + + def setSceneDuration(self, duration: float): + self.duration = duration + scene = bpy.context.scene + fps = scene.render.fps + scene.frame_start = 0 + scene.frame_end = int(duration * fps) + + def frameDuration(self): + return + # todo: need to be in screen context or something + context_override = bpy.context.copy() + # context_override["selected_objects"] = list(context.scene.objects) + with bpy.context.temp_override(**context_override): + bpy.ops.action.view_all() + + def setCurrentTime(self, t: float): + scene = bpy.context.scene + fps = scene.render.fps + fr = int(clamp(t, 0, self.duration) * fps) + self.lastSetFrame = fr + scene.frame_set(fr) + + def setBlenderTime(self, t: float, duration: float): + log.info(f'set blender time to {t:.2f}') + self.setSceneDuration(duration) + self.frameDuration() + self.setCurrentTime(t) + + def emitEvent(self, event: _TimeEvent): + log.info(f'🌹 emitEvent {event}') + self._onEvent(event) + + @persistent + def update(self): + if 0: + if self.playing: + with self.lock: + t = self.currentTime() + if t is not None: + self.blenderTime.setBlenderTime(t, self.duration) + return UPDATE_PERIOD + + @persistent + def _on_load_post(self, scene, deps): + self.emitEvent(SceneLoaded()) + + @persistent + def _on_frame_change_post(self, scene, deps): + # if scene.frame_current == self.lastSetFrame: + # return + # blender requested this frame change, either playing or paused (scrubbing timeline) + self.lastSetFrame = scene.frame_current + t = round(scene.frame_current / scene.render.fps, 3) + if bpy.context.screen.is_animation_playing and not bpy.context.screen.is_scrubbing: + self.emitEvent(PlayingGotoTime(t=t)) + else: + self.emitEvent(PausedGotoTime(t=t))
--- 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)