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

split blender code to new file
author drewp@bigasterisk.com
date Tue, 20 May 2025 09:24:35 -0700
parents 405abed9a45c
children d94480bfb179
line wrap: on
line source

import asyncio
import threading
import time
from typing import Coroutine

from rdfdb.patch import Patch
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
from rdflib import Literal

from light9 import networking, showconfig
from light9.ascoltami.graph_context import ascoltamiContext
from light9.namespaces import L9
from light9.newtypes import decimalLiteral
from light9.run_local import log
from light9.typedgraph import typedValue

from ..asyncio_thread import startLoopInThread
from .blender_time import (
    BlenderTime,
    PausedGotoTime,
    PlayingGotoTime,
    SceneLoaded,
    _TimeEvent,
)





class Sync:
    """asco is the authority on playback status. Sync maintains a copy of the state"""
    lock = threading.Lock()

    # these are written ONLY by bg thread
    duration: float = 1
    wallStartTime: float | None = None
    pausedSongTime: float | None = None
    latestSongTime: float
    playing = False

    def __init__(self):
        # main thread
        self.blenderTime = BlenderTime(self.onEvent)
        self.blenderTime.start()

        self.ctx = ascoltamiContext(showconfig.showUri())
        log.debug('🚋3 startLoopInThread')
        self._loop = startLoopInThread(self.connectGraph())
        log.info('🚋10 Sync initd')

    def onEvent(self, event: _TimeEvent):
        # main thread
        match event:
            case SceneLoaded():
                if hasattr(self, "latestSongTime"):
                    self.blenderTime.setBlenderTime(self.latestSongTime, self.duration)
            case PausedGotoTime(t):
                self.runInBackgroundLoop(self.setInGraph(t, False))
            case PlayingGotoTime(t):
                self.runInBackgroundLoop(self.setInGraph(t, True))

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

    async def connectGraph(self):
        # bg thread
        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

    async def setGraphPlaying(self, isBlenderPlaying: bool):
        # bg thread
        log.info(f'set graph playing to {isBlenderPlaying}')
        self.graph.patchObject(self.ctx, L9['ascoltami'], L9['playing'], Literal(isBlenderPlaying))

    async def setInGraph(self, t: float, isBlenderPlaying: bool):
        # bg thread
        log.info(f'set graph time to {t:.2f} {isBlenderPlaying=}')
        p = Patch()
        if isBlenderPlaying:
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['playing'], Literal(True)))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], decimalLiteral(round(time.time() - t, 1))))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], None))
        else:
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['playing'], Literal(False)))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['wallStartTime'], None))
            p = p.update(self.graph.getObjectPatch(self.ctx, L9['ascoltami'], L9['pausedSongTime'], decimalLiteral(round(t, 1))))

        if p.isEmpty():
            return
        log.info(f'setInGraph {p.shortSummary()}')
        await self.graph.patch(p)