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))