view 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
line wrap: on
line source

import threading
import time

import bpy  # type: ignore
from bpy.app.handlers import persistent  # type: ignore
from light9_sync.asyncio_thread import startLoopInThread
from rdfdb.syncedgraph.syncedgraph import SyncedGraph

from light9 import networking
from light9.namespaces import L9
from light9.run_local import log
from light9.typedgraph import typedValue


def clamp(lo, hi, x):
    return max(lo, min(hi, x))


UPDATE_PERIOD = 1 / 20


class Sync:
    lock = threading.Lock()
    duration: float = 1
    wallStartTime: float | None = None
    pausedSongTime: float | None = None
    playing = False
    latestTime: float

    def __init__(self):
        # main thread
        self.lastSetFrame = -1
        self.lastGraphFrame = -1
        startLoopInThread(self.task())
        # need persistent because `blender --addons ...` seems to start addon
        # before loading scene.
        bpy.app.timers.register(self.update, persistent=True)
        bpy.app.handlers.frame_change_post.append(self.on_frame_change_post)

    ## updates from graph -> self

    async def task(self):
        # bg thread with asyncio loop
        self.graph = SyncedGraph(networking.rdfdb.url, "time_sync")
        self.graph.addHandler(self.syncFromGraph)

    def syncFromGraph(self):
        # bg thread
        with self.lock:
            asco = L9['ascoltami']
            self.wallStartTime = typedValue(float | None, self.graph, asco, L9['wallStartTime'])
            self.pausedSongTime = typedValue(float | None, self.graph, asco, L9['pausedSongTime'])
            self.duration = typedValue(float | None, self.graph, asco, L9['duration']) or 1.0
            self.playing = typedValue(bool | None, self.graph, asco, L9['playing']) or False

    def currentTime(self) -> float | None:
        if self.wallStartTime is not None:
            return time.time() - self.wallStartTime
        if self.pausedSongTime is not None:
            return self.pausedSongTime
        log.warn('no time data')
        return None

    ## graph time -> blender time

    # @persistent
    def update(self):
        # main thread? wherever blender runs timers
        if self.playing:
            with self.lock:
                t = self.currentTime()
            if t is not None:
                self.setBlenderTime(t, self.duration)
        return UPDATE_PERIOD

    def setBlenderTime(self, t: float, duration: float):
        scene = bpy.context.scene
        fps = scene.render.fps
        scene.frame_start = 0
        scene.frame_end = int(duration * fps)
        fr = int(clamp(t, 0, duration) * fps)
        self.lastSetFrame = fr
        scene.frame_set(fr)

    ## blender time -> graph time

    @persistent
    def on_frame_change_post(self, scene, deps):
        if scene.frame_current != self.lastSetFrame:
            # self.setGraphTime(scene.frame_current / scene.render.fps, self.duration)
            self.lastSetFrame = scene.frame_current
            t = scene.frame_current / scene.render.fps
            self.setInGraph(t)

    def setInGraph(self, t: float):
        log.warning(f'todo: set graph to {t:.2f}')