Mercurial > code > home > repos > light9
changeset 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 | f21b61884b0f |
children | 405abed9a45c |
files | blender/__init__.py blender/asyncio_thread.py blender/light_control/__init__.py blender/light_control/send_to_collector.py blender/pdm.lock blender/pyproject.toml blender/run.sh blender/time_sync/__init__.py blender/time_sync/time_from_graph.py src/light9/blender/__init__.py src/light9/blender/asyncio_thread.py src/light9/blender/light_control/__init__.py src/light9/blender/light_control/send_to_collector.py src/light9/blender/time_sync/__init__.py src/light9/blender/time_sync/time_from_graph.py |
diffstat | 15 files changed, 795 insertions(+), 318 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/__init__.py Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,34 @@ +import logging +import sys + +from . import light_control, time_sync + +bl_info = { + "name": "light9_sync", + "description": "light9 sync", + "version": (0, 0, 1), + "blender": (4, 4, 0), + "category": "Object", +} + +modules = (time_sync, light_control) + + +def register(): + logging.getLogger('autodepgraphapi').setLevel(logging.INFO) + logging.getLogger('syncedgraph').setLevel(logging.INFO) + for p in [ + '/home/drewp/.local/share/pdm/venvs/blender-XpnfiNSq-3.11/lib/python3.11/site-packages', + '/home/drewp/projects/light9/src/', + '/my/proj/rdfdb', + ]: + if p not in sys.path: + sys.path.append(p) + + for module in modules: + module.register() + + +def unregister(): + for module in reversed(modules): + module.unregister()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/asyncio_thread.py Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,16 @@ +import asyncio +from threading import Thread +from typing import Coroutine + + +def startLoopInThread(task: Coroutine) -> asyncio.AbstractEventLoop: + + def start_background_loop(loop: asyncio.AbstractEventLoop) -> None: + asyncio.set_event_loop(loop) + loop.run_forever() + + loop = asyncio.new_event_loop() + t = Thread(target=start_background_loop, args=(loop,), daemon=True) + t.start() + asyncio.run_coroutine_threadsafe(task, loop) + return loop
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/light_control/__init__.py Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,16 @@ +""" +watch blender lights, output to real lights +""" + +sender: object = None + + +def register(): + global sender + from .send_to_collector import Sender + + sender = Sender() + + +def unregister(): + raise NotImplementedError
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/light_control/send_to_collector.py Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,138 @@ +import asyncio +import logging +from dataclasses import dataclass, field +from os import sync +from typing import cast + +import bpy # type: ignore +from light9_sync.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 RDF, ConjunctiveGraph, Literal, URIRef +from rdflib.graph import _ContextType + +from light9 import networking +from light9_sync.asyncio_thread import startLoopInThread +from light9.namespaces import FUNC, L9 +from light9.newtypes import DeviceUri +from light9.showconfig import showUri + +log = logging.getLogger('sendcoll') + +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.setting, L9['value'], c) + + +@dataclass +class Sender: + 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): + 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 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, + setting=L9['blenderControl/set1'], + devSets=self.devSets, + ) + log.info(f'found {self.syncedObjects[obj.name]}') + + self.watchForUpdates() + + def watchForUpdates(self): + bpy.app.handlers.depsgraph_update_post.append(self.onColorChanges) + bpy.app.handlers.frame_change_post.append(self.onColorChanges) + + def onColorChanges(self, scene, deps): + for obj in deps.objects: + dev = self.syncedObjects.get(obj.name) + if dev is None: + continue + dev.updateColor()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/pdm.lock Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,449 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:80b75d833f60f5d286742dfe9131cbda6859436c581622d5fbf664d9f13e5c1e" + +[[metadata.targets]] +requires_python = "==3.11.*" + +[[package]] +name = "aiohttp" +version = "3.9.5" +requires_python = ">=3.8" +summary = "Async http client/server framework (asyncio)" +groups = ["default"] +dependencies = [ + "aiosignal>=1.1.2", + "async-timeout<5.0,>=4.0; python_version < \"3.11\"", + "attrs>=17.3.0", + "frozenlist>=1.1.1", + "multidict<7.0,>=4.5", + "yarl<2.0,>=1.0", +] +files = [ + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +requires_python = ">=3.9" +summary = "aiosignal: a list of registered asynchronous callbacks" +groups = ["default"] +dependencies = [ + "frozenlist>=1.1.0", +] +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[[package]] +name = "attrs" +version = "25.3.0" +requires_python = ">=3.8" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[[package]] +name = "automat" +version = "25.4.16" +requires_python = ">=3.9" +summary = "Self-service finite-state machines for the programmer on the go." +groups = ["default"] +dependencies = [ + "typing-extensions; python_version < \"3.10\"", +] +files = [ + {file = "automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1"}, + {file = "automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0"}, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "Colored terminal output for Python's logging module" +groups = ["default"] +dependencies = [ + "humanfriendly>=9.1", +] +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[[package]] +name = "constantly" +version = "23.10.4" +requires_python = ">=3.8" +summary = "Symbolic constants in Python" +groups = ["default"] +files = [ + {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, + {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, +] + +[[package]] +name = "frozenlist" +version = "1.6.0" +requires_python = ">=3.9" +summary = "A list-like structure which implements collections.abc.MutableSequence" +groups = ["default"] +files = [ + {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d"}, + {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0"}, + {file = "frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e"}, + {file = "frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860"}, + {file = "frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603"}, + {file = "frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191"}, + {file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"}, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "Human friendly output for text interfaces using Python" +groups = ["default"] +dependencies = [ + "monotonic; python_version == \"2.7\"", + "pyreadline3; sys_platform == \"win32\" and python_version >= \"3.8\"", + "pyreadline; sys_platform == \"win32\" and python_version < \"3.8\"", +] +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "A featureful, immutable, and correct URL for Python." +groups = ["default"] +dependencies = [ + "idna>=2.5", + "typing; python_version < \"3.5\"", +] +files = [ + {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, + {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, +] + +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[[package]] +name = "incremental" +version = "24.7.2" +requires_python = ">=3.8" +summary = "A small library that versions your Python projects." +groups = ["default"] +dependencies = [ + "setuptools>=61.0", + "tomli; python_version < \"3.11\"", +] +files = [ + {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"}, + {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"}, +] + +[[package]] +name = "isodate" +version = "0.6.1" +summary = "An ISO 8601 date/time/duration parser and formatter" +groups = ["default"] +dependencies = [ + "six", +] +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + +[[package]] +name = "multidict" +version = "6.4.3" +requires_python = ">=3.9" +summary = "multidict implementation" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.1.0; python_version < \"3.11\"", +] +files = [ + {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd"}, + {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8"}, + {file = "multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7"}, + {file = "multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378"}, + {file = "multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589"}, + {file = "multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9"}, + {file = "multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec"}, +] + +[[package]] +name = "noise" +version = "1.2.2" +summary = "Perlin noise for Python" +groups = ["default"] +files = [ + {file = "noise-1.2.2.tar.gz", hash = "sha256:57a2797436574391ff63a111e852e53a4164ecd81ad23639641743cd1a209b65"}, + {file = "noise-1.2.2.zip", hash = "sha256:36036cdaca131ddd2ab4397fba649af7f074ec08031e1e0a51031d0ae23b509a"}, +] + +[[package]] +name = "pillow" +version = "10.3.0" +requires_python = ">=3.8" +summary = "Python Imaging Library (Fork)" +groups = ["default"] +files = [ + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, +] + +[[package]] +name = "propcache" +version = "0.3.1" +requires_python = ">=3.9" +summary = "Accelerated property cache" +groups = ["default"] +files = [ + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, + {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, + {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, + {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, + {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +requires_python = ">=3.9" +summary = "pyparsing module - Classes and methods to define and execute parsing grammars" +groups = ["default"] +files = [ + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +requires_python = ">=3.8" +summary = "A python implementation of GNU readline." +groups = ["default"] +marker = "sys_platform == \"win32\" and python_version >= \"3.8\"" +files = [ + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, +] + +[[package]] +name = "rdflib" +version = "7.0.0" +requires_python = ">=3.8.1,<4.0.0" +summary = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." +groups = ["default"] +dependencies = [ + "isodate<0.7.0,>=0.6.0", + "pyparsing<4,>=2.1.0", +] +files = [ + {file = "rdflib-7.0.0-py3-none-any.whl", hash = "sha256:0438920912a642c866a513de6fe8a0001bd86ef975057d6962c79ce4771687cd"}, + {file = "rdflib-7.0.0.tar.gz", hash = "sha256:9995eb8569428059b8c1affd26b25eac510d64f5043d9ce8c84e0d0036e995ae"}, +] + +[[package]] +name = "setuptools" +version = "80.7.1" +requires_python = ">=3.9" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +groups = ["default"] +files = [ + {file = "setuptools-80.7.1-py3-none-any.whl", hash = "sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009"}, + {file = "setuptools-80.7.1.tar.gz", hash = "sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552"}, +] + +[[package]] +name = "six" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "twisted" +version = "24.11.0" +requires_python = ">=3.8.0" +summary = "An asynchronous networking framework written in Python" +groups = ["default"] +dependencies = [ + "attrs>=22.2.0", + "automat>=24.8.0", + "constantly>=15.1", + "hyperlink>=17.1.1", + "incremental>=24.7.0", + "typing-extensions>=4.2.0", + "zope-interface>=5", +] +files = [ + {file = "twisted-24.11.0-py3-none-any.whl", hash = "sha256:fe403076c71f04d5d2d789a755b687c5637ec3bcd3b2b8252d76f2ba65f54261"}, + {file = "twisted-24.11.0.tar.gz", hash = "sha256:695d0556d5ec579dcc464d2856b634880ed1319f45b10d19043f2b57eb0115b5"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + +[[package]] +name = "webcolors" +version = "24.11.1" +requires_python = ">=3.9" +summary = "A library for working with the color formats defined by HTML and CSS." +groups = ["default"] +files = [ + {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, + {file = "webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6"}, +] + +[[package]] +name = "yarl" +version = "1.20.0" +requires_python = ">=3.9" +summary = "Yet another URL library" +groups = ["default"] +dependencies = [ + "idna>=2.0", + "multidict>=4.0", + "propcache>=0.2.1", +] +files = [ + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, + {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, + {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, + {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, + {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, +] + +[[package]] +name = "zope-interface" +version = "7.2" +requires_python = ">=3.8" +summary = "Interfaces for Python" +groups = ["default"] +dependencies = [ + "setuptools", +] +files = [ + {file = "zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2"}, + {file = "zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a"}, + {file = "zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1"}, + {file = "zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe"}, +]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/pyproject.toml Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,23 @@ +[project] +name = "light9_sync" +version = "0.1.0" +description = "Default template for PDM package" +authors = [ + {name = "drew", email = "drewp@bigasterisk.com"}, +] +dependencies = [ + "rdflib==7.0.0", + "aiohttp==3.9.5", + "twisted>=24.11.0", + "coloredlogs>=15.0.1", + "noise>=1.2.2", + "pillow==10.3.0", + "webcolors>=24.11.1", +] +requires-python = "==3.11.*" +readme = "README.md" +license = {text = "MIT"} + + +[tool.pdm] +distribution = false
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/run.sh Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,7 @@ +#!/bin/zsh +HERE=$(realpath $PWD) +ADDONS=$HOME/.config/blender/4.4/scripts/addons +rm -f $ADDONS/light9_sync/ +ln -sf $HERE $ADDONS/light9_sync + +/home/drewp/own/tool/blender-4.4.3-linux-x64/blender --addons light9_sync $LIGHT9_SHOW/blender/01.blend $@ \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/time_sync/__init__.py Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,16 @@ +""" +sets blender time to track ascoltami time; +blender-side time changes are sent back to ascoltami +""" + +sync: object = None + + +def register(): + global sync + from .time_from_graph import Sync + sync = Sync() + + +def unregister(): + raise NotImplementedError
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/blender/time_sync/time_from_graph.py Sun May 18 20:08:35 2025 -0700 @@ -0,0 +1,96 @@ +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}')
--- a/src/light9/blender/__init__.py Sun May 18 14:37:06 2025 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -# ln -s ./ ~/.config/blender/4.2/scripts/addons/light9_sync/ - -#~/own/tool/blender $LIGHT9_SHOW/song1.blend --addons light9_sync -import logging -import sys - -from . import light_control, time_sync - -bl_info = { - "name": "light9_sync", - "description": "light9 sync", - "version": (0, 0, 1), - "blender": (4, 2, 0), - "category": "Object", -} - -modules = (time_sync, light_control) - - -def register(): - logging.getLogger('autodepgraphapi').setLevel(logging.INFO) - logging.getLogger('syncedgraph').setLevel(logging.INFO) - for p in [ - '/home/drewp/.local/share/pdm/venvs/light9-zxQNOBNq-3.11/lib/python3.11/site-packages', - '/home/drewp/projects/light9/src/', - '/my/proj/rdfdb', - ]: - if p not in sys.path: - sys.path.append(p) - - for module in modules: - module.register() - - -def unregister(): - for module in reversed(modules): - module.unregister()
--- a/src/light9/blender/asyncio_thread.py Sun May 18 14:37:06 2025 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -import asyncio -from threading import Thread -from typing import Coroutine - - -def startLoopInThread(task: Coroutine) -> asyncio.AbstractEventLoop: - - def start_background_loop(loop: asyncio.AbstractEventLoop) -> None: - asyncio.set_event_loop(loop) - loop.run_forever() - - loop = asyncio.new_event_loop() - t = Thread(target=start_background_loop, args=(loop,), daemon=True) - t.start() - asyncio.run_coroutine_threadsafe(task, loop) - return loop
--- a/src/light9/blender/light_control/__init__.py Sun May 18 14:37:06 2025 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -""" -watch blender lights, output to real lights -""" - -sender: object = None - - -def register(): - global sender - from .send_to_collector import Sender - - sender = Sender() - - -def unregister(): - raise NotImplementedError
--- a/src/light9/blender/light_control/send_to_collector.py Sun May 18 14:37:06 2025 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,138 +0,0 @@ -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 RDF, ConjunctiveGraph, Literal, URIRef -from rdflib.graph import _ContextType - -from light9 import networking -from light9.blender.asyncio_thread import startLoopInThread -from light9.namespaces import FUNC, L9 -from light9.newtypes import DeviceUri -from light9.showconfig import showUri - -log = logging.getLogger('sendcoll') - -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.setting, L9['value'], c) - - -@dataclass -class Sender: - 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): - 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 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, - setting=L9['blenderControl/set1'], - devSets=self.devSets, - ) - log.info(f'found {self.syncedObjects[obj.name]}') - - self.watchForUpdates() - - def watchForUpdates(self): - bpy.app.handlers.depsgraph_update_post.append(self.onColorChanges) - bpy.app.handlers.frame_change_post.append(self.onColorChanges) - - def onColorChanges(self, scene, deps): - for obj in deps.objects: - dev = self.syncedObjects.get(obj.name) - if dev is None: - continue - dev.updateColor()
--- a/src/light9/blender/time_sync/__init__.py Sun May 18 14:37:06 2025 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -""" -sets blender time to track ascoltami time; -blender-side time changes are sent back to ascoltami -""" - -sync: object = None - - -def register(): - global sync - from .time_from_graph import Sync - sync = Sync() - - -def unregister(): - raise NotImplementedError
--- a/src/light9/blender/time_sync/time_from_graph.py Sun May 18 14:37:06 2025 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,95 +0,0 @@ -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}')