Mercurial > code > home > repos > light9
view blender/time_sync/blender_time.py @ 2458:0e27ba33118c default tip
better blender<->asco playback cooperation. still no play support in blender; only seek
author | drewp@bigasterisk.com |
---|---|
date | Tue, 20 May 2025 16:25:06 -0700 |
parents | d94480bfb179 |
children |
line wrap: on
line source
from dataclasses import dataclass from typing import Callable import bpy from bpy.app.handlers import persistent from light9.run_local import log from light9.ascoltami.play_state import AscoPlayState 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 @dataclass class BlenderPlayState: isAnimationPlaying: bool isScrubbing: bool class BlenderTime: """all methods to run in main thread""" def __init__(self, onEvent: Callable[[_TimeEvent], None], ascoltamiPlayStateRO: AscoPlayState): self.ascoltamiPlayStateRO = ascoltamiPlayStateRO self._onEvent = onEvent self.lastSetFrame = -1 self.lastPlayState = BlenderPlayState(False, False) self.loaded = False self.curFrameDirty = True self.durationDirty = True 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): if not self.loaded: return self.duration = duration scene = bpy.context.scene fps = scene.render.fps scene.frame_start = 0 scene.frame_end = int(duration * fps) def frameDuration(self): """press Home on the timeline to frame the whole range""" 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): if not self.loaded: return scene = bpy.context.scene fps = scene.render.fps fr = int(clamp(t * fps, 0, scene.frame_end)) self.lastSetFrame = fr scene.frame_set(fr) def setRange(self, duration: float): self.setSceneDuration(duration) self.frameDuration() def _emitEvent(self, event: _TimeEvent): # log.info(f'🌹 emitEvent {event}') self._onEvent(event) @persistent def _update(self): if self.curFrameDirty or self.ascoltamiPlayStateRO.playing: self._followAscoTime() self.curFrameDirty=False if self.durationDirty: if (d := self.ascoltamiPlayStateRO.duration) is not None: self.setRange(d) self.durationDirty = False # # todo: playing in blender should start ascoltami playback # cur = BlenderPlayState(bpy.context.screen.is_animation_playing, bpy.context.screen.is_scrubbing) return UPDATE_PERIOD def _followAscoTime(self): if (t := self.ascoltamiPlayStateRO.getCurrentSongTime()) is not None: self.setCurrentTime(t) @persistent def _on_load_post(self, scene, deps): self._emitEvent(SceneLoaded()) self.loaded = True self.curFrameDirty = True @persistent def _on_frame_change_post(self, scene, deps): # log.info(f' _on_frame_change_post {self.lastPlayState}') 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 self.lastPlayState.isAnimationPlaying and not self.lastPlayState.isScrubbing: self._emitEvent(PlayingGotoTime(t=t)) else: self._emitEvent(PausedGotoTime(t=t))