feat(ui): refine live observation workspace

This commit is contained in:
DCCONSTRUCTIONS 2026-07-17 01:04:48 +03:00
parent 2bda1986bd
commit aa2df560b7
11 changed files with 319 additions and 143 deletions

View File

@ -137,6 +137,14 @@ export default function App() {
runtime.state?.observationSources ?? [], runtime.state?.observationSources ?? [],
runtime.setObservationSourceActive, runtime.setObservationSourceActive,
); );
const focusedObservationSource = runtime.state?.observationSources?.find(
(source) => source.id === observationLayout.focusedSourceId,
);
const observationFullscreenActive = Boolean(
activeDefinition?.kind === "spatial"
? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud"
: activeDefinition?.kind === "cameras" && focusedObservationSource,
);
useEffect(() => { useEffect(() => {
const remote = runtime.state?.viewerSettings; const remote = runtime.state?.viewerSettings;
@ -290,6 +298,7 @@ export default function App() {
<ApplicationShell <ApplicationShell
data-nodedc-ui data-nodedc-ui
className="control-station" className="control-station"
data-observation-fullscreen={observationFullscreenActive ? "true" : undefined}
navigationOpen={workspace.navigationOpen && activeRoot !== null} navigationOpen={workspace.navigationOpen && activeRoot !== null}
contentOpen={workspace.contentOpen && activeDefinition !== null} contentOpen={workspace.contentOpen && activeDefinition !== null}
contentExpanded={workspace.contentExpanded} contentExpanded={workspace.contentExpanded}

View File

@ -1,22 +1,56 @@
import type { RefObject } from "react"; import { useLayoutEffect, useState, type RefObject } from "react";
import { WorkspaceWindow } from "@nodedc/ui-react"; import { WorkspaceWindow } from "@nodedc/ui-react";
import type { ObservationWindowRect } from "../core/observation/useObservationLayout"; import type { ObservationWindowRect } from "../core/observation/useObservationLayout";
import type { ObservationSourceDescriptor } from "../core/runtime/contracts"; import type { ObservationSourceDescriptor } from "../core/runtime/contracts";
import { ObservationMedia, observationSourceStatusLabel } from "./ObservationSources"; import { ObservationMedia, observationSourceStatusLabel } from "./ObservationSources";
export function initialObservationWindowRect(index: number): ObservationWindowRect { const WINDOW_WIDTH = 336;
const WINDOW_HEIGHT = 210;
const WINDOW_GAP = 16;
const WINDOW_INSET = 18;
const TIMELINE_CLEARANCE = 64;
export function initialObservationWindowRect(
index: number,
count = 1,
bounds: { width: number; height: number } = { width: 1280, height: 720 },
): ObservationWindowRect {
const availableWidth = Math.max(0, bounds.width - WINDOW_INSET * 2);
const width = Math.max(1, Math.min(WINDOW_WIDTH, availableWidth || WINDOW_WIDTH));
const availableHeight = Math.max(0, bounds.height - WINDOW_INSET - TIMELINE_CLEARANCE);
const maxColumns = Math.max(
1,
Math.floor((availableWidth + WINDOW_GAP) / (width + WINDOW_GAP)),
);
const columns = Math.max(1, Math.min(Math.max(1, count), maxColumns));
const rows = Math.max(1, Math.ceil(Math.max(1, count) / columns));
const rowHeight = Math.max(
1,
(availableHeight - Math.max(0, rows - 1) * WINDOW_GAP) / rows,
);
const height = Math.max(1, Math.min(WINDOW_HEIGHT, rowHeight));
const row = Math.floor(index / columns);
const column = index % columns;
const rowItemCount = Math.min(columns, Math.max(1, count - row * columns));
const rowWidth = rowItemCount * width + Math.max(0, rowItemCount - 1) * WINDOW_GAP;
const rowStart = Math.max(0, bounds.width - WINDOW_INSET - rowWidth);
return { return {
x: 18 + (index % 3) * 354, x: rowStart + Math.min(column, rowItemCount - 1) * (width + WINDOW_GAP),
y: 78 + Math.floor(index / 3) * 226, y: Math.max(
width: 336, 0,
height: 210, bounds.height - TIMELINE_CLEARANCE - height - row * (height + WINDOW_GAP),
),
width,
height,
}; };
} }
export function FloatingObservationWindow({ export function FloatingObservationWindow({
source, source,
index, index,
count,
boundsRef, boundsRef,
rect, rect,
maximized, maximized,
@ -28,6 +62,7 @@ export function FloatingObservationWindow({
}: { }: {
source: ObservationSourceDescriptor; source: ObservationSourceDescriptor;
index: number; index: number;
count: number;
boundsRef: RefObject<HTMLElement | null>; boundsRef: RefObject<HTMLElement | null>;
rect?: ObservationWindowRect; rect?: ObservationWindowRect;
maximized: boolean; maximized: boolean;
@ -37,10 +72,33 @@ export function FloatingObservationWindow({
onActivate: () => void; onActivate: () => void;
onClose: () => void; onClose: () => void;
}) { }) {
const [bounds, setBounds] = useState<{ width: number; height: number } | null>(null);
useLayoutEffect(() => {
const element = boundsRef.current;
if (!element) return;
const measure = () => {
const next = { width: element.clientWidth, height: element.clientHeight };
setBounds((current) => (
current?.width === next.width && current.height === next.height ? current : next
));
};
measure();
const observer = new ResizeObserver(measure);
observer.observe(element);
return () => observer.disconnect();
}, [boundsRef]);
const initialRect = bounds ? initialObservationWindowRect(index, count, bounds) : null;
const windowRect = rect ?? initialRect;
if (!windowRect) return null;
return ( return (
<WorkspaceWindow <WorkspaceWindow
boundsRef={boundsRef} boundsRef={boundsRef}
rect={rect ?? initialObservationWindowRect(index)} rect={windowRect}
onRectChange={onRectChange} onRectChange={onRectChange}
maximized={maximized} maximized={maximized}
onMaximizedChange={onMaximizedChange} onMaximizedChange={onMaximizedChange}
@ -60,8 +118,8 @@ export function FloatingObservationWindow({
<span>{source.capabilities.timelineMode === "live-only" ? "Эфир без буфера" : "Временная шкала"}</span> <span>{source.capabilities.timelineMode === "live-only" ? "Эфир без буфера" : "Временная шкала"}</span>
</span> </span>
)} )}
minWidth={280} minWidth={Math.min(280, windowRect.width)}
minHeight={190} minHeight={Math.min(190, windowRect.height)}
resizable={source.capabilities.resizable} resizable={source.capabilities.resizable}
active={active} active={active}
zIndex={maximized ? 15 : active ? 9 : 7} zIndex={maximized ? 15 : active ? 9 : 7}

View File

@ -15,6 +15,7 @@ body,
body { body {
margin: 0; margin: 0;
font-family: var(--nodedc-font-family);
} }
#root[data-nodedc-theme="dark"], #root[data-nodedc-theme="dark"],
@ -331,7 +332,6 @@ body {
display: grid; display: grid;
grid-template-columns: minmax(12rem, 0.9fr) minmax(0, 1.1fr); grid-template-columns: minmax(12rem, 0.9fr) minmax(0, 1.1fr);
gap: 1rem; gap: 1rem;
border-top: 1px solid var(--station-hairline);
padding: 0.7rem 0; padding: 0.7rem 0;
} }

View File

@ -20,7 +20,6 @@
display: grid; display: grid;
min-width: 0; min-width: 0;
gap: 0.9rem; gap: 0.9rem;
border: 1px solid var(--station-hairline);
border-radius: 1rem; border-radius: 1rem;
background: rgb(255 255 255 / 0.025); background: rgb(255 255 255 / 0.025);
padding: 1rem; padding: 1rem;
@ -74,8 +73,6 @@
.device-model-card dl { .device-model-card dl {
display: grid; display: grid;
gap: 0.42rem; gap: 0.42rem;
border-top: 1px solid var(--station-hairline);
border-bottom: 1px solid var(--station-hairline);
padding: 0.7rem 0; padding: 0.7rem 0;
} }
@ -441,7 +438,6 @@
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
margin-top: 1rem; margin-top: 1rem;
border-top: 1px solid var(--station-hairline);
padding-top: 0.9rem; padding-top: 0.9rem;
} }
@ -469,7 +465,6 @@
min-width: 0; min-width: 0;
grid-template-columns: minmax(7rem, 0.72fr) minmax(0, 1.28fr); grid-template-columns: minmax(7rem, 0.72fr) minmax(0, 1.28fr);
gap: 1rem; gap: 1rem;
border-top: 1px solid var(--station-hairline);
padding: 0.7rem 0; padding: 0.7rem 0;
} }

View File

@ -69,6 +69,7 @@
.observation-source-menu { .observation-source-menu {
overflow: hidden; overflow: hidden;
font-family: var(--nodedc-font-family);
} }
.observation-source-menu__head, .observation-source-menu__head,
@ -81,7 +82,6 @@
align-items: flex-end; align-items: flex-end;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
border-bottom: 1px solid var(--station-hairline);
} }
.observation-source-menu__head strong, .observation-source-menu__head strong,
@ -92,13 +92,13 @@
.observation-source-menu__head strong { .observation-source-menu__head strong {
margin-top: 0.34rem; margin-top: 0.34rem;
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
font-size: 0.76rem; font-size: var(--nodedc-font-size-md);
} }
.observation-source-menu__head small, .observation-source-menu__head small,
.observation-source-menu__foot { .observation-source-menu__foot {
color: var(--nodedc-text-muted); color: var(--nodedc-text-muted);
font-size: 0.55rem; font-size: var(--nodedc-font-size-xs);
line-height: 1.4; line-height: 1.4;
} }
@ -176,7 +176,7 @@ i[data-availability="error"] {
} }
.observation-source-menu__foot { .observation-source-menu__foot {
border-top: 1px solid var(--station-hairline); border-top: 0;
} }
.observation-media__asset, .observation-media__asset,
@ -307,13 +307,21 @@ i[data-availability="error"] {
background: #06070a; background: #06070a;
} }
.floating-observation-window.nodedc-material-rim::after {
display: none;
}
.floating-observation-window[data-active="true"] {
box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 0.34);
}
.observation-timeline { .observation-timeline {
display: grid; display: grid;
min-width: 0; min-width: 0;
grid-template-columns: auto auto minmax(0, 1fr) auto auto; grid-template-columns: auto auto minmax(0, 1fr) auto auto;
align-items: center; align-items: center;
gap: 0.6rem; gap: 0.6rem;
border: 1px solid rgb(255 255 255 / 0.055); border: 0;
border-radius: 0.9rem; border-radius: 0.9rem;
background: rgb(9 10 13 / 0.78); background: rgb(9 10 13 / 0.78);
padding: 0.4rem; padding: 0.4rem;
@ -432,7 +440,7 @@ i[data-availability="error"] {
min-height: 18rem; min-height: 18rem;
border-radius: 1rem; border-radius: 1rem;
background: #07080a; background: #07080a;
box-shadow: inset 0 0 0 1px var(--station-hairline); box-shadow: none;
} }
.camera-slot { .camera-slot {

View File

@ -29,6 +29,36 @@
background: rgb(var(--nodedc-danger-rgb)); background: rgb(var(--nodedc-danger-rgb));
} }
.control-station[data-observation-fullscreen="true"] .nodedc-app-shell__content {
z-index: calc(var(--nodedc-layer-panel) + 1);
right: var(--nodedc-app-page-pad);
left: var(--nodedc-app-page-pad);
width: auto;
transition: none;
}
.control-station[data-observation-fullscreen="true"] .nodedc-app-shell__navigation {
pointer-events: none;
}
.control-station[data-observation-fullscreen="true"] .nodedc-application-panel {
grid-template-rows: minmax(0, 1fr);
gap: 0;
background: #06070a;
padding: 0;
box-shadow: none;
animation: none;
}
.control-station[data-observation-fullscreen="true"] .nodedc-application-panel__head {
display: none;
}
.control-station[data-observation-fullscreen="true"] .nodedc-application-panel__body {
height: 100%;
overflow: hidden;
}
.landing-stage { .landing-stage {
position: relative; position: relative;
min-width: 0; min-width: 0;
@ -79,7 +109,6 @@
display: grid; display: grid;
width: min(24rem, 30vw); width: min(24rem, 30vw);
gap: 0.6rem; gap: 0.6rem;
border: 1px solid rgb(255 255 255 / 0.055);
border-radius: 1.25rem; border-radius: 1.25rem;
background: rgb(10 10 12 / 0.58); background: rgb(10 10 12 / 0.58);
padding: 1rem; padding: 1rem;

View File

@ -32,7 +32,7 @@
overflow: hidden; overflow: hidden;
border-radius: 1rem; border-radius: 1rem;
background: #06070a; background: #06070a;
box-shadow: inset 0 0 0 1px var(--station-hairline); box-shadow: none;
} }
.rerun-viewport, .rerun-viewport,
@ -45,6 +45,19 @@
min-height: 0; min-height: 0;
} }
.rerun-viewport {
--rerun-tab-bar-height: 24px;
overflow: hidden;
}
/* Rerun WebViewer 0.34.1 draws its selected recording tab inside WASM.
The native panels are already disabled; crop only that fixed tab row. */
.rerun-viewport__canvas {
top: calc(-1 * var(--rerun-tab-bar-height));
bottom: auto;
height: calc(100% + var(--rerun-tab-bar-height));
}
.rerun-viewport__canvas canvas { .rerun-viewport__canvas canvas {
display: block; display: block;
width: 100% !important; width: 100% !important;
@ -63,7 +76,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.8rem; gap: 0.8rem;
border: 1px solid var(--station-hairline); border: 0;
border-radius: 1rem; border-radius: 1rem;
background: rgb(10 11 14 / 0.82); background: rgb(10 11 14 / 0.82);
padding: 0.9rem 1rem; padding: 0.9rem 1rem;
@ -179,7 +192,7 @@
width: 3.4rem; width: 3.4rem;
height: 3.4rem; height: 3.4rem;
place-items: center; place-items: center;
border: 1px solid var(--station-hairline-strong); border: 0;
border-radius: 50%; border-radius: 50%;
background: rgb(255 255 255 / 0.035); background: rgb(255 255 255 / 0.035);
color: var(--nodedc-text-secondary); color: var(--nodedc-text-secondary);
@ -204,7 +217,7 @@
.scene-timeline { .scene-timeline {
position: absolute; position: absolute;
z-index: 3; z-index: 3;
border: 1px solid rgb(255 255 255 / 0.055); border: 0;
background: rgb(9 10 13 / 0.74); background: rgb(9 10 13 / 0.74);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
} }
@ -240,7 +253,7 @@
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
border-bottom: 1px solid var(--station-hairline); border-bottom: 0;
padding: 0.38rem 0; padding: 0.38rem 0;
} }

