view blender/time_sync/time_from_graph.py @ 2454:405abed9a45c default tip

fix up asyncio-in-bg-thread sorcery
author drewp@bigasterisk.com
date Mon, 19 May 2025 21:25:32 -0700
parents b23afde50bc2
children
line wrap: on
line source

import asyncio
import threading
import time
from typing import Coroutine, cast

import bpy  # type: ignore
from bpy.app.handlers import persistent  # type: ignore
from ..asyncio_thread import startLoopInThread
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
from rdflib import URIRef
from rdflib.graph import _ContextType

from light9 import networking, showconfig
from light9.namespaces import L9
from light9.newtypes import decimalLiteral
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
        show = showconfig.showUri()
        self.ctx = cast(_ContextType, (URIRef(show + '/ascoltami')))
        log.debug('🚋3 startLoopInThread')
        self._loop = 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)
        log.info('🚋10 Sync initd')

    ## updates from graph -> self

    def runInBackgroundLoop(self, f: Coroutine):
        asyncio.run_coroutine_threadsafe(f, self._loop)


    async def task(self):
        # bg thread with asyncio loop
        log.info('🚋11 start SyncedGraph')
        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.runInBackgroundLoop(self.setInGraph(t, bpy.context.screen.is_animation_playing))

    async def setInGraph(self, t: float, isBlenderPlaying: bool):
        log.info(f'set graph time to {t:.2f}')
        if isBlenderPlaying:
            log.info('  playing mode')
            p = self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], decimalLiteral(t))
        else:
            log.info('  paused mode')
            p = self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], decimalLiteral(t))

        await self.graph.patch(p)