fix(viewer): preserve operator camera across AI layers

This commit is contained in:
DCCONSTRUCTIONS
2026-07-23 10:51:18 +03:00
parent a0706fd5d8
commit 3a64f54dea
12 changed files with 425 additions and 109 deletions
+134 -24
View File
@@ -2,11 +2,14 @@
from __future__ import annotations
from collections import OrderedDict
from contextlib import suppress
from threading import Lock
from typing import Any, Literal
from uuid import UUID
import rerun as rr
import rerun_bindings as bindings
from rerun import blueprint as rrb
from k1link.viewer.rerun_bridge import RerunSceneSettings, _parse_hex_color
@@ -40,6 +43,97 @@ class RecordedBlueprintError(RuntimeError):
"""A viewer blueprint update could not be serialized safely."""
class _RecordedBlueprintStream:
"""Keep one browser viewport on one mutable Rerun blueprint store.
Rerun persists the operator-controlled eye inside the active blueprint
store. Creating and activating a fresh store for every layer toggle drops
that eye and snaps the view back to its fallback camera. This stream keeps
the store identity stable until the operator explicitly requests a reset.
"""
def __init__(
self,
application_id: str,
*,
view_reset_generation: Literal[0, 1],
) -> None:
self.view_reset_generation = view_reset_generation
self._lock = Lock()
self._pending: list[bytes] = []
self._initial_payload: bytes | None = None
self._latest_payload = b""
native = bindings.new_blueprint(
application_id=application_id,
make_default=False,
make_thread_default=False,
default_enabled=True,
)
self._recording = rr.RecordingStream._from_native(native)
bindings.set_callback_sink_blueprint(
lambda chunk: self._pending.append(bytes(chunk)),
True,
False,
native,
)
self._recording.set_time("blueprint", sequence=0)
def render(self, blueprint: rrb.Blueprint) -> bytes:
with self._lock:
if self._initial_payload is not None:
self._pending.clear()
blueprint._log_to_stream(self._recording)
self._recording.flush(timeout_sec=5.0)
delta = b"".join(self._pending)
if not delta:
raise RecordedBlueprintError("stable blueprint stream produced no data")
if self._initial_payload is None:
self._initial_payload = delta
self._latest_payload = b""
else:
self._latest_payload = delta
return self._initial_payload + self._latest_payload
def close(self) -> None:
with suppress(Exception):
self._recording.disconnect()
_MAX_RECORDED_BLUEPRINT_STREAMS = 32
_recorded_blueprint_streams_lock = Lock()
_recorded_blueprint_streams: OrderedDict[
tuple[str, str, str], _RecordedBlueprintStream
] = OrderedDict()
def _stable_recorded_blueprint_stream(
*,
application_id: str,
recording_id: str,
blueprint_session_id: str,
view_reset_generation: Literal[0, 1],
) -> _RecordedBlueprintStream:
key = (application_id, recording_id, blueprint_session_id)
with _recorded_blueprint_streams_lock:
stream = _recorded_blueprint_streams.get(key)
if stream is not None and stream.view_reset_generation != view_reset_generation:
stream.close()
del _recorded_blueprint_streams[key]
stream = None
if stream is None:
stream = _RecordedBlueprintStream(
application_id,
view_reset_generation=view_reset_generation,
)
_recorded_blueprint_streams[key] = stream
else:
_recorded_blueprint_streams.move_to_end(key)
while len(_recorded_blueprint_streams) > _MAX_RECORDED_BLUEPRINT_STREAMS:
_, stale = _recorded_blueprint_streams.popitem(last=False)
stale.close()
return stream
def recorded_blueprint(
settings: RerunSceneSettings,
*,
@@ -254,6 +348,7 @@ def recorded_blueprint_rrd(
*,
application_id: str = APPLICATION_ID,
recording_id: str,
blueprint_session_id: str | None = None,
active_view: RecordedView = "spatial",
view_reset_generation: Literal[0, 1] = 0,
unified_perception: bool = False,
@@ -263,33 +358,48 @@ def recorded_blueprint_rrd(
) -> bytes:
"""Serialize a bounded active blueprint update without recorded data."""
recording = rr.RecordingStream(
application_id,
recording_id=recording_id,
send_properties=False,
blueprint = recorded_blueprint(
settings,
include_initial_playback_state=False,
active_view=active_view,
view_reset_generation=view_reset_generation,
unified_perception=unified_perception,
show_detections_2d=show_detections_2d,
show_segmentation=show_segmentation,
show_cuboids_3d=show_cuboids_3d,
)
stream = rr.binary_stream(recording)
try:
recording.send_blueprint(
recorded_blueprint(
settings,
include_initial_playback_state=False,
active_view=active_view,
payload: bytes | None
if blueprint_session_id is not None:
try:
payload = _stable_recorded_blueprint_stream(
application_id=application_id,
recording_id=recording_id,
blueprint_session_id=blueprint_session_id,
view_reset_generation=view_reset_generation,
unified_perception=unified_perception,
show_detections_2d=show_detections_2d,
show_segmentation=show_segmentation,
show_cuboids_3d=show_cuboids_3d,
),
make_active=True,
make_default=False,
).render(blueprint)
except RecordedBlueprintError:
raise
except Exception as exc:
raise RecordedBlueprintError("failed to serialize stable recorded blueprint") from exc
else:
recording = rr.RecordingStream(
application_id,
recording_id=recording_id,
send_properties=False,
)
payload = stream.read(flush=True, flush_timeout_sec=5.0)
except Exception as exc:
raise RecordedBlueprintError("failed to serialize recorded blueprint") from exc
finally:
with suppress(Exception):
recording.disconnect()
stream = rr.binary_stream(recording)
try:
recording.send_blueprint(
blueprint,
make_active=True,
make_default=False,
)
payload = stream.read(flush=True, flush_timeout_sec=5.0)
except Exception as exc:
raise RecordedBlueprintError("failed to serialize recorded blueprint") from exc
finally:
with suppress(Exception):
recording.disconnect()
if not payload or not payload.startswith(b"RRF2") or len(payload) > 1_048_576:
raise RecordedBlueprintError("serialized recorded blueprint is invalid")
return payload
+6
View File
@@ -98,6 +98,11 @@ class RecordedBlueprintRequest(StrictApiModel):
max_length=128,
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
)
blueprint_session_id: str = Field(
min_length=32,
max_length=32,
pattern=r"^[a-f0-9]{32}$",
)
accumulation_seconds: float = Field(strict=True, ge=0.0, le=3600.0)
show_points: StrictBool
show_trajectory: StrictBool
@@ -800,6 +805,7 @@ def build_session_router(
),
application_id=RECORDED_APPLICATION_ID,
recording_id=request.recording_id,
blueprint_session_id=request.blueprint_session_id,
active_view=request.active_view,
view_reset_generation=request.view_reset_generation,
unified_perception=request.unified_perception,