changeset 2436:e683b449506b

blender effect that sets lights to match blender lights
author drewp@bigasterisk.com
date Wed, 29 May 2024 15:06:51 -0700
parents 207fe0670952
children 26f84fc67ab1
files src/light9/blender/light_control/send_to_collector.py src/light9/blender/time_sync/time_from_graph.py
diffstat 2 files changed, 75 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/src/light9/blender/light_control/send_to_collector.py	Wed May 29 14:56:58 2024 -0700
+++ b/src/light9/blender/light_control/send_to_collector.py	Wed May 29 15:06:51 2024 -0700
@@ -1,17 +1,20 @@
 import asyncio
 import logging
 from dataclasses import dataclass, field
+from os import sync
+from typing import cast
 
 import bpy  # type: ignore
 from light9.blender.time_sync.time_from_graph import clamp
 from light9.effect.effecteval import literalColor
 from rdfdb.patch import Patch
 from rdfdb.syncedgraph.syncedgraph import SyncedGraph
-from rdflib import Literal, URIRef
+from rdflib import RDF, ConjunctiveGraph, Literal, URIRef
+from rdflib.graph import _ContextType
 
 from light9 import networking
 from light9.blender.asyncio_thread import startLoopInThread
-from light9.namespaces import L9
+from light9.namespaces import FUNC, L9
 from light9.newtypes import DeviceUri
 from light9.showconfig import showUri
 
@@ -20,19 +23,34 @@
 UPDATE_PERIOD = 1 / 20
 
 
+async def waitForConnection(graph: SyncedGraph):
+    # workaround for patch() quietly failing before connection. See SyncedGraph.
+    while not graph._isConnected():
+        await asyncio.sleep(.5)
+
 @dataclass
 class BlenderDev:
     ctx: URIRef
     obj: bpy.types.Object
     uri: DeviceUri
+    setting: URIRef
+    devSets: URIRef
+
     color: tuple[float, float, float] = (0, 0, 0)
 
     def updateColor(self):
         self.color = self.obj.data.color
 
+    def effectGraphStmts(self, bset0) -> list:
+        return [
+            (self.devSets, L9['setting'], self.setting, self.ctx),
+            (self.setting, L9['device'], self.uri, self.ctx),
+            (self.setting, L9['deviceAttr'], L9['color'], self.ctx),
+        ]
+
     def makePatch(self, graph: SyncedGraph) -> Patch:
         c = literalColor(*[clamp(x, 0, 1) for x in self.color])
-        return graph.getObjectPatch(self.ctx, self.uri, L9['blenderColor'], c)
+        return graph.getObjectPatch(self.ctx, self.setting, L9['value'], c)
 
 
 @dataclass
@@ -40,31 +58,70 @@
     syncedObjects: dict[str, BlenderDev] = field(default_factory=dict)
 
     ctx = URIRef(showUri() + '/blender')
+    devSets = L9['blenderControlDevSets']
 
     def __post_init__(self):
         bpy.app.timers.register(self.findLightsInScene, first_interval=0.1)  # todo: what event to use?
         startLoopInThread(self.task())
 
     async def task(self):
-        graph = SyncedGraph(networking.rdfdb.url, "blender_light_control")
-        while True:
-            try:
-                p = Patch(addQuads=[], delQuads=[])
-                for d in self.syncedObjects.values():
-                    p = p.update(d.makePatch(graph))
-                if p:
-                    await graph.patch(p)
-            except Exception:
-                log.error('skip', exc_info=True)
-                await asyncio.sleep(1)
-            await asyncio.sleep(UPDATE_PERIOD)
+        log.info('start task')
+        try:
+            self.graph = SyncedGraph(networking.rdfdb.url, "blender_light_control")
+            await waitForConnection(self.graph)
+            self.patchInitialGraph()
+
+            while True:
+                try:
+                    await self.patchCurrentLightColors()
+                except Exception:
+                    log.error('skip', exc_info=True)
+                    await asyncio.sleep(1)
+                # todo: this could be triggered by onColorChanges instead of running constantly
+                await asyncio.sleep(UPDATE_PERIOD)
+        except Exception:
+            log.error('task', exc_info=True)
+
+    def patchInitialGraph(self):
+        g = self.effectGraph()
+        for d in self.syncedObjects.values():
+            for stmt in d.effectGraphStmts(self.devSets):
+                g.add(stmt)
+        for stmt in g:
+            log.info("adding %s", stmt)
+        self.graph.patchSubgraph(self.ctx, g)
+
+    async def patchCurrentLightColors(self):
+        p = Patch(addQuads=[], delQuads=[])
+        for d in self.syncedObjects.values():
+            p = p.update(d.makePatch(self.graph))
+        if p:
+            await self.graph.patch(p)
+
+    def effectGraph(self) -> ConjunctiveGraph:
+        g = ConjunctiveGraph()
+        ctx = cast(_ContextType, self.ctx)
+        g.add((L9['blenderControl'], RDF.type, L9['Effect'], ctx))
+        g.add((L9['blenderControl'], L9['effectFunction'], FUNC['scale'], ctx))
+        g.add((L9['blenderControl'], L9['publishAttr'], L9['strength'], ctx))
+        setting = L9['blenderControl/set0']
+        g.add((L9['blenderControl'], L9['setting'], setting, ctx))
+        g.add((setting, L9['value'], self.devSets, ctx))
+        g.add((setting, L9['effectAttr'], L9['deviceSettings'], ctx))
+        return g
 
     def findLightsInScene(self):
         self.syncedObjects.clear()
-        for obj in bpy.data.objects:
+        for obj in sorted(bpy.data.objects, key=lambda x: x.name):
             if obj.get('l9dev'):
                 uri = DeviceUri(URIRef(obj['l9dev']))
-                self.syncedObjects[obj.name] = BlenderDev(self.ctx, obj, uri)
+                self.syncedObjects[obj.name] = BlenderDev(
+                    self.ctx,
+                    obj,
+                    uri,
+                    setting=L9['blenderControl/set1'],
+                    devSets=self.devSets,
+                )
                 log.info(f'found {self.syncedObjects[obj.name]}')
 
         self.watchForUpdates()
--- a/src/light9/blender/time_sync/time_from_graph.py	Wed May 29 14:56:58 2024 -0700
+++ b/src/light9/blender/time_sync/time_from_graph.py	Wed May 29 15:06:51 2024 -0700
@@ -50,7 +50,7 @@
             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
+            self.playing = typedValue(bool | None, self.graph, asco, L9['playing']) or False
 
     def currentTime(self) -> float | None:
         if self.wallStartTime is not None: