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 |
|---|---|---|
snapshot |
SceneState |
Returns a deep copy of the current scene state |
upsert |
_UpsertProxy |
Proxy for upsert operations with @ operator |
add |
_AddProxy |
Proxy for add operations with @ operator |
update |
_UpdateProxy |
Proxy for update operations with @ operator |
remove |
_RemoveProxy |
Proxy for remove operations with @ operator |
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