Mercurial > code > home > repos > light9
comparison blender/time_sync/blender_time.py @ 2457:d94480bfb179
more work on blender time sync. Might be working, aside from blender play-button
author | drewp@bigasterisk.com |
---|---|
date | Tue, 20 May 2025 13:48:07 -0700 |
parents | 2d454737a916 |
children |
comparison
equal
deleted
inserted
replaced
2456:917bc2eaf4f4 | 2457:d94480bfb179 |
---|---|
3 | 3 |
4 import bpy | 4 import bpy |
5 from bpy.app.handlers import persistent | 5 from bpy.app.handlers import persistent |
6 | 6 |
7 from light9.run_local import log | 7 from light9.run_local import log |
8 from light9.ascoltami.play_state import AscoPlayState | |
8 | 9 |
9 UPDATE_PERIOD = 1 / 20 | 10 UPDATE_PERIOD = 1 / 20 |
10 | 11 |
11 | 12 |
12 def clamp(lo, hi, x): | 13 def clamp(lo, hi, x): |
29 @dataclass | 30 @dataclass |
30 class PlayingGotoTime(_TimeEvent): | 31 class PlayingGotoTime(_TimeEvent): |
31 t: float | 32 t: float |
32 | 33 |
33 | 34 |
35 @dataclass | |
36 class BlenderPlayState: | |
37 isAnimationPlaying: bool | |
38 isScrubbing: bool | |
39 | |
40 | |
34 class BlenderTime: | 41 class BlenderTime: |
35 """all methods to run in main thread""" | 42 """all methods to run in main thread""" |
36 | 43 |
37 def __init__(self, onEvent: Callable[[_TimeEvent], None]): | 44 def __init__(self, onEvent: Callable[[_TimeEvent], None], ascoltamiPlayStateRO: AscoPlayState): |
45 self.ascoltamiPlayStateRO = ascoltamiPlayStateRO | |
38 self._onEvent = onEvent | 46 self._onEvent = onEvent |
39 self.lastSetFrame = -1 | 47 self.lastSetFrame = -1 |
48 self.lastPlayState = BlenderPlayState(False, False) | |
49 self.loaded = False | |
50 self.curFrameDirty = True | |
51 self.durationDirty = True | |
40 | 52 |
41 def start(self): | 53 def start(self): |
42 bpy.app.handlers.load_post.append(self._on_load_post) | 54 bpy.app.handlers.load_post.append(self._on_load_post) |
43 bpy.app.handlers.frame_change_post.append(self._on_frame_change_post) | 55 bpy.app.handlers.frame_change_post.append(self._on_frame_change_post) |
44 | 56 |
45 # need persistent because `blender --addons ...` seems to start addon | 57 # need persistent because `blender --addons ...` seems to start addon |
46 # before loading scene. | 58 # before loading scene. |
47 bpy.app.timers.register(self.update, persistent=True) | 59 bpy.app.timers.register(self._update, persistent=True) |
48 | 60 |
49 self.emitEvent(SceneLoaded()) | 61 self._emitEvent(SceneLoaded()) |
50 | 62 |
51 def setSceneDuration(self, duration: float): | 63 def setSceneDuration(self, duration: float): |
64 if not self.loaded: | |
65 return | |
52 self.duration = duration | 66 self.duration = duration |
53 scene = bpy.context.scene | 67 scene = bpy.context.scene |
54 fps = scene.render.fps | 68 fps = scene.render.fps |
55 scene.frame_start = 0 | 69 scene.frame_start = 0 |
56 scene.frame_end = int(duration * fps) | 70 scene.frame_end = int(duration * fps) |
57 | 71 |
58 def frameDuration(self): | 72 def frameDuration(self): |
73 """press Home on the timeline to frame the whole range""" | |
59 return | 74 return |
60 # todo: need to be in screen context or something | 75 # todo: need to be in screen context or something |
61 context_override = bpy.context.copy() | 76 context_override = bpy.context.copy() |
62 # context_override["selected_objects"] = list(context.scene.objects) | 77 # context_override["selected_objects"] = list(context.scene.objects) |
63 with bpy.context.temp_override(**context_override): | 78 with bpy.context.temp_override(**context_override): |
64 bpy.ops.action.view_all() | 79 bpy.ops.action.view_all() |
65 | 80 |
66 def setCurrentTime(self, t: float): | 81 def setCurrentTime(self, t: float): |
82 if not self.loaded: | |
83 return | |
67 scene = bpy.context.scene | 84 scene = bpy.context.scene |
68 fps = scene.render.fps | 85 fps = scene.render.fps |
69 fr = int(clamp(t, 0, self.duration) * fps) | 86 fr = int(clamp(t * fps, 0, scene.frame_end)) |
70 self.lastSetFrame = fr | 87 self.lastSetFrame = fr |
71 scene.frame_set(fr) | 88 scene.frame_set(fr) |
72 | 89 |
73 def setBlenderTime(self, t: float, duration: float): | 90 def setRange(self, duration: float): |
74 log.info(f'set blender time to {t:.2f}') | |
75 self.setSceneDuration(duration) | 91 self.setSceneDuration(duration) |
76 self.frameDuration() | 92 self.frameDuration() |
77 self.setCurrentTime(t) | |
78 | 93 |
79 def emitEvent(self, event: _TimeEvent): | 94 def _emitEvent(self, event: _TimeEvent): |
80 log.info(f'🌹 emitEvent {event}') | 95 # log.info(f'🌹 emitEvent {event}') |
81 self._onEvent(event) | 96 self._onEvent(event) |
82 | 97 |
83 @persistent | 98 @persistent |
84 def update(self): | 99 def _update(self): |
85 if 0: | 100 if self.curFrameDirty or self.ascoltamiPlayStateRO.playing: |
86 if self.playing: | 101 self._followAscoTime() |
87 with self.lock: | 102 self.curFrameDirty=False |
88 t = self.currentTime() | 103 if self.durationDirty: |
89 if t is not None: | 104 if (d := self.ascoltamiPlayStateRO.duration) is not None: |
90 self.blenderTime.setBlenderTime(t, self.duration) | 105 self.setRange(d) |
106 self.durationDirty = False | |
107 # # todo: playing in blender should start ascoltami playback | |
108 # cur = BlenderPlayState(bpy.context.screen.is_animation_playing, bpy.context.screen.is_scrubbing) | |
109 | |
91 return UPDATE_PERIOD | 110 return UPDATE_PERIOD |
111 | |
112 def _followAscoTime(self): | |
113 if (t := self.ascoltamiPlayStateRO.getCurrentSongTime()) is not None: | |
114 self.setCurrentTime(t) | |
92 | 115 |
93 @persistent | 116 @persistent |
94 def _on_load_post(self, scene, deps): | 117 def _on_load_post(self, scene, deps): |
95 self.emitEvent(SceneLoaded()) | 118 self._emitEvent(SceneLoaded()) |
119 self.loaded = True | |
120 self.curFrameDirty = True | |
96 | 121 |
97 @persistent | 122 @persistent |
98 def _on_frame_change_post(self, scene, deps): | 123 def _on_frame_change_post(self, scene, deps): |
99 # if scene.frame_current == self.lastSetFrame: | 124 # log.info(f' _on_frame_change_post {self.lastPlayState}') |
100 # return | 125 if scene.frame_current == self.lastSetFrame: |
126 return | |
101 # blender requested this frame change, either playing or paused (scrubbing timeline) | 127 # blender requested this frame change, either playing or paused (scrubbing timeline) |
102 self.lastSetFrame = scene.frame_current | 128 self.lastSetFrame = scene.frame_current |
103 t = round(scene.frame_current / scene.render.fps, 3) | 129 t = round(scene.frame_current / scene.render.fps, 3) |
104 if bpy.context.screen.is_animation_playing and not bpy.context.screen.is_scrubbing: | 130 if self.lastPlayState.isAnimationPlaying and not self.lastPlayState.isScrubbing: |
105 self.emitEvent(PlayingGotoTime(t=t)) | 131 self._emitEvent(PlayingGotoTime(t=t)) |
106 else: | 132 else: |
107 self.emitEvent(PausedGotoTime(t=t)) | 133 self._emitEvent(PausedGotoTime(t=t)) |