Files @ 9b101d8bd7ea
Branch filter:

Location: light9/src/light9/blender/time_sync/time_from_graph.py

drewp@bigasterisk.com
discover annotated lights in blender; send their color to the graph (temporary stmt)
import threading
import time

import bpy  # type: ignore
from bpy.app.handlers import persistent  # type: ignore
from light9.typedgraph import typedValue
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
from rdflib import URIRef

from light9 import networking
from light9.blender.asyncio_thread import startLoopInThread
from light9.namespaces import L9
from light9.run_local import log


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())
        bpy.app.timers.register(self.update)
        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}')