view blender/time_sync/blender_time.py @ 2455:2d454737a916

split blender code to new file
author drewp@bigasterisk.com
date Tue, 20 May 2025 09:24:35 -0700
parents blender/time_sync/time_from_graph.py@405abed9a45c
children d94480bfb179
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

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