View File

@ -16,7 +16,6 @@
min-width: 0; min-width: 0;
grid-template-columns: minmax(14rem, 0.72fr) minmax(0, 1.6fr); grid-template-columns: minmax(14rem, 0.72fr) minmax(0, 1.6fr);
gap: 1.6rem; gap: 1.6rem;
border-top: 1px solid var(--station-hairline);
padding-top: 1.2rem; padding-top: 1.2rem;
} }
@ -47,7 +46,6 @@
grid-template-columns: auto minmax(0, 1fr) auto; grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: 0.8rem; gap: 0.8rem;
border-bottom: 1px solid var(--station-hairline);
padding: 0.78rem 0; padding: 0.78rem 0;
} }
@ -270,7 +268,7 @@
overflow: hidden; overflow: hidden;
border-radius: 1rem; border-radius: 1rem;
background: #090a0c; background: #090a0c;
box-shadow: inset 0 0 0 1px var(--station-hairline); box-shadow: none;
} }
.camera-slot[data-primary="true"] { .camera-slot[data-primary="true"] {
@ -347,7 +345,7 @@
overflow: hidden; overflow: hidden;
border-radius: 1rem; border-radius: 1rem;
background: #0a0b0d; background: #0a0b0d;
box-shadow: inset 0 0 0 1px var(--station-hairline); box-shadow: none;
} }
.map-canvas__grid, .map-canvas__grid,
@ -484,7 +482,6 @@
} }
.timeline-ruler span { .timeline-ruler span {
border-left: 1px solid var(--station-hairline);
padding-left: 0.2rem; padding-left: 0.2rem;
} }
@ -498,7 +495,6 @@
grid-template-columns: 7.5rem minmax(0, 1fr) 10rem; grid-template-columns: 7.5rem minmax(0, 1fr) 10rem;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
border-top: 1px solid var(--station-hairline);
padding: 0.82rem 0; padding: 0.82rem 0;
} }

View File

@ -301,7 +301,10 @@ function SpatialWorkspace({
const viewerStatusTone = viewerStatus === "ready" ? "success" : viewerStatus === "error" ? "danger" : "neutral"; const viewerStatusTone = viewerStatus === "ready" ? "success" : viewerStatus === "error" ? "danger" : "neutral";
return ( return (
<div className="spatial-workspace" data-focused={pointCloudFocused ? "true" : undefined}> <div
className="spatial-workspace"
data-focused={pointCloudFocused || floatingSourceMaximized ? "true" : undefined}
>
<div className="spatial-toolbar"> <div className="spatial-toolbar">
<div className="spatial-toolbar__mode"> <div className="spatial-toolbar__mode">
<span className="section-eyebrow">СЦЕНА 3D · RERUN</span> <span className="section-eyebrow">СЦЕНА 3D · RERUN</span>
@ -428,6 +431,7 @@ function SpatialWorkspace({
key={source.id} key={source.id}
source={source} source={source}
index={index} index={index}
count={visibleMediaSources.length}
boundsRef={viewportRef} boundsRef={viewportRef}
rect={observationLayout.windowRects[source.id]} rect={observationLayout.windowRects[source.id]}
maximized={observationLayout.maximizedFloatingSourceId === source.id} maximized={observationLayout.maximizedFloatingSourceId === source.id}
@ -436,7 +440,11 @@ function SpatialWorkspace({
onMaximizedChange={(maximized) => onMaximizedChange={(maximized) =>
observationLayout.setFloatingMaximized(source.id, maximized)} observationLayout.setFloatingMaximized(source.id, maximized)}
onActivate={() => observationLayout.activateFloatingSource(source.id)} onActivate={() => observationLayout.activateFloatingSource(source.id)}
onClose={() => void observationLayout.hideSource(source.id)} onClose={() => {
if (observationLayout.pendingSourceIds.has(source.id)) return;
observationLayout.setFloatingMaximized(source.id, false);
void observationLayout.hideSource(source.id);
}}
/> />
)) : null} )) : null}
</div> </div>
@ -461,6 +469,7 @@ function CameraSourceCard({
selectedPeer, selectedPeer,
onToggle, onToggle,
onFocus, onFocus,
onClose,
}: { }: {
source: ObservationSourceDescriptor; source: ObservationSourceDescriptor;
focused: boolean; focused: boolean;
@ -469,6 +478,7 @@ function CameraSourceCard({
selectedPeer: boolean; selectedPeer: boolean;
onToggle: () => void; onToggle: () => void;
onFocus: () => void; onFocus: () => void;
onClose: () => void;
}) { }) {
const deliveryActive = Boolean( const deliveryActive = Boolean(
(source.delivery || source.previewUrl) && (source.delivery || source.previewUrl) &&
@ -500,7 +510,7 @@ function CameraSourceCard({
<i data-availability={source.availability} aria-hidden="true" /> <i data-availability={source.availability} aria-hidden="true" />
{observationSourceStatusLabel(source)} {observationSourceStatusLabel(source)}
</span> </span>
{source.activation ? ( {source.activation && !focused ? (
<button <button
type="button" type="button"
className="camera-slot__activation" className="camera-slot__activation"
@ -510,7 +520,7 @@ function CameraSourceCard({
{activationLabel} {activationLabel}
</button> </button>
) : null} ) : null}
{source.capabilities.fullscreen && deliveryActive ? ( {source.capabilities.fullscreen && (focused || deliveryActive) ? (
<button <button
type="button" type="button"
aria-label={focused ? `Свернуть ${source.label}` : `Развернуть ${source.label}`} aria-label={focused ? `Свернуть ${source.label}` : `Развернуть ${source.label}`}
@ -519,6 +529,16 @@ function CameraSourceCard({
<Icon name={focused ? "minimize" : "expand"} size={15} /> <Icon name={focused ? "minimize" : "expand"} size={15} />
</button> </button>
) : null} ) : null}
{focused ? (
<button
type="button"
aria-label={`Закрыть ${source.label}`}
disabled={pending}
onClick={onClose}
>
<Icon name="close" size={15} />
</button>
) : null}
</div> </div>
</header> </header>
<div className="camera-slot__body"> <div className="camera-slot__body">
@ -563,6 +583,10 @@ function CamerasWorkspace({ definition, state, observationLayout }: WorkspaceRen
onFocus={() => observationLayout.setFocusedSourceId( onFocus={() => observationLayout.setFocusedSourceId(
focusedSource?.id === source.id ? null : source.id, focusedSource?.id === source.id ? null : source.id,
)} )}
onClose={() => {
observationLayout.setFocusedSourceId(null);
void observationLayout.hideSource(source.id);
}}
/> />
)) : ( )) : (
<div className="camera-grid__empty"> <div className="camera-grid__empty">

View File

@ -10,6 +10,7 @@ let openObservationSource;
let shouldRestartObservationSource; let shouldRestartObservationSource;
let consumeCameraLeaseRetry; let consumeCameraLeaseRetry;
let resetCameraLeaseRetryBudget; let resetCameraLeaseRetryBudget;
let initialObservationWindowRect;
before(async () => { before(async () => {
server = await createServer({ server = await createServer({
@ -29,6 +30,49 @@ before(async () => {
({ consumeCameraLeaseRetry, resetCameraLeaseRetryBudget } = await server.ssrLoadModule( ({ consumeCameraLeaseRetry, resetCameraLeaseRetryBudget } = await server.ssrLoadModule(
"/src/components/MseFmp4WebSocketPlayer.tsx", "/src/components/MseFmp4WebSocketPlayer.tsx",
)); ));
({ initialObservationWindowRect } = await server.ssrLoadModule(
"/src/components/FloatingObservationWindow.tsx",
));
});
test("observation camera windows tile from the bottom-right above the live timeline", () => {
const bounds = { width: 1280, height: 720 };
const left = initialObservationWindowRect(0, 2, bounds);
const right = initialObservationWindowRect(1, 2, bounds);
assert.equal(left.y, right.y);
assert.ok(left.x + left.width < right.x);
assert.equal(right.x + right.width, bounds.width - 18);
assert.ok(right.y + right.height <= bounds.height - 64);
});
test("observation camera windows stack without overlap when the viewport is narrow", () => {
const bounds = { width: 420, height: 700 };
const lower = initialObservationWindowRect(0, 2, bounds);
const upper = initialObservationWindowRect(1, 2, bounds);
assert.equal(lower.x, upper.x);
assert.ok(upper.y + upper.height < lower.y);
for (const rect of [lower, upper]) {
assert.ok(rect.x >= 0 && rect.y >= 0);
assert.ok(rect.x + rect.width <= bounds.width);
assert.ok(rect.y + rect.height <= bounds.height - 64);
}
});
test("observation camera tiling remains in bounds on a constrained viewport", () => {
const bounds = { width: 260, height: 280 };
const rects = Array.from({ length: 2 }, (_, index) => (
initialObservationWindowRect(index, 2, bounds)
));
for (const rect of rects) {
assert.ok(rect.width > 0 && rect.height > 0);
assert.ok(rect.x >= 0 && rect.y >= 0);
assert.ok(rect.x + rect.width <= bounds.width);
assert.ok(rect.y + rect.height <= bounds.height - 64);
}
assert.ok(rects[1].y + rects[1].height <= rects[0].y);
}); });
after(async () => { after(async () => {

View File

@ -102,7 +102,7 @@ class RerunBridge:
recording.log("/world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True) recording.log("/world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True)
recording.log( recording.log(
"/world/sensor_pose", "/world/sensor_pose",
rr.TransformAxes3D(axis_length=0.45, show_frame=True), rr.TransformAxes3D(axis_length=0.45, show_frame=False),
static=True, static=True,
) )
# Do not expose a URL whose StoreInfo, blueprint and static scene are # Do not expose a URL whose StoreInfo, blueprint and static scene are
@ -142,7 +142,7 @@ class RerunBridge:
self._recording.log("/world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True) self._recording.log("/world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True)
self._recording.log( self._recording.log(
"/world/sensor_pose", "/world/sensor_pose",
rr.TransformAxes3D(axis_length=0.45, show_frame=True), rr.TransformAxes3D(axis_length=0.45, show_frame=False),
static=True, static=True,
) )
self._recording.send_blueprint( self._recording.send_blueprint(