feat(observatory): share native result replay and expose saved TGS layers

This commit is contained in:
DCCONSTRUCTIONS
2026-09-03 13:23:56 +03:00
parent 27979854a7
commit 9db29bcdc6
15 changed files with 594 additions and 433 deletions
@@ -166,6 +166,12 @@ interface RecordedRerunIdentity {
const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/; const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
const LAB_RECORDED_REPLAY_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-replay\.rrd$/; const LAB_RECORDED_REPLAY_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-replay\.rrd$/;
const PORTABLE_RECORDED_REPLAY_PATH = /^\/api\/v1\/observatory\/portable-results\/m49-tgs-portable-review-[a-f0-9]{64}\/replays\/[a-f0-9]{64}\/recording\.rrd$/;
export function isRecordedRrdSource(path: string): boolean {
return RECORDED_RRD_PATH.test(path) || LAB_RECORDED_REPLAY_PATH.test(path)
|| PORTABLE_RECORDED_REPLAY_PATH.test(path);
}
const RECORDED_BLUEPRINT_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/blueprint\.rrd$/; const RECORDED_BLUEPRINT_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/blueprint\.rrd$/;
const RECORDED_PERCEPTION_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/perception\.rrd$/; const RECORDED_PERCEPTION_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/perception\.rrd$/;
const LAB_RECORDED_PERCEPTION_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-overlay\.rrd$/; const LAB_RECORDED_PERCEPTION_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-overlay\.rrd$/;
@@ -230,8 +236,7 @@ export function resolveRecordedViewerSourceUrl(
const expectedViewerSourceUrl = `${descriptor.sourceUrl}?generation=${descriptor.sha256}`; const expectedViewerSourceUrl = `${descriptor.sourceUrl}?generation=${descriptor.sha256}`;
const endpoint = new URL(descriptor.viewerSourceUrl, `${base.origin}/`); const endpoint = new URL(descriptor.viewerSourceUrl, `${base.origin}/`);
if ( if (
!(RECORDED_RRD_PATH.test(descriptor.sourceUrl) !isRecordedRrdSource(descriptor.sourceUrl) ||
|| LAB_RECORDED_REPLAY_PATH.test(descriptor.sourceUrl)) ||
descriptor.viewerSourceUrl !== expectedViewerSourceUrl || descriptor.viewerSourceUrl !== expectedViewerSourceUrl ||
endpoint.origin !== base.origin || endpoint.origin !== base.origin ||
endpoint.pathname !== descriptor.sourceUrl || endpoint.pathname !== descriptor.sourceUrl ||
@@ -262,7 +267,7 @@ export function resolveRecordedBlueprintUrl(
if (explicitSourceUrl !== undefined) { if (explicitSourceUrl !== undefined) {
const explicit = explicitSourceUrl.trim(); const explicit = explicitSourceUrl.trim();
if ( if (
!LAB_RECORDED_REPLAY_PATH.test(normalized) !(LAB_RECORDED_REPLAY_PATH.test(normalized) || PORTABLE_RECORDED_REPLAY_PATH.test(normalized))
|| !RECORDED_BLUEPRINT_PATH.test(explicit) || !RECORDED_BLUEPRINT_PATH.test(explicit)
) return null; ) return null;
const endpoint = new URL(explicit, `${base.origin}/`); const endpoint = new URL(explicit, `${base.origin}/`);
@@ -438,6 +443,7 @@ export async function fetchRecordedBlueprintRrd(
perceptionLayers.detections2d, perceptionLayers.detections2d,
perceptionLayers.segmentation, perceptionLayers.segmentation,
perceptionLayers.cuboids3d, perceptionLayers.cuboids3d,
perceptionLayers.costmap ?? false,
].some((value) => typeof value !== "boolean") || ].some((value) => typeof value !== "boolean") ||
identity.applicationId !== "nodedc_mission_core_recorded" || identity.applicationId !== "nodedc_mission_core_recorded" ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId) || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId) ||
@@ -473,6 +479,10 @@ export async function fetchRecordedBlueprintRrd(
show_detections_2d: perceptionLayers.detections2d, show_detections_2d: perceptionLayers.detections2d,
show_segmentation: perceptionLayers.segmentation, show_segmentation: perceptionLayers.segmentation,
show_cuboids_3d: perceptionLayers.cuboids3d, show_cuboids_3d: perceptionLayers.cuboids3d,
...(perceptionLayers.costmap === undefined ? {} : {
show_costmap: perceptionLayers.costmap,
reactivate_updates: true,
}),
}), }),
signal, signal,
}); });
@@ -791,8 +801,7 @@ export function RerunViewport({
return; return;
} }
const isRecordedSource = RECORDED_RRD_PATH.test(normalizedSource) const isRecordedSource = isRecordedRrdSource(normalizedSource);
|| LAB_RECORDED_REPLAY_PATH.test(normalizedSource);
let resolvedSource: string; let resolvedSource: string;
try { try {
if (isRecordedSource) { if (isRecordedSource) {
@@ -1938,6 +1947,9 @@ export function RerunViewport({
useEffect(() => { useEffect(() => {
if (!recordedBlueprintUrl || !sceneSettings) return; if (!recordedBlueprintUrl || !sceneSettings) return;
// Initialize the portable following-eye blueprint after native admission
// and its initial seek, not while the base RRD is still arriving.
if (recordedPerceptionLayers.costmap !== undefined && status !== "ready") return;
const active = blueprintChannelRef.current; const active = blueprintChannelRef.current;
const identity = recordedIdentityRef.current; const identity = recordedIdentityRef.current;
if ( if (
@@ -1986,6 +1998,8 @@ export function RerunViewport({
recordedPerceptionLayers.detections2d, recordedPerceptionLayers.detections2d,
recordedPerceptionLayers.segmentation, recordedPerceptionLayers.segmentation,
recordedPerceptionLayers.cuboids3d, recordedPerceptionLayers.cuboids3d,
recordedPerceptionLayers.costmap,
status,
sceneSettings?.accumulationSeconds, sceneSettings?.accumulationSeconds,
sceneSettings?.customColor, sceneSettings?.customColor,
sceneSettings?.colorMode, sceneSettings?.colorMode,
@@ -0,0 +1,397 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type PointerEvent as ReactPointerEvent,
} from "react";
import {
ActivityIndicator,
Button,
Icon,
SegmentedControl,
} from "@nodedc/ui-react";
import { ObservationTimeline } from "../ObservationTimeline";
import {
RerunViewport,
isRecordedPlaybackPresentationReady,
type RerunPlaybackController,
type RerunPlaybackState,
type RerunViewportStatus,
} from "../RerunViewport";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../laboratory/CanonicalRecordedLabReplay";
import {
type CanonicalLabReplayDescriptor,
} from "../../core/laboratory/canonicalLabReplay";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import { defaultSceneSettings } from "../../sceneSettings";
type MediaMode = "video" | "camera";
type SpatialMode = "3d" | "plan";
type SpatialLayer = "source" | "local" | "tgs" | "semantic";
type SemanticLayer = "city" | "vegetation";
interface CanonicalReplayLaunch {
base: ObservationSessionReplayLaunch;
replay: CanonicalLabReplayDescriptor;
}
const RERUN_UNIFIED_CAMERA_SHARE_PERCENT = 46;
const RERUN_NATIVE_DIVIDER_HIT_SLOP_PX = 10;
export function CanonicalResultRerunReplay({
resultId,
sessionId,
initialPlaybackStartSeconds,
resolveReplay,
semantics = false,
costmap = false,
label = "Сохранённый результат · синхронизированный повтор",
}: {
resultId: string;
sessionId: string;
initialPlaybackStartSeconds?: number;
resolveReplay: (resultId: string, launch: ObservationSessionReplayLaunch, options: { signal: AbortSignal }) => Promise<CanonicalLabReplayDescriptor>;
semantics?: boolean;
costmap?: boolean;
label?: string;
}) {
const {
mediaMode,
spatialMode,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange,
onExpandedChange,
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
initialMediaMode: "video",
initialSpatialMode: "3d",
});
const splitView = mediaMode !== null && spatialMode !== null;
const [semanticLayer, setSemanticLayer] = useState<SemanticLayer>("vegetation");
const [showSemantics, setShowSemantics] = useState(true);
const [spatialLayer, setSpatialLayer] = useState<SpatialLayer>(costmap ? "tgs" : "source");
// Give the portable composition its own initial native view identity.
const [viewResetGeneration, setViewResetGeneration] = useState<0 | 1>(costmap ? 1 : 0);
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
const [playbackController, setPlaybackController] =
useState<RerunPlaybackController | null>(null);
const [viewerStatus, setViewerStatus] = useState<RerunViewportStatus>("idle");
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
const [launchError, setLaunchError] = useState<string | null>(null);
const viewerFrameRef = useRef<HTMLDivElement>(null);
const nativeSplitPercentRef = useRef(RERUN_UNIFIED_CAMERA_SHARE_PERCENT);
const nativeSplitTrackingCleanupRef = useRef<(() => void) | null>(null);
const previousSplitViewRef = useRef(splitView);
const trackedLaunchSha256Ref = useRef<string | null>(null);
const stopNativeSplitTracking = useCallback(() => {
nativeSplitTrackingCleanupRef.current?.();
nativeSplitTrackingCleanupRef.current = null;
}, []);
const trackNativeSplit = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
if (
!splitView
|| event.button !== 0
|| !(event.target instanceof HTMLCanvasElement)
) return;
const frame = viewerFrameRef.current;
if (!frame) return;
const bounds = frame.getBoundingClientRect();
if (bounds.width <= 0) return;
const dividerX = bounds.left
+ bounds.width * nativeSplitPercentRef.current / 100;
if (Math.abs(event.clientX - dividerX) > RERUN_NATIVE_DIVIDER_HIT_SLOP_PX) return;
stopNativeSplitTracking();
const pointerId = event.pointerId;
const update = (pointerEvent: PointerEvent) => {
if (pointerEvent.pointerId !== pointerId) return;
const currentBounds = frame.getBoundingClientRect();
if (currentBounds.width <= 0) return;
const next = Math.min(90, Math.max(
10,
(pointerEvent.clientX - currentBounds.left) / currentBounds.width * 100,
));
nativeSplitPercentRef.current = next;
frame.style.setProperty("--canonical-rerun-camera-pane", `${next}%`);
};
const stop = (pointerEvent: PointerEvent) => {
if (pointerEvent.pointerId !== pointerId) return;
stopNativeSplitTracking();
};
window.addEventListener("pointermove", update, true);
window.addEventListener("pointerup", stop, true);
window.addEventListener("pointercancel", stop, true);
nativeSplitTrackingCleanupRef.current = () => {
window.removeEventListener("pointermove", update, true);
window.removeEventListener("pointerup", stop, true);
window.removeEventListener("pointercancel", stop, true);
};
}, [splitView, stopNativeSplitTracking]);
useEffect(() => stopNativeSplitTracking, [stopNativeSplitTracking]);
useEffect(() => {
if (!splitView) stopNativeSplitTracking();
const launchSha256 = launch?.replay.sha256 ?? null;
if (
trackedLaunchSha256Ref.current !== launchSha256
|| (splitView && !previousSplitViewRef.current)
) {
nativeSplitPercentRef.current = RERUN_UNIFIED_CAMERA_SHARE_PERCENT;
}
trackedLaunchSha256Ref.current = launchSha256;
previousSplitViewRef.current = splitView;
const cameraPanePercent = mediaMode === null
? 0
: splitView
? nativeSplitPercentRef.current
: 100;
viewerFrameRef.current?.style.setProperty(
"--canonical-rerun-camera-pane",
`${cameraPanePercent}%`,
);
}, [launch?.replay.sha256, mediaMode, splitView, stopNativeSplitTracking]);
useEffect(() => {
const controller = new AbortController();
setLaunch(null);
setLaunchError(null);
setPlayback(null);
setPlaybackController(null);
setViewerStatus("idle");
void resolveObservationSessionReplay(sessionId, {
signal: controller.signal,
maximumWaitMs: 30 * 60 * 1000,
onUpdate: () => undefined,
}).then(async (value) => ({
base: value,
replay: await resolveReplay(resultId, value, {
signal: controller.signal,
}),
})).then((value) => {
if (!controller.signal.aborted) setLaunch(value);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setLaunchError(
caught instanceof Error ? caught.message : "Сохранённая запись недоступна.",
);
}
});
return () => controller.abort();
}, [resultId, sessionId, resolveReplay]);
const presentationReady = playbackController !== null
&& isRecordedPlaybackPresentationReady(viewerStatus, playback);
const presentationState = launchError || viewerStatus === "error"
? "error"
: presentationReady
? "ready"
: "loading";
const sceneSettings = useMemo(() => ({
...defaultSceneSettings,
accumulationSeconds: spatialLayer === "local" ? 5 : 0,
showPoints: spatialMode !== null,
showTrajectory: spatialMode !== null,
showGrid: spatialMode !== null,
pointSize: 3.8,
}), [spatialLayer, spatialMode]);
const profile = launch ? recordedSessionRerunProfile({
sourceUrl: launch.replay.sourceUrl,
artifact: {
sourceUrl: launch.replay.sourceUrl,
viewerSourceUrl: launch.replay.viewerSourceUrl,
byteLength: launch.replay.byteLength,
sha256: launch.replay.sha256,
},
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
// Seek just inside the first portable sample, not onto the exact latest-at
// boundary. This changes only the initial cursor, never source timestamps.
initialPlaybackStartSeconds: initialPlaybackStartSeconds
?? ((launch.base.mediaSources[0]?.timelineStartSeconds ?? launch.base.timelineStartSeconds)
+ (costmap ? 0.000001 : 0)),
view: mediaMode !== null ? "perception" : "spatial",
viewResetGeneration,
followTrajectory: true,
semanticLayer,
unifiedPerception: splitView,
planView: spatialMode === "plan",
perceptionLayers: {
enabled: mediaMode !== null,
detections2d: semantics && mediaMode === "video",
segmentation: semantics && mediaMode === "video" && showSemantics,
cuboids3d: false,
...(costmap ? { costmap: spatialMode !== null && spatialLayer === "tgs" } : {}),
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: mediaMode !== null,
}) : null;
const mediaLayerControls = semantics ? (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<Button
size="dense"
shape="pill"
variant={showSemantics ? "primary" : "secondary"}
aria-pressed={showSemantics}
onClick={() => setShowSemantics((visible) => !visible)}
>
СЕМАНТИКА
</Button>
<SegmentedControl
value={semanticLayer}
items={[
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
]}
label="Источник семантики"
size="dense"
onChange={(value) => {
setSemanticLayer(value);
setShowSemantics(true);
}}
/>
</div>
) : undefined;
const spatialLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Пространственные слои результата"
>
<SegmentedControl
value={spatialLayer}
items={[
{ value: "source", label: "ИСХ. ТОЧКИ" },
{ value: "local", label: "ЛОК. SLAM" },
{ value: "tgs", label: "TGS", disabled: !costmap },
{ value: "semantic", label: "СЕМАНТИКА", disabled: true },
]}
label="Пространственные слои"
size="dense"
onChange={setSpatialLayer}
/>
</div>
);
const resetSpatialView = (
<Button
size="dense"
variant="ghost"
icon={<Icon name="refresh" size={14} />}
aria-label="Сбросить положение 3D камеры"
title="Сбросить положение 3D камеры"
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}
>
</Button>
);
const transport = presentationReady && playback && playbackController ? (
<ObservationTimeline
className="m4-replay-threat-visual__timeline canonical-vegetation-rerun-replay__timeline"
active
sourceCount={3}
mode="recorded"
seekable
synchronization="shared-clock"
rangeNs={playback.rangeNs}
currentNs={playback.currentNs}
playing={playback.playing}
onSeek={playbackController.seek}
onPlayingChange={playbackController.setPlaying}
showJumpToEnd={false}
/>
) : undefined;
return (
<div
className="canonical-vegetation-rerun-replay"
data-presentation-state={presentationState}
aria-busy={presentationState === "loading"}
>
<CanonicalRecordedLabReplay
label={label}
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "ВИДЕО" },
{ value: "camera", label: "КАМЕРА" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "ПЛАН" },
]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео и семантика"}
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer
unifiedContent={profile ? (
<div
ref={viewerFrameRef}
className="canonical-vegetation-rerun-replay__viewport-lock"
data-split-view={splitView ? "true" : undefined}
style={{
"--canonical-rerun-camera-pane": `${
mediaMode === null
? 0
: splitView
? nativeSplitPercentRef.current
: 100
}%`,
} as CSSProperties}
onPointerDownCapture={splitView ? trackNativeSplit : undefined}
>
<RerunViewport
profile={profile}
sceneSettings={sceneSettings}
onStatusChange={setViewerStatus}
onPlaybackChange={setPlayback}
onPlaybackControllerChange={setPlaybackController}
/>
</div>
) : launchError ? (
<div className="l3-visual-audit__state" role="alert">
{launchError}
</div>
) : (
<div aria-hidden="true" />
)}
emptyMessage="Выберите ВИДЕО/КАМЕРА или 3D/ПЛАН. Общие часы Rerun останутся на месте."
deckOverlays={presentationState === "loading" ? (
<div className="canonical-vegetation-rerun-replay__loading">
<ActivityIndicator label="Загружаем синхронизированную запись" />
</div>
) : undefined}
transport={transport}
onMediaModeChange={onMediaModeChange}
onSpatialModeChange={onSpatialModeChange}
onExpandedChange={onExpandedChange}
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
/>
</div>
);
}
@@ -1,383 +1,12 @@
import { import { CanonicalResultRerunReplay } from "./CanonicalResultRerunReplay";
useCallback, import { resolveCanonicalLabReplay } from "../../core/laboratory/canonicalLabReplay";
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type PointerEvent as ReactPointerEvent,
} from "react";
import {
ActivityIndicator,
Button,
Icon,
SegmentedControl,
} from "@nodedc/ui-react";
import { ObservationTimeline } from "../ObservationTimeline";
import {
RerunViewport,
isRecordedPlaybackPresentationReady,
type RerunPlaybackController,
type RerunPlaybackState,
type RerunViewportStatus,
} from "../RerunViewport";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../laboratory/CanonicalRecordedLabReplay";
import {
resolveCanonicalLabReplay,
type CanonicalLabReplayDescriptor,
} from "../../core/laboratory/canonicalLabReplay";
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow"; import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import { defaultSceneSettings } from "../../sceneSettings";
type MediaMode = "video" | "camera"; export function CanonicalVegetationRerunReplay({ resultId, review }: {
type SpatialMode = "3d" | "plan";
type SpatialLayer = "source" | "local" | "tgs" | "semantic";
type SemanticLayer = "city" | "vegetation";
interface CanonicalReplayLaunch {
base: ObservationSessionReplayLaunch;
replay: CanonicalLabReplayDescriptor;
}
const RERUN_UNIFIED_CAMERA_SHARE_PERCENT = 46;
const RERUN_NATIVE_DIVIDER_HIT_SLOP_PX = 10;
export function CanonicalVegetationRerunReplay({
resultId,
review,
}: {
resultId: string; resultId: string;
review: VegetationFullRouteReview; review: VegetationFullRouteReview;
}) { }) {
const { return <CanonicalResultRerunReplay resultId={resultId} sessionId={review.sessionId}
mediaMode, initialPlaybackStartSeconds={review.timelineStartSeconds} semantics
spatialMode, resolveReplay={resolveCanonicalLabReplay} label="RAVNOVES004TREE · канонический повтор Rerun" />;
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange,
onExpandedChange,
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
initialMediaMode: "video",
initialSpatialMode: "3d",
});
const splitView = mediaMode !== null && spatialMode !== null;
const [semanticLayer, setSemanticLayer] = useState<SemanticLayer>("vegetation");
const [showSemantics, setShowSemantics] = useState(true);
const [spatialLayer, setSpatialLayer] = useState<SpatialLayer>("source");
const [viewResetGeneration, setViewResetGeneration] = useState<0 | 1>(0);
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
const [playbackController, setPlaybackController] =
useState<RerunPlaybackController | null>(null);
const [viewerStatus, setViewerStatus] = useState<RerunViewportStatus>("idle");
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
const [launchError, setLaunchError] = useState<string | null>(null);
const viewerFrameRef = useRef<HTMLDivElement>(null);
const nativeSplitPercentRef = useRef(RERUN_UNIFIED_CAMERA_SHARE_PERCENT);
const nativeSplitTrackingCleanupRef = useRef<(() => void) | null>(null);
const previousSplitViewRef = useRef(splitView);
const trackedLaunchSha256Ref = useRef<string | null>(null);
const stopNativeSplitTracking = useCallback(() => {
nativeSplitTrackingCleanupRef.current?.();
nativeSplitTrackingCleanupRef.current = null;
}, []);
const trackNativeSplit = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
if (
!splitView
|| event.button !== 0
|| !(event.target instanceof HTMLCanvasElement)
) return;
const frame = viewerFrameRef.current;
if (!frame) return;
const bounds = frame.getBoundingClientRect();
if (bounds.width <= 0) return;
const dividerX = bounds.left
+ bounds.width * nativeSplitPercentRef.current / 100;
if (Math.abs(event.clientX - dividerX) > RERUN_NATIVE_DIVIDER_HIT_SLOP_PX) return;
stopNativeSplitTracking();
const pointerId = event.pointerId;
const update = (pointerEvent: PointerEvent) => {
if (pointerEvent.pointerId !== pointerId) return;
const currentBounds = frame.getBoundingClientRect();
if (currentBounds.width <= 0) return;
const next = Math.min(90, Math.max(
10,
(pointerEvent.clientX - currentBounds.left) / currentBounds.width * 100,
));
nativeSplitPercentRef.current = next;
frame.style.setProperty("--canonical-rerun-camera-pane", `${next}%`);
};
const stop = (pointerEvent: PointerEvent) => {
if (pointerEvent.pointerId !== pointerId) return;
stopNativeSplitTracking();
};
window.addEventListener("pointermove", update, true);
window.addEventListener("pointerup", stop, true);
window.addEventListener("pointercancel", stop, true);
nativeSplitTrackingCleanupRef.current = () => {
window.removeEventListener("pointermove", update, true);
window.removeEventListener("pointerup", stop, true);
window.removeEventListener("pointercancel", stop, true);
};
}, [splitView, stopNativeSplitTracking]);
useEffect(() => stopNativeSplitTracking, [stopNativeSplitTracking]);
useEffect(() => {
if (!splitView) stopNativeSplitTracking();
const launchSha256 = launch?.replay.sha256 ?? null;
if (
trackedLaunchSha256Ref.current !== launchSha256
|| (splitView && !previousSplitViewRef.current)
) {
nativeSplitPercentRef.current = RERUN_UNIFIED_CAMERA_SHARE_PERCENT;
}
trackedLaunchSha256Ref.current = launchSha256;
previousSplitViewRef.current = splitView;
const cameraPanePercent = mediaMode === null
? 0
: splitView
? nativeSplitPercentRef.current
: 100;
viewerFrameRef.current?.style.setProperty(
"--canonical-rerun-camera-pane",
`${cameraPanePercent}%`,
);
}, [launch?.replay.sha256, mediaMode, splitView, stopNativeSplitTracking]);
useEffect(() => {
const controller = new AbortController();
setLaunch(null);
setLaunchError(null);
setPlayback(null);
setPlaybackController(null);
setViewerStatus("idle");
void resolveObservationSessionReplay(review.sessionId, {
signal: controller.signal,
maximumWaitMs: 30 * 60 * 1000,
onUpdate: () => undefined,
}).then(async (value) => ({
base: value,
replay: await resolveCanonicalLabReplay(resultId, value, {
signal: controller.signal,
}),
})).then((value) => {
if (!controller.signal.aborted) setLaunch(value);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setLaunchError(
caught instanceof Error ? caught.message : "Каноническая запись RAV004 недоступна.",
);
}
});
return () => controller.abort();
}, [resultId, review.sessionId]);
const presentationReady = playbackController !== null
&& isRecordedPlaybackPresentationReady(viewerStatus, playback);
const presentationState = launchError || viewerStatus === "error"
? "error"
: presentationReady
? "ready"
: "loading";
const sceneSettings = useMemo(() => ({
...defaultSceneSettings,
accumulationSeconds: spatialLayer === "local" ? 5 : 0,
showPoints: spatialMode !== null,
showTrajectory: spatialMode !== null,
showGrid: spatialMode !== null,
pointSize: 3.8,
}), [spatialLayer, spatialMode]);
const profile = launch ? recordedSessionRerunProfile({
sourceUrl: launch.replay.sourceUrl,
artifact: {
sourceUrl: launch.replay.sourceUrl,
viewerSourceUrl: launch.replay.viewerSourceUrl,
byteLength: launch.replay.byteLength,
sha256: launch.replay.sha256,
},
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
initialPlaybackStartSeconds: review.timelineStartSeconds,
view: mediaMode !== null ? "perception" : "spatial",
viewResetGeneration,
followTrajectory: true,
semanticLayer,
unifiedPerception: splitView,
planView: spatialMode === "plan",
perceptionLayers: {
enabled: mediaMode !== null,
detections2d: mediaMode === "video",
segmentation: mediaMode === "video" && showSemantics,
cuboids3d: false,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: mediaMode !== null,
}) : null;
const mediaLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<Button
size="dense"
shape="pill"
variant={showSemantics ? "primary" : "secondary"}
aria-pressed={showSemantics}
onClick={() => setShowSemantics((visible) => !visible)}
>
СЕМАНТИКА
</Button>
<SegmentedControl
value={semanticLayer}
items={[
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
]}
label="Источник семантики"
size="dense"
onChange={(value) => {
setSemanticLayer(value);
setShowSemantics(true);
}}
/>
</div>
);
const spatialLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Пространственные слои RAV004"
>
<SegmentedControl
value={spatialLayer}
items={[
{ value: "source", label: "ИСХ. ТОЧКИ" },
{ value: "local", label: "ЛОК. SLAM" },
{ value: "tgs", label: "TGS", disabled: true },
{ value: "semantic", label: "СЕМАНТИКА", disabled: true },
]}
label="Пространственные слои"
size="dense"
onChange={setSpatialLayer}
/>
</div>
);
const resetSpatialView = (
<Button
size="dense"
variant="ghost"
icon={<Icon name="refresh" size={14} />}
aria-label="Сбросить положение 3D камеры"
title="Сбросить положение 3D камеры"
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}
>
</Button>
);
const transport = presentationReady && playback && playbackController ? (
<ObservationTimeline
className="m4-replay-threat-visual__timeline canonical-vegetation-rerun-replay__timeline"
active
sourceCount={3}
mode="recorded"
seekable
synchronization="shared-clock"
rangeNs={playback.rangeNs}
currentNs={playback.currentNs}
playing={playback.playing}
onSeek={playbackController.seek}
onPlayingChange={playbackController.setPlaying}
showJumpToEnd={false}
/>
) : undefined;
return (
<div
className="canonical-vegetation-rerun-replay"
data-presentation-state={presentationState}
aria-busy={presentationState === "loading"}
>
<CanonicalRecordedLabReplay
label="RAVNOVES004TREE · канонический повтор Rerun"
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "ВИДЕО" },
{ value: "camera", label: "КАМЕРА" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "ПЛАН" },
]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео и семантика"}
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer
unifiedContent={profile ? (
<div
ref={viewerFrameRef}
className="canonical-vegetation-rerun-replay__viewport-lock"
data-split-view={splitView ? "true" : undefined}
style={{
"--canonical-rerun-camera-pane": `${
mediaMode === null
? 0
: splitView
? nativeSplitPercentRef.current
: 100
}%`,
} as CSSProperties}
onPointerDownCapture={splitView ? trackNativeSplit : undefined}
>
<RerunViewport
profile={profile}
sceneSettings={sceneSettings}
onStatusChange={setViewerStatus}
onPlaybackChange={setPlayback}
onPlaybackControllerChange={setPlaybackController}
/>
</div>
) : launchError ? (
<div className="l3-visual-audit__state" role="alert">
{launchError}
</div>
) : (
<div aria-hidden="true" />
)}
emptyMessage="Выберите ВИДЕО/КАМЕРА или 3D/ПЛАН. Общие часы Rerun останутся на месте."
deckOverlays={presentationState === "loading" ? (
<div className="canonical-vegetation-rerun-replay__loading">
<ActivityIndicator label="Загружаем синхронизированную запись" />
</div>
) : undefined}
transport={transport}
onMediaModeChange={onMediaModeChange}
onSpatialModeChange={onSpatialModeChange}
onExpandedChange={onExpandedChange}
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
/>
</div>
);
} }
@@ -0,0 +1,30 @@
import { GlassSurface, StatusBadge } from "@nodedc/ui-react";
import { CanonicalResultRerunReplay } from "./CanonicalResultRerunReplay";
import { resolveCanonicalLabReplay } from "../../core/laboratory/canonicalLabReplay";
import type { ObservatoryPortableResultReview } from "../../core/observatory/recordedRun";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
function resolvePortableReplay(resultId: string, launch: ObservationSessionReplayLaunch,
options: { signal: AbortSignal }) {
return resolveCanonicalLabReplay(resultId, launch, { ...options, sourceKind: "portable-tgs" });
}
export function PortableResultReplay({ review }: { review: ObservatoryPortableResultReview }) {
const tgs = review.resultDocument.schema_version === "missioncore.recorded-tgs-costmap-review/v2";
if (!tgs) return (
<GlassSurface className="observatory-replay-state" padding="lg">
<div><StatusBadge tone="success">Проверено</StatusBadge>
<h3>{review.resultKind}</h3><p>Связанных артефактов: {review.artifacts.length}</p>
<details><summary>Документ результата</summary>
<pre>{JSON.stringify(review.resultDocument, null, 2)}</pre></details>
</div>
</GlassSurface>
);
return <>
<p>Камера, исходное облако и TGS · общая шкала времени. Зелёный — опора,
красный — занятое пространство, жёлтый — отклонённые точки.
Отсутствие ячеек не означает свободный путь. Сегментация и детекция в этот профиль не входят.</p>
<CanonicalResultRerunReplay key={review.resultId} resultId={review.resultId}
sessionId={review.sourceSessionId} costmap resolveReplay={resolvePortableReplay} />
</>;
}
@@ -23,26 +23,28 @@ export async function resolveCanonicalLabReplay(
origin = window.location.origin, origin = window.location.origin,
signal, signal,
fetcher = globalThis.fetch, fetcher = globalThis.fetch,
sourceKind = "legacy-vegetation",
}: { }: {
origin?: string; origin?: string;
signal?: AbortSignal; signal?: AbortSignal;
fetcher?: typeof globalThis.fetch; fetcher?: typeof globalThis.fetch;
sourceKind?: "legacy-vegetation" | "portable-tgs";
} = {}, } = {},
): Promise<CanonicalLabReplayDescriptor> { ): Promise<CanonicalLabReplayDescriptor> {
const base = new URL(origin); const base = new URL(origin);
if ( if (
!SAFE_RESULT_ID.test(resultId) !(sourceKind === "portable-tgs" ? /^m49-tgs-portable-review-[a-f0-9]{64}$/.test(resultId) : SAFE_RESULT_ID.test(resultId))
|| !SAFE_SESSION_SOURCE.test(launch.sourceUrl) || !SAFE_SESSION_SOURCE.test(launch.sourceUrl)
|| launch.viewerSourceUrl !== `${launch.sourceUrl}?generation=${launch.sha256}` || launch.viewerSourceUrl !== `${launch.sourceUrl}?generation=${launch.sha256}`
|| !/^[a-f0-9]{64}$/.test(launch.sha256) || !/^[a-f0-9]{64}$/.test(launch.sha256)
) { ) {
throw new Error("Канонический replay LAB имеет небезопасный descriptor."); throw new Error("Канонический replay LAB имеет небезопасный descriptor.");
} }
const sourceUrl = const sourceUrl = sourceKind === "portable-tgs"
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}` ? `/api/v1/observatory/portable-results/${resultId}/replays/${launch.sha256}/recording.rrd`
+ "/canonical-replay.rrd"; : `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/canonical-replay.rrd`;
const descriptorUrl = new URL(sourceUrl, `${base.origin}/`); const descriptorUrl = new URL(sourceUrl, `${base.origin}/`);
descriptorUrl.searchParams.set("base_generation", launch.sha256); if (sourceKind === "legacy-vegetation") descriptorUrl.searchParams.set("base_generation", launch.sha256);
if (descriptorUrl.origin !== base.origin) { if (descriptorUrl.origin !== base.origin) {
throw new Error("Канонический replay LAB должен быть same-origin."); throw new Error("Канонический replay LAB должен быть same-origin.");
} }
@@ -27,6 +27,7 @@ export interface RecordedPerceptionLayers {
detections2d: boolean; detections2d: boolean;
segmentation: boolean; segmentation: boolean;
cuboids3d: boolean; cuboids3d: boolean;
costmap?: boolean;
} }
export interface RecordedRrdArtifactDescriptor { export interface RecordedRrdArtifactDescriptor {
@@ -15,6 +15,7 @@ import {
} from "@nodedc/ui-react"; } from "@nodedc/ui-react";
import { CanonicalVegetationRerunReplay } from "../../components/laboratory/CanonicalVegetationRerunReplay"; import { CanonicalVegetationRerunReplay } from "../../components/laboratory/CanonicalVegetationRerunReplay";
import { PortableResultReplay } from "../../components/laboratory/PortableResultReplay";
import type { ObservationSessionStatus } from "../../core/observation/sessionArchive"; import type { ObservationSessionStatus } from "../../core/observation/sessionArchive";
import { createObservationReplayCoordinator } from "../../core/observation/replayCoordinator"; import { createObservationReplayCoordinator } from "../../core/observation/replayCoordinator";
import { import {
@@ -715,15 +716,13 @@ export function ObservatoryWorkspace({
<header className="observatory-replay__header"> <header className="observatory-replay__header">
<div> <div>
<span className="section-eyebrow"> <span className="section-eyebrow">
{replay.review.kind === "canonical-recorded-rerun" СОХРАНЁННЫЙ РЕЗУЛЬТАТ / ЗАПИСАННАЯ СЕССИЯ
? "ВИЗУАЛЬНЫЙ РАЗБОР / ЗАПИСАННАЯ СЕССИЯ"
: "РЕЗУЛЬТАТ / ПРОВЕРЕННЫЙ ДОКУМЕНТ"}
</span> </span>
<h3>{replayEvidence?.label ?? replay.binding.resultId}</h3> <h3>{replayEvidence?.label ?? replay.binding.resultId}</h3>
<p> <p>
{replay.review.kind === "canonical-recorded-rerun" {replay.review.kind === "canonical-recorded-rerun"
? "Записанный маршрут синхронизирован по общей временной шкале." ? "Записанный маршрут синхронизирован по общей временной шкале."
: "Показан проверенный документ результата и связанные с ним артефакты."} : "Просмотр сохранённых данных результата без повторного расчёта."}
</p> </p>
</div> </div>
<Button <Button
@@ -741,17 +740,7 @@ export function ObservatoryWorkspace({
review={replay.review.review} review={replay.review.review}
/> />
) : ( ) : (
<GlassSurface className="observatory-replay-state" padding="lg"> <PortableResultReplay review={replay.review} />
<div>
<StatusBadge tone="success">Проверено</StatusBadge>
<h3>{replay.review.resultKind}</h3>
<p>Связанных артефактов: {replay.review.artifacts.length}</p>
<details>
<summary>Документ результата</summary>
<pre>{JSON.stringify(replay.review.resultDocument, null, 2)}</pre>
</details>
</div>
</GlassSurface>
)} )}
</section> </section>
) : null} ) : null}
@@ -111,7 +111,7 @@ test("Observatory owns a bounded explicit-open lifecycle around the shared recor
const observatoryCore = await read("core/observatory/catalog.ts"); const observatoryCore = await read("core/observatory/catalog.ts");
const recordedRun = await read("core/observatory/recordedRun.ts"); const recordedRun = await read("core/observatory/recordedRun.ts");
const sharedReplay = await read( const sharedReplay = await read(
"components/laboratory/CanonicalVegetationRerunReplay.tsx", "components/laboratory/CanonicalResultRerunReplay.tsx",
); );
const workspaceCss = await read("styles/workspaces.css"); const workspaceCss = await read("styles/workspaces.css");
const observatoryCss = await read("styles/observatory.css"); const observatoryCss = await read("styles/observatory.css");
@@ -144,6 +144,13 @@ test("Observatory owns a bounded explicit-open lifecycle around the shared recor
); );
assert.equal(sharedReplay.match(/<RerunViewport\b/g)?.length, 1); assert.equal(sharedReplay.match(/<RerunViewport\b/g)?.length, 1);
assert.match(sharedReplay, /recordedSessionRerunProfile/); assert.match(sharedReplay, /recordedSessionRerunProfile/);
assert.match(sharedReplay, /useState<0 \| 1>\(costmap \? 1 : 0\)/);
assert.match(sharedReplay, /costmap \? 0\.000001 : 0/);
for (const adapter of ["CanonicalVegetationRerunReplay", "PortableResultReplay"]) {
const source = await read(`components/laboratory/${adapter}.tsx`);
assert.match(source, /<CanonicalResultRerunReplay/);
assert.doesNotMatch(source, /<RerunViewport|<video|setInterval|Canvas|THREE\./);
}
assert.doesNotMatch(workspaceCss, /\.observatory-/); assert.doesNotMatch(workspaceCss, /\.observatory-/);
assert.match(observatoryCss, /\.observatory-workspace/); assert.match(observatoryCss, /\.observatory-workspace/);
}); });
@@ -132,3 +132,26 @@ test("canonical LAB resolves one generation-bound merged RRD", async () => {
blueprintSourceUrl: "/api/v1/observation-sessions/session-001/blueprint.rrd", blueprintSourceUrl: "/api/v1/observation-sessions/session-001/blueprint.rrd",
}); });
}); });
test("portable TGS resolves Core cache for the exact result and base, never a legacy LAB", async () => {
const base = "a".repeat(64), generation = "b".repeat(64);
const resultId = `m49-tgs-portable-review-${"c".repeat(64)}`;
const launch = { sourceUrl: "/api/v1/observation-sessions/source/recording.rrd",
viewerSourceUrl: `/api/v1/observation-sessions/source/recording.rrd?generation=${base}`,
sha256: base };
let calls = 0;
const fetcher = async (url, options) => {
calls++;
assert.equal(url, `http://mission-core.test/api/v1/observatory/portable-results/${resultId}/replays/${base}/recording.rrd`);
assert.equal(options.method, "HEAD");
return new Response(null, { headers: { "Content-Type": "application/vnd.rerun.rrd",
"Content-Length": "100", "ETag": `"${generation}"`, "X-Rerun-Format": "RRF2" } });
};
const options = { sourceKind: "portable-tgs", origin: "http://mission-core.test", fetcher };
const replay = await resolveCanonicalLabReplay(resultId, launch, options);
assert.equal(replay.viewerSourceUrl, `${replay.sourceUrl}?generation=${generation}`);
assert.equal(replay.blueprintSourceUrl, "/api/v1/observation-sessions/source/blueprint.rrd");
await assert.rejects(resolveCanonicalLabReplay(`lab-v1-vegetation-shadow-${"c".repeat(64)}`, launch, options));
await assert.rejects(resolveCanonicalLabReplay(resultId, { ...launch, sha256: "d".repeat(64) }, options));
assert.equal(calls, 1);
});
@@ -63,7 +63,7 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
read("workspaces/observatory/ObservatoryWorkspace.tsx"), read("workspaces/observatory/ObservatoryWorkspace.tsx"),
read("core/observatory/useObservatoryCatalog.ts"), read("core/observatory/useObservatoryCatalog.ts"),
read("core/observatory/recordedRun.ts"), read("core/observatory/recordedRun.ts"),
read("components/laboratory/CanonicalVegetationRerunReplay.tsx"), read("components/laboratory/CanonicalResultRerunReplay.tsx"),
read("workspaces/laboratory/CanonicalVegetationRerunReplay.tsx"), read("workspaces/laboratory/CanonicalVegetationRerunReplay.tsx"),
read("core/observation/viewerProfile.ts"), read("core/observation/viewerProfile.ts"),
read("styles/observatory.css"), read("styles/observatory.css"),
@@ -99,9 +99,10 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
assert.doesNotMatch(workspace, /observatory-evidence-card[^>]*tone="soft"/); assert.doesNotMatch(workspace, /observatory-evidence-card[^>]*tone="soft"/);
assert.match(workspace, /Открыть визуальный разбор/); assert.match(workspace, /Открыть визуальный разбор/);
assert.match(workspace, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/); assert.match(workspace, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/);
assert.match(workspace, /replay\.review\.kind === "canonical-recorded-rerun"[\s\S]*РЕЗУЛЬТАТ \/ ПРОВЕРЕННЫЙ ДОКУМЕНТ/); assert.match(workspace, /<PortableResultReplay review=\{replay\.review\}/);
assert.match(workspace, /Связанных артефактов: \{replay\.review\.artifacts\.length\}/); const portable = await read("components/laboratory/PortableResultReplay.tsx");
assert.match(workspace, /<summary>Документ результата<\/summary>/); assert.match(portable, /<summary>Документ результата<\/summary>/);
assert.match(portable, /<CanonicalResultRerunReplay/);
assert.doesNotMatch(workspace, /UNIVERSAL VIEWER|content-addressed artifacts/); assert.doesNotMatch(workspace, /UNIVERSAL VIEWER|content-addressed artifacts/);
assert.match(workspace, /Проверяем точную связь результата/); assert.match(workspace, /Проверяем точную связь результата/);
assert.match(workspace, /role="alert"/); assert.match(workspace, /role="alert"/);
@@ -14,6 +14,8 @@ let liveRerunReceiverBindingIdentity;
let recordedOpenWatchdogTimeoutMs; let recordedOpenWatchdogTimeoutMs;
let rerunViewerInitialSource; let rerunViewerInitialSource;
let resolveRecordedViewerSourceUrl; let resolveRecordedViewerSourceUrl;
let isRecordedRrdSource;
let resolveRecordedBlueprintUrl;
before(async () => { before(async () => {
server = await createServer({ server = await createServer({
@@ -31,6 +33,8 @@ before(async () => {
recordedOpenWatchdogTimeoutMs, recordedOpenWatchdogTimeoutMs,
rerunViewerInitialSource, rerunViewerInitialSource,
resolveRecordedViewerSourceUrl, resolveRecordedViewerSourceUrl,
isRecordedRrdSource,
resolveRecordedBlueprintUrl,
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx")); } = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
}); });
@@ -86,6 +90,18 @@ test("one canonical LAB replay generation reaches the same native receiver", ()
); );
}); });
test("portable replay uses recorded admission and the shared source blueprint", () => {
const source = `/api/v1/observatory/portable-results/m49-tgs-portable-review-${"c".repeat(64)}/replays/${"d".repeat(64)}/recording.rrd`;
const blueprint = "/api/v1/observation-sessions/source/blueprint.rrd";
assert.equal(isRecordedRrdSource(source), true);
assert.equal(isRecordedRrdSource(source.replace("/replays/", "/untrusted/")), false);
assert.equal(resolveRecordedViewerSourceUrl({ sourceUrl: source,
viewerSourceUrl: `${source}?generation=${sha256}`, sha256, byteLength: 100 },
"http://mission-core.test"), `http://mission-core.test${source}?generation=${sha256}`);
assert.equal(resolveRecordedBlueprintUrl(source, "http://mission-core.test", blueprint),
`http://mission-core.test${blueprint}`);
});
test("live presentation waits for the exact receiver to expose a usable range", () => { test("live presentation waits for the exact receiver to expose a usable range", () => {
assert.equal(isLiveRerunPresentationReady(false, { min: 1, max: 2 }, 1), false); assert.equal(isLiveRerunPresentationReady(false, { min: 1, max: 2 }, 1), false);
assert.equal(isLiveRerunPresentationReady(true, null, 1), false); assert.equal(isLiveRerunPresentationReady(true, null, 1), false);
@@ -225,6 +241,9 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
assert.doesNotMatch(source, /streamVerifiedRecordedRrd/); assert.doesNotMatch(source, /streamVerifiedRecordedRrd/);
assert.doesNotMatch(source, /missioncore\/recorded-recording/); assert.doesNotMatch(source, /missioncore\/recorded-recording/);
assert.doesNotMatch(source, /recordedChannel/); assert.doesNotMatch(source, /recordedChannel/);
assert.match(source, /recordedPerceptionLayers\.costmap,/);
assert.match(source, /recordedPerceptionLayers\.costmap !== undefined && status !== "ready"/);
assert.match(source, /perceptionLayers\.costmap === undefined \? \{\} : \{\s*show_costmap: perceptionLayers\.costmap,\s*reactivate_updates: true/s);
assert.match(source, /viewer\.start\(\s*rerunViewerInitialSource\(resolvedSource\)/s); assert.match(source, /viewer\.start\(\s*rerunViewerInitialSource\(resolvedSource\)/s);
assert.doesNotMatch(source, /rerunViewerOpenOptions/); assert.doesNotMatch(source, /rerunViewerOpenOptions/);
assert.match( assert.match(
@@ -470,7 +470,7 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
"utf8", "utf8",
), ),
readFile( readFile(
new URL("../src/components/laboratory/CanonicalVegetationRerunReplay.tsx", import.meta.url), new URL("../src/components/laboratory/CanonicalResultRerunReplay.tsx", import.meta.url),
"utf8", "utf8",
), ),
readFile( readFile(
@@ -494,7 +494,7 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
assert.match(rerunSource, /<RerunViewport/); assert.match(rerunSource, /<RerunViewport/);
assert.match(rerunSource, /ИСХ\. ТОЧКИ/); assert.match(rerunSource, /ИСХ\. ТОЧКИ/);
assert.match(rerunSource, /ЛОК\. SLAM/); assert.match(rerunSource, /ЛОК\. SLAM/);
assert.match(rerunSource, /resolveCanonicalLabReplay/); assert.match(rerunSource, /resolveReplay\(resultId, value/);
assert.doesNotMatch(rerunSource, /canonical-overlay\.rrd/); assert.doesNotMatch(rerunSource, /canonical-overlay\.rrd/);
assert.match(rerunSource, /unifiedPerception: splitView/); assert.match(rerunSource, /unifiedPerception: splitView/);
assert.match(rerunSource, /lockPerceptionCameraInteraction: mediaMode !== null/); assert.match(rerunSource, /lockPerceptionCameraInteraction: mediaMode !== null/);
@@ -527,7 +527,7 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
); );
assert.doesNotMatch(rerunSource, /LaboratoryRecordedClipPlayer|LaboratoryMetricEvidenceScene/); assert.doesNotMatch(rerunSource, /LaboratoryRecordedClipPlayer|LaboratoryMetricEvidenceScene/);
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/); assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
assert.match(rerunSource, /value: "tgs", label: "TGS", disabled: true/); assert.match(rerunSource, /value: "tgs", label: "TGS", disabled: !costmap/);
assert.match(canonicalSource, /primary=\{mediaPane\}/); assert.match(canonicalSource, /primary=\{mediaPane\}/);
assert.match(canonicalSource, /secondary=\{spatialPane/); assert.match(canonicalSource, /secondary=\{spatialPane/);
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/); assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
+35 -19
View File
@@ -45,12 +45,11 @@ class RecordedBlueprintError(RuntimeError):
class _RecordedBlueprintStream: class _RecordedBlueprintStream:
"""Keep one browser viewport on one mutable Rerun blueprint store. """Keep one bounded SDK blueprint source for each browser viewport.
Rerun persists the operator-controlled eye inside the active blueprint Upstream 0.36.3 activates a clone, not this source store. Explicit refresh
store. Creating and activating a fresh store for every layer toggle drops makes layer changes visible but cannot retain the clone's operator eye.
that eye and snaps the view back to its fallback camera. This stream keeps It is opt-in for portable replay pending native camera-state support.
the store identity stable until the operator explicitly requests a reset.
""" """
def __init__( def __init__(
@@ -82,18 +81,24 @@ class _RecordedBlueprintStream:
def render( def render(
self, self,
blueprint_factory: Callable[[bool], rrb.Blueprint], blueprint_factory: Callable[[bool, bool], rrb.Blueprint],
*, *,
follow_trajectory: bool, follow_trajectory: bool,
plan_view: bool, plan_view: bool,
reactivate_updates: bool = False,
) -> bytes: ) -> bytes:
with self._lock: with self._lock:
if self._closed: if self._closed:
raise RecordedBlueprintError("stable blueprint stream is closed") raise RecordedBlueprintError("stable blueprint stream is closed")
eye_contract = (follow_trajectory, plan_view) eye_contract = (follow_trajectory, plan_view)
update_eye_controls = self._eye_contract != eye_contract update_eye_controls = self._eye_contract != eye_contract
blueprint = blueprint_factory(update_eye_controls) # Initial admission/reset uses native framing. Explicit presets
make_active = self._sequence == 0 # apply to mode transitions, after the viewer has a source cursor.
blueprint = blueprint_factory(update_eye_controls, self._eye_contract is not None)
# Appending rows alone does not refresh upstream's active clone.
# Keep legacy admission unchanged; portable replay opts into
# working layer updates with an explicitly documented eye reset.
make_active = self._sequence == 0 or reactivate_updates
self._blueprint_recording.set_time( self._blueprint_recording.set_time(
"blueprint", "blueprint",
sequence=self._sequence, sequence=self._sequence,
@@ -124,9 +129,9 @@ class _RecordedBlueprintStream:
_MAX_RECORDED_BLUEPRINT_STREAMS = 32 _MAX_RECORDED_BLUEPRINT_STREAMS = 32
_recorded_blueprint_streams_lock = Lock() _recorded_blueprint_streams_lock = Lock()
_recorded_blueprint_streams: OrderedDict[ _recorded_blueprint_streams: OrderedDict[tuple[str, str, str], _RecordedBlueprintStream] = (
tuple[str, str, str], _RecordedBlueprintStream OrderedDict()
] = OrderedDict() )
def _stable_recorded_blueprint_stream( def _stable_recorded_blueprint_stream(
@@ -170,8 +175,10 @@ def recorded_blueprint(
show_detections_2d: bool = False, show_detections_2d: bool = False,
show_segmentation: bool = False, show_segmentation: bool = False,
show_cuboids_3d: bool = False, show_cuboids_3d: bool = False,
show_costmap: bool = False,
follow_trajectory: bool = False, follow_trajectory: bool = False,
update_eye_controls: bool = True, update_eye_controls: bool = True,
explicit_spatial_preset: bool = False,
) -> rrb.Blueprint: ) -> rrb.Blueprint:
accumulation = max(0.0, settings.accumulation_seconds) accumulation = max(0.0, settings.accumulation_seconds)
time_ranges: list[rr.VisibleTimeRange] | None = None time_ranges: list[rr.VisibleTimeRange] | None = None
@@ -220,6 +227,9 @@ def recorded_blueprint(
if plan_view and update_eye_controls if plan_view and update_eye_controls
else rrb.EyeControls3D.from_fields( else rrb.EyeControls3D.from_fields(
kind=rrb.Eye3DKind.Orbital if follow_trajectory else None, kind=rrb.Eye3DKind.Orbital if follow_trajectory else None,
position=[16.0, -16.0, 18.0] if explicit_spatial_preset else None,
look_target=[0.0, 0.0, 0.0] if explicit_spatial_preset else None,
eye_up=[0.0, 0.0, 1.0] if explicit_spatial_preset else None,
tracking_entity="/world/sensor_pose" if follow_trajectory else "", tracking_entity="/world/sensor_pose" if follow_trajectory else "",
) )
if update_eye_controls if update_eye_controls
@@ -239,6 +249,7 @@ def recorded_blueprint(
# inherits the view's latest-at query and can never turn into an # inherits the view's latest-at query and can never turn into an
# object-history trail when the operator widens the cloud window. # object-history trail when the operator widens the cloud window.
"/world/points": point_overrides, "/world/points": point_overrides,
"/world/costmap": rrb.EntityBehavior(visible=show_costmap),
"/world/trajectory": trajectory_overrides, "/world/trajectory": trajectory_overrides,
"/world/perception": rrb.EntityBehavior(visible=show_cuboids_3d), "/world/perception": rrb.EntityBehavior(visible=show_cuboids_3d),
"/world/perception/lidar": rrb.EntityBehavior(visible=False), "/world/perception/lidar": rrb.EntityBehavior(visible=False),
@@ -251,16 +262,13 @@ def recorded_blueprint(
# Log an explicit empty range set so a previous accumulated blueprint # Log an explicit empty range set so a previous accumulated blueprint
# for this stable view id is cleared instead of surviving in Rerun. # for this stable view id is cleared instead of surviving in Rerun.
time_ranges=rrb.VisibleTimeRanges([]), time_ranges=rrb.VisibleTimeRanges([]),
# Follow is a property of the same operator eye, not another view. # Follow and plan configure the native eye, not the point coordinates.
# Keeping the view id stable preserves the current orbit offset when # An empty tracking path clears tracking. Retaining this source view id
# tracking is toggled. An explicit empty path clears tracking without # alone does not preserve edits in upstream's activated blueprint clone.
# overwriting the position/look-target saved by user interaction.
eye_controls=spatial_eye_controls, eye_controls=spatial_eye_controls,
) )
spatial_view.id = ( spatial_view.id = (
RECORDED_SPATIAL_RESET_VIEW_ID RECORDED_SPATIAL_RESET_VIEW_ID if view_reset_generation else RECORDED_SPATIAL_VIEW_ID
if view_reset_generation
else RECORDED_SPATIAL_VIEW_ID
) )
camera_view = rrb.Spatial2DView( camera_view = rrb.Spatial2DView(
origin="/perception/camera", origin="/perception/camera",
@@ -309,6 +317,7 @@ def recorded_blueprint(
visible=settings.show_trajectory, visible=settings.show_trajectory,
), ),
"/world/perception": rrb.EntityBehavior(visible=True), "/world/perception": rrb.EntityBehavior(visible=True),
"/world/costmap": rrb.EntityBehavior(visible=show_costmap),
# The overlay already carries the selected fusion support points; # The overlay already carries the selected fusion support points;
# its grey diagnostic LiDAR copy would otherwise double-render the # its grey diagnostic LiDAR copy would otherwise double-render the
# native cloud from /world/points. # native cloud from /world/points.
@@ -420,11 +429,15 @@ def recorded_blueprint_rrd(
show_detections_2d: bool = False, show_detections_2d: bool = False,
show_segmentation: bool = False, show_segmentation: bool = False,
show_cuboids_3d: bool = False, show_cuboids_3d: bool = False,
show_costmap: bool = False,
follow_trajectory: bool = False, follow_trajectory: bool = False,
reactivate_updates: bool = False,
) -> bytes: ) -> bytes:
"""Serialize a bounded active blueprint update without recorded data.""" """Serialize a bounded active blueprint update without recorded data."""
def build_blueprint(update_eye_controls: bool) -> rrb.Blueprint: def build_blueprint(
update_eye_controls: bool, use_spatial_preset: bool = False
) -> rrb.Blueprint:
return recorded_blueprint( return recorded_blueprint(
settings, settings,
include_initial_playback_state=False, include_initial_playback_state=False,
@@ -436,8 +449,10 @@ def recorded_blueprint_rrd(
show_detections_2d=show_detections_2d, show_detections_2d=show_detections_2d,
show_segmentation=show_segmentation, show_segmentation=show_segmentation,
show_cuboids_3d=show_cuboids_3d, show_cuboids_3d=show_cuboids_3d,
show_costmap=show_costmap,
follow_trajectory=follow_trajectory, follow_trajectory=follow_trajectory,
update_eye_controls=update_eye_controls, update_eye_controls=update_eye_controls,
explicit_spatial_preset=reactivate_updates and use_spatial_preset,
) )
payload: bytes | None payload: bytes | None
@@ -452,6 +467,7 @@ def recorded_blueprint_rrd(
build_blueprint, build_blueprint,
follow_trajectory=follow_trajectory, follow_trajectory=follow_trajectory,
plan_view=plan_view, plan_view=plan_view,
reactivate_updates=reactivate_updates,
) )
except RecordedBlueprintError: except RecordedBlueprintError:
raise raise
+4
View File
@@ -130,6 +130,8 @@ class RecordedBlueprintRequest(StrictApiModel):
show_detections_2d: StrictBool = False show_detections_2d: StrictBool = False
show_segmentation: StrictBool = False show_segmentation: StrictBool = False
show_cuboids_3d: StrictBool = False show_cuboids_3d: StrictBool = False
show_costmap: StrictBool = False
reactivate_updates: StrictBool = False
follow_trajectory: StrictBool = False follow_trajectory: StrictBool = False
@@ -980,6 +982,8 @@ def build_session_router(
show_detections_2d=request.show_detections_2d, show_detections_2d=request.show_detections_2d,
show_segmentation=request.show_segmentation, show_segmentation=request.show_segmentation,
show_cuboids_3d=request.show_cuboids_3d, show_cuboids_3d=request.show_cuboids_3d,
show_costmap=request.show_costmap,
reactivate_updates=request.reactivate_updates,
follow_trajectory=request.follow_trajectory, follow_trajectory=request.follow_trajectory,
) )
except SessionNotFoundError as exc: except SessionNotFoundError as exc:
+33 -4
View File
@@ -499,11 +499,14 @@ def test_recorded_follow_mode_tracks_sensor_pose_with_the_same_orbital_eye() ->
} }
def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset( @pytest.mark.parametrize("reactivate_updates", [False, True])
def test_recorded_blueprint_updates_reuse_source_store_and_opt_in_to_activation(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
reactivate_updates: bool,
) -> None: ) -> None:
activations: list[tuple[object, bool, bool]] = [] activations: list[tuple[object, bool, bool]] = []
eye_control_updates: list[bool] = [] eye_control_updates: list[bool] = []
explicit_presets: list[bool] = []
send_blueprint = viewer_recorded_module.bindings.send_blueprint send_blueprint = viewer_recorded_module.bindings.send_blueprint
make_blueprint = viewer_recorded_module.recorded_blueprint make_blueprint = viewer_recorded_module.recorded_blueprint
@@ -524,6 +527,7 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
def capture_eye_control_update(*args: object, **kwargs: object) -> object: def capture_eye_control_update(*args: object, **kwargs: object) -> object:
eye_control_updates.append(bool(kwargs["update_eye_controls"])) eye_control_updates.append(bool(kwargs["update_eye_controls"]))
explicit_presets.append(bool(kwargs["explicit_spatial_preset"]))
return make_blueprint(*args, **kwargs) return make_blueprint(*args, **kwargs)
monkeypatch.setattr( monkeypatch.setattr(
@@ -537,6 +541,7 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
recording_id="stable-camera", recording_id="stable-camera",
blueprint_session_id=session_id, blueprint_session_id=session_id,
unified_perception=True, unified_perception=True,
reactivate_updates=reactivate_updates,
) )
layers_enabled = viewer_recorded_blueprint_rrd( layers_enabled = viewer_recorded_blueprint_rrd(
RerunSceneSettings(accumulation_seconds=12.0), RerunSceneSettings(accumulation_seconds=12.0),
@@ -546,6 +551,7 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
show_detections_2d=True, show_detections_2d=True,
show_segmentation=True, show_segmentation=True,
show_cuboids_3d=True, show_cuboids_3d=True,
reactivate_updates=reactivate_updates,
) )
follow_enabled = viewer_recorded_blueprint_rrd( follow_enabled = viewer_recorded_blueprint_rrd(
RerunSceneSettings(accumulation_seconds=12.0), RerunSceneSettings(accumulation_seconds=12.0),
@@ -556,6 +562,7 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
show_segmentation=True, show_segmentation=True,
show_cuboids_3d=True, show_cuboids_3d=True,
follow_trajectory=True, follow_trajectory=True,
reactivate_updates=reactivate_updates,
) )
layer_disabled_while_following = viewer_recorded_blueprint_rrd( layer_disabled_while_following = viewer_recorded_blueprint_rrd(
RerunSceneSettings(accumulation_seconds=12.0), RerunSceneSettings(accumulation_seconds=12.0),
@@ -566,6 +573,7 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
show_segmentation=True, show_segmentation=True,
show_cuboids_3d=True, show_cuboids_3d=True,
follow_trajectory=True, follow_trajectory=True,
reactivate_updates=reactivate_updates,
) )
reset = viewer_recorded_blueprint_rrd( reset = viewer_recorded_blueprint_rrd(
RerunSceneSettings(accumulation_seconds=12.0), RerunSceneSettings(accumulation_seconds=12.0),
@@ -577,6 +585,7 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
show_segmentation=True, show_segmentation=True,
show_cuboids_3d=True, show_cuboids_3d=True,
follow_trajectory=True, follow_trajectory=True,
reactivate_updates=reactivate_updates,
) )
store_pattern = rb"rec_[0-9a-f]{32}" store_pattern = rb"rec_[0-9a-f]{32}"
@@ -592,11 +601,14 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
assert len(reset_store_ids) == 1 assert len(reset_store_ids) == 1
assert reset_store_ids.isdisjoint(initial_store_ids) assert reset_store_ids.isdisjoint(initial_store_ids)
assert eye_control_updates == [True, False, True, False, True] assert eye_control_updates == [True, False, True, False, True]
assert explicit_presets == [
False, reactivate_updates, reactivate_updates, reactivate_updates, False
]
assert [activation[1:] for activation in activations] == [ assert [activation[1:] for activation in activations] == [
(True, False), (True, False),
(False, False), (reactivate_updates, False),
(False, False), (reactivate_updates, False),
(False, False), (reactivate_updates, False),
(True, False), (True, False),
] ]
assert activations[0][0] is activations[1][0] assert activations[0][0] is activations[1][0]
@@ -610,6 +622,23 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
assert len(reset) < 350_000 assert len(reset) < 350_000
def test_portable_replay_has_distinct_explicit_3d_and_plan_presets() -> None:
eyes = []
for plan in [False, True]:
blueprint = viewer_recorded_blueprint(
RerunSceneSettings(),
include_initial_playback_state=False,
explicit_spatial_preset=True,
follow_trajectory=True,
plan_view=plan,
)
eyes.append(blueprint.root_container.contents[0].properties["EyeControls3D"])
assert eyes[0].position.as_arrow_array().to_pylist() == [[16.0, -16.0, 18.0]]
assert eyes[1].position.as_arrow_array().to_pylist() == [[0.0, 0.0, 30.0]]
assert eyes[0].eye_up.as_arrow_array().to_pylist() == [[0.0, 0.0, 1.0]]
assert eyes[1].eye_up.as_arrow_array().to_pylist() == [[0.0, 1.0, 0.0]]
def test_viewer_blueprint_unifies_original_video_and_independent_ai_layers() -> None: def test_viewer_blueprint_unifies_original_video_and_independent_ai_layers() -> None:
blueprint = viewer_recorded_blueprint( blueprint = viewer_recorded_blueprint(
RerunSceneSettings(accumulation_seconds=12.0), RerunSceneSettings(accumulation_seconds=12.0),