comparison 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
comparison
equal deleted inserted replaced
2452:f21b61884b0f 2453:b23afde50bc2
1 import threading
2 import time
3
4 import bpy # type: ignore
5 from bpy.app.handlers import persistent # type: ignore
6 from light9_sync.asyncio_thread import startLoopInThread
7 from rdfdb.syncedgraph.syncedgraph import SyncedGraph
8
9 from light9 import networking
10 from light9.namespaces import L9
11 from light9.run_local import log
12 from light9.typedgraph import typedValue
13
14
15 def clamp(lo, hi, x):
16 return max(lo, min(hi, x))
17
18
19 UPDATE_PERIOD = 1 / 20
20
21
22 class Sync:
23 lock = threading.Lock()
24 duration: float = 1
25 wallStartTime: float | None = None
26 pausedSongTime: float | None = None
27 playing = False
28 latestTime: float
29
30 def __init__(self):
31 # main thread
32 self.lastSetFrame = -1
33 self.lastGraphFrame = -1
34 startLoopInThread(self.task())
35 # need persistent because `blender --addons ...` seems to start addon
36 # before loading scene.
37 bpy.app.timers.register(self.update, persistent=True)
38 bpy.app.handlers.frame_change_post.append(self.on_frame_change_post)
39
40 ## updates from graph -> self
41
42 async def task(self):
43 # bg thread with asyncio loop
44 self.graph = SyncedGraph(networking.rdfdb.url, "time_sync")
45 self.graph.addHandler(self.syncFromGraph)
46
47 def syncFromGraph(self):
48 # bg thread
49 with self.lock:
50 asco = L9['ascoltami']
51 self.wallStartTime = typedValue(float | None, self.graph, asco, L9['wallStartTime'])
52 self.pausedSongTime = typedValue(float | None, self.graph, asco, L9['pausedSongTime'])
53 self.duration = typedValue(float | None, self.graph, asco, L9['duration']) or 1.0
54 self.playing = typedValue(bool | None, self.graph, asco, L9['playing']) or False
55
56 def currentTime(self) -> float | None:
57 if self.wallStartTime is not None:
58 return time.time() - self.wallStartTime
59 if self.pausedSongTime is not None:
60 return self.pausedSongTime
61 log.warn('no time data')
62 return None
63
64 ## graph time -> blender time
65
66 # @persistent
67 def update(self):
68 # main thread? wherever blender runs timers
69 if self.playing:
70 with self.lock:
71 t = self.currentTime()
72 if t is not None:
73 self.setBlenderTime(t, self.duration)
74 return UPDATE_PERIOD
75
76 def setBlenderTime(self, t: float, duration: float):
77 scene = bpy.context.scene
78 fps = scene.render.fps
79 scene.frame_start = 0
80 scene.frame_end = int(duration * fps)
81 fr = int(clamp(t, 0, duration) * fps)
82 self.lastSetFrame = fr
83 scene.frame_set(fr)
84
85 ## blender time -> graph time
86
87 @persistent
88 def on_frame_change_post(self, scene, deps):
89 if scene.frame_current != self.lastSetFrame:
90 # self.setGraphTime(scene.frame_current / scene.render.fps, self.duration)
91 self.lastSetFrame = scene.frame_current
92 t = scene.frame_current / scene.render.fps
93 self.setInGraph(t)
94
95 def setInGraph(self, t: float):
96 log.warning(f'todo: set graph to {t:.2f}')