fix(viewer): preserve operator camera across AI layers
This commit is contained in:
+134
-24
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user