SceneStore - Reactive Scene Graph Management¶
The SceneStore class provides a reactive store for managing 3D scene graph state and synchronizing it across multiple connected VuerSession clients.
Overview¶
SceneStore solves a common problem in multi-client visualization: maintaining consistent scene state across all connected browsers while allowing the Python backend to track and modify the scene.
Key features:
Reactive updates: Changes automatically propagate to all subscribed sessions
Snapshot access: Get the current scene state at any time
Auto-cleanup: Context manager pattern prevents memory leaks from disconnected sessions
Familiar API: Uses the same
@operator pattern as VuerSession
Installation¶
SceneStore is included in the vuer package:
pip install vuer
Quick Start¶
import asyncio
from vuer import Vuer, VuerSession
from vuer.rtc.scene_store import SceneStore
from vuer.schemas import Box, Sphere, DefaultScene
app = Vuer()
scene_store = SceneStore()
@app.spawn(start=True)
async def main(sess: VuerSession):
# Subscribe session - auto-unsubscribes when context exits
async with scene_store.subscribe(sess):
# Set initial scene
await scene_store.set_scene(
children=[
Box(key="box-1", position=[0, 0, 0], color="red"),
]
)
# Main loop - update scene reactively
t = 0
while True:
# Upsert updates existing nodes or adds new ones
await scene_store.upsert @ Sphere(
key="sphere-1",
position=[2 * math.sin(t), 0, 2 * math.cos(t)],
color="blue",
)
t += 0.1
await asyncio.sleep(0.05)
API Reference¶
SceneStore¶
from vuer.rtc.scene_store import SceneStore
store = SceneStore()
Properties¶
Property |
Type |
Description |
|---|---|---|
|
|
Returns a deep copy of the current scene state |
|
|
Proxy for upsert operations with |
|
|
Proxy for add operations with |
|
|
Proxy for update operations with |
|
|
Proxy for remove operations with |
Methods¶
subscribe(session: VuerSession) -> SubscriptionContext¶
Subscribe a session to receive scene updates. Use as an async context manager:
async with scene_store.subscribe(sess):
# Session receives all updates here
await scene_store.set_scene(...)
# Session automatically unsubscribed - no memory leaks
async set_scene(...)¶
Set the entire scene state:
await scene_store.set_scene(
children=[Box(key="box")],
bgChildren=[AmbientLight()],
position=[0, 0, 0],
rotation=[0, 0, 0],
scale=[1, 1, 1],
up=[0, 1, 0],
)
async upsert_async(*nodes, to=None)¶
Update existing nodes or add new ones:
await scene_store.upsert_async(
Box(key="box-1", color="red"),
Sphere(key="sphere-1", radius=0.5),
)
async add_async(*nodes, to=None)¶
Add new nodes to the scene:
# Add to root
await scene_store.add_async(Box(key="box"))
# Add to a specific parent
await scene_store.add_async(Sphere(key="child"), to="parent-group")
async update_async(*updates)¶
Update properties of existing nodes:
await scene_store.update_async(
{"key": "box-1", "color": "blue", "position": [1, 0, 0]},
)
async remove_async(*keys)¶
Remove nodes by key:
await scene_store.remove_async("box-1", "sphere-1")
get_node(key: str) -> Optional[SceneNode]¶
Find a node by key:
node = scene_store.get_node("box-1")
if node:
print(f"Found: {node.tag} at {node.properties.get('position')}")
has_node(key: str) -> bool¶
Check if a node exists:
if scene_store.has_node("box-1"):
await scene_store.remove_async("box-1")
Using the @ Operator¶
SceneStore supports the same @ operator pattern as VuerSession:
# Upsert
await scene_store.upsert @ Box(key="box", color="red")
await scene_store.upsert @ [Box(key="b1"), Sphere(key="s1")]
# Add
await scene_store.add @ Sphere(key="sphere")
await scene_store.add(to="parent") @ Box(key="child")
# Update
await scene_store.update @ {"key": "box", "color": "blue"}
# Remove
await scene_store.remove @ "box-key"
await scene_store.remove @ ["key1", "key2"]
Usage Patterns¶
Multi-Session Synchronization¶
scene_store = SceneStore()
@app.spawn(start=True)
async def handle_session(sess: VuerSession):
async with scene_store.subscribe(sess):
# New clients automatically receive current scene state
# via set_scene or can query snapshot
# Send current state to new client
snapshot = scene_store.snapshot
sess.set @ Scene(**snapshot.to_dict())
# Handle client events
async for event in sess:
if event.etype == "CLICK":
# All subscribed sessions see this update
await scene_store.upsert @ Box(
key=f"box-{event.value['id']}",
position=event.value['position'],
)
Conditional Updates¶
async with scene_store.subscribe(sess):
await scene_store.set_scene(children=[Box(key="target")])
while True:
if scene_store.has_node("target"):
node = scene_store.get_node("target")
current_pos = node.properties.get("position", [0, 0, 0])
new_pos = [current_pos[0] + 0.1, current_pos[1], current_pos[2]]
await scene_store.update @ {"key": "target", "position": new_pos}
await asyncio.sleep(0.05)
Cleanup Pattern¶
The context manager ensures sessions are properly unsubscribed even if errors occur:
async with scene_store.subscribe(sess):
try:
await scene_store.set_scene(...)
while True:
await process_updates()
except ConnectionError:
# Session disconnected - context manager handles cleanup
pass
# Subscriber automatically removed here