refactor(lab): перевести RAV004 на канонический Rerun pipeline

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 12:59:16 +03:00
parent e9ffb829c9
commit f5ee42751d
30 changed files with 1425 additions and 288 deletions
@@ -151,6 +151,7 @@ interface RecordedRerunIdentity {
const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.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 LAB_RECORDED_PERCEPTION_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-overlay\.rrd$/;
const RECORDED_POINT_COLORS_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/point-colors\.rrd$/;
const MAX_BLUEPRINT_BYTES = 1_048_576;
const MAX_PERCEPTION_BYTES = 512 * 1024 * 1024;
@@ -203,12 +204,7 @@ export function resolveRerunSourceUrl(sourceUrl: string, origin: string): string
return new URL(normalized, `${base.origin}/`).href;
}
/**
* The native Rerun HTTP receiver is the only browser API in 0.34.1 with a
* persistent incremental RRD decoder. Bind it to the exact immutable
* generation selected by the launch contract; arbitrary `send_rrd` byte
* slices are independently decoded files and are therefore invalid.
*/
/** Bind the upstream Rerun HTTP receiver to one immutable source generation. */
export function resolveRecordedViewerSourceUrl(
descriptor: RecordedRrdArtifactDescriptor,
origin: string,
@@ -238,12 +234,6 @@ export function rerunViewerInitialSource(
return resolvedSourceUrl;
}
export function rerunViewerOpenOptions(
followLive: boolean,
): { follow_if_http: true } | null {
return followLive ? { follow_if_http: true } : null;
}
export function resolveRecordedBlueprintUrl(sourceUrl: string, origin: string): string | null {
const normalized = sourceUrl.trim();
if (!RECORDED_RRD_PATH.test(normalized)) return null;
@@ -365,6 +355,9 @@ export async function fetchRecordedBlueprintRrd(
activeView = "spatial",
viewResetGeneration = 0,
followTrajectory = false,
semanticLayer,
unifiedPerception,
planView = false,
perceptionLayers = {
enabled: false,
detections2d: false,
@@ -379,10 +372,15 @@ export async function fetchRecordedBlueprintRrd(
activeView?: RecordedRerunView;
viewResetGeneration?: 0 | 1;
followTrajectory?: boolean;
semanticLayer?: "city" | "vegetation";
unifiedPerception?: boolean;
planView?: boolean;
perceptionLayers?: RecordedPerceptionLayers;
fetcher?: typeof globalThis.fetch;
},
): Promise<Uint8Array> {
const resolvedUnifiedPerception =
unifiedPerception ?? (perceptionLayers.enabled && activeView !== "spatial");
const base = new URL(origin);
const endpoint = new URL(endpointUrl, base.origin);
if (
@@ -401,6 +399,9 @@ export async function fetchRecordedBlueprintRrd(
!/^#[0-9A-Fa-f]{6}$/.test(settings.customColor) ||
!["spatial", "perception", "perception3d", "metrics"].includes(activeView) ||
![0, 1].includes(viewResetGeneration) ||
(semanticLayer !== undefined && !["city", "vegetation"].includes(semanticLayer)) ||
typeof resolvedUnifiedPerception !== "boolean" ||
typeof planView !== "boolean" ||
[
perceptionLayers.enabled,
perceptionLayers.detections2d,
@@ -435,8 +436,9 @@ export async function fetchRecordedBlueprintRrd(
active_view: activeView,
view_reset_generation: viewResetGeneration,
follow_trajectory: followTrajectory,
unified_perception:
perceptionLayers.detections2d || perceptionLayers.segmentation,
unified_perception: resolvedUnifiedPerception,
semantic_layer: semanticLayer ?? null,
plan_view: planView,
show_detections_2d: perceptionLayers.detections2d,
show_segmentation: perceptionLayers.segmentation,
show_cuboids_3d: perceptionLayers.cuboids3d,
@@ -487,7 +489,8 @@ export async function fetchRecordedPerceptionRrd(
endpoint.origin !== base.origin ||
endpoint.search ||
endpoint.hash ||
!RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
!(RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
LAB_RECORDED_PERCEPTION_PATH.test(endpoint.pathname)) ||
identity.applicationId !== "nodedc_mission_core_recorded" ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId)
) {
@@ -588,6 +591,10 @@ export function RerunViewport({
segmentation: false,
cuboids3d: false,
};
const recordedPerceptionSourceUrl = recordedProfile?.perceptionSourceUrl;
const recordedSemanticLayer = recordedProfile?.semanticLayer;
const recordedUnifiedPerception = recordedProfile?.unifiedPerception ?? false;
const recordedPlanView = recordedProfile?.planView ?? false;
const recordedPerceptionRetryGeneration =
recordedProfile?.perceptionRetryGeneration ?? 0;
const lockPerceptionCameraInteraction =
@@ -621,9 +628,11 @@ export function RerunViewport({
const recordedBlueprintUrl = sourceUrl
? resolveRecordedBlueprintUrl(sourceUrl, window.location.origin)
: null;
const recordedPerceptionUrl = sourceUrl
? resolveRecordedPerceptionUrl(sourceUrl, window.location.origin)
: null;
const recordedPerceptionUrl = recordedPerceptionSourceUrl
? resolveRerunSourceUrl(recordedPerceptionSourceUrl, window.location.origin)
: sourceUrl
? resolveRecordedPerceptionUrl(sourceUrl, window.location.origin)
: null;
const recordedPointColorsUrl = sourceUrl
? resolveRecordedPointColorsUrl(sourceUrl, window.location.origin)
: null;
@@ -1264,7 +1273,7 @@ export function RerunViewport({
try {
const recordingId = viewer.get_active_recording_id();
if (!recordingId) return;
// Rerun 0.34.1 may ingest an SDK gRPC store without forwarding its
// A live SDK receiver may ingest a store without forwarding its
// recording_open event to the JavaScript wrapper. The active store
// is the authoritative fallback and avoids hiding a ready canvas.
admitRecording({
@@ -1311,11 +1320,8 @@ export function RerunViewport({
);
try {
// `panel_state_overrides` is part of Rerun's runtime AppOptions but
// omitted from the public WebViewerOptions declaration in 0.34.1.
// Its generated TypeScript declaration says `hidden`, while the
// WASM constructor actually deserializes the Rust enum `Hidden`.
// Keep the post-start overrides below as a compatibility fallback.
// Keep all application chrome in the Mission Core shell. Rerun owns
// the synchronized canvas, data store and clock, not another window.
const viewerOptions = {
width: "100%",
height: "100%",
@@ -1336,7 +1342,6 @@ export function RerunViewport({
rerunViewerInitialSource(resolvedSource),
host,
viewerOptions,
rerunViewerOpenOptions(followLive),
);
if (disposed) {
disposeViewer();
@@ -1429,6 +1434,7 @@ export function RerunViewport({
recordedArtifact?.sha256,
recordedArtifact?.sourceUrl,
recordedArtifact?.viewerSourceUrl,
recordedPerceptionUrl,
retryNonce,
]);
@@ -1484,6 +1490,7 @@ export function RerunViewport({
message: "Сервер готовит AI-слои.",
});
const pollPreparationStatus = () => {
if (!RECORDED_PERCEPTION_PATH.test(new URL(recordedPerceptionUrl).pathname)) return;
void fetchPerceptionPreparationStatus(
recordedPerceptionUrl,
identity.recordingId,
@@ -1704,6 +1711,9 @@ export function RerunViewport({
viewResetGeneration: recordedViewResetGeneration,
followTrajectory: recordedFollowTrajectory,
perceptionLayers: recordedPerceptionLayers,
semanticLayer: recordedSemanticLayer,
unifiedPerception: recordedUnifiedPerception,
planView: recordedPlanView,
}).then((payload) => {
if (
abort.signal.aborted ||
@@ -1725,6 +1735,9 @@ export function RerunViewport({
recordedView,
recordedViewResetGeneration,
recordedFollowTrajectory,
recordedSemanticLayer,
recordedUnifiedPerception,
recordedPlanView,
recordedPerceptionLayers.enabled,
recordedPerceptionLayers.detections2d,
recordedPerceptionLayers.segmentation,
@@ -9,6 +9,7 @@ export const CANONICAL_RECORDED_LAB_REPLAY_CONTRACT =
export interface CanonicalRecordedLabMode<T extends string> {
value: T;
label: string;
disabled?: boolean;
}
/**
@@ -90,6 +91,7 @@ export function CanonicalRecordedLabReplay<
mediaMultiLayer = false,
mediaContent,
spatialContent,
unifiedContent,
emptyMessage,
deckOverlays,
actions,
@@ -115,8 +117,10 @@ export function CanonicalRecordedLabReplay<
spatialLayerControls?: ReactNode;
spatialLeadingControl?: ReactNode;
mediaMultiLayer?: boolean;
mediaContent: ReactNode;
spatialContent: ReactNode;
mediaContent?: ReactNode;
spatialContent?: ReactNode;
/** One upstream Rerun viewer owns both panes and the shared playback clock. */
unifiedContent?: ReactNode;
emptyMessage: string;
deckOverlays?: ReactNode;
actions?: ReactNode;
@@ -220,7 +224,13 @@ export function CanonicalRecordedLabReplay<
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={mediaMode === "none" && spatialMode === "none" ? "true" : undefined}
data-native-rerun={unifiedContent ? "true" : undefined}
>
{unifiedContent ? (
<div className="m4-replay-threat-visual__unified-content">
{unifiedContent}
</div>
) : null}
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
@@ -37,7 +37,7 @@ export function laboratoryRecordedEvidenceDemand(
profile: LaboratoryRecordedEvidenceViewerProfile =
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
): LaboratoryRecordedEvidenceDemand {
if (profile.loadPolicy !== "visible-evidence-only") {
if (profile.loadPolicy !== "explicit-legacy-comparison-only") {
throw new Error("Unsupported LAB recorded evidence load policy");
}
const mediaVisible = visibility.mediaMode !== null;
@@ -67,20 +67,29 @@ export interface RecordedSessionRerunProfile {
viewResetGeneration: 0 | 1;
followTrajectory: boolean;
perceptionLayers: RecordedPerceptionLayers;
/** Optional immutable RRD sidecar for LAB/model evidence on the same recording clock. */
perceptionSourceUrl?: string;
/** Selects one semantic entity without changing the sealed sidecar. */
semanticLayer?: "city" | "vegetation";
/** Keeps one native Rerun store/viewer while presenting the accepted two-pane LAB layout. */
unifiedPerception?: boolean;
/** Requests the canonical top-down eye without changing the world coordinate frame. */
planView?: boolean;
perceptionRetryGeneration: number;
lockPerceptionCameraInteraction: boolean;
}
/**
* LAB recorded evidence is not a native Rerun mode. It owns a source-sequence
* clock and composes the existing sealed fMP4 and retained spatial primitives.
* Deprecated comparison-only contract for LAB artifacts that have not yet
* been republished as a native Rerun sidecar. It must never be selected by a
* canonical LAB route or start work in the background.
*/
export interface LaboratoryRecordedEvidenceViewerProfile {
kind: "lab-recorded-evidence";
clock: "source-sequence";
cameraTransport: "generation-bound-fmp4";
spatialTransport: "bounded-sealed-artifacts";
loadPolicy: "visible-evidence-only";
loadPolicy: "explicit-legacy-comparison-only";
workerRequired: false;
}
@@ -97,7 +106,7 @@ export const LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE = Object.freeze({
clock: "source-sequence",
cameraTransport: "generation-bound-fmp4",
spatialTransport: "bounded-sealed-artifacts",
loadPolicy: "visible-evidence-only",
loadPolicy: "explicit-legacy-comparison-only",
workerRequired: false,
} satisfies LaboratoryRecordedEvidenceViewerProfile);
@@ -27,6 +27,41 @@
display: block;
}
.m4-replay-threat-visual__unified-content {
position: absolute;
z-index: 1;
inset: 0;
min-width: 0;
min-height: 0;
}
.m4-replay-threat-visual__unified-content > *,
.m4-replay-threat-visual__unified-content .rerun-viewport {
width: 100%;
height: 100%;
}
.m4-replay-threat-visual__deck[data-native-rerun="true"] > .nodedc-split-pane {
position: relative;
z-index: 2;
pointer-events: none;
}
.m4-replay-threat-visual__deck[data-native-rerun="true"]
.m4-replay-threat-visual__pane {
background: transparent;
pointer-events: none;
}
.m4-replay-threat-visual__deck[data-native-rerun="true"]
.nodedc-split-pane__separator,
.m4-replay-threat-visual__deck[data-native-rerun="true"]
.m4-replay-threat-visual__pane-toolbar,
.m4-replay-threat-visual__deck[data-native-rerun="true"]
.m4-replay-threat-visual__pane-toolbar * {
pointer-events: auto;
}
.m4-replay-threat-visual__deck[data-empty="true"] > .l3-visual-audit__state {
position: absolute;
z-index: 1;
+1 -1
View File
@@ -78,7 +78,7 @@
overflow: hidden;
}
/* Rerun WebViewer 0.34.1 keeps three fixed 24px canvas rows even after its
/* The upstream Rerun canvas keeps three fixed 24px rows even after its
panels are overridden: the native top row, recording tab and view tab.
They are drawn inside WASM and cannot be styled independently, so crop the
fixed native chrome while keeping the actual 3D viewport full-height. */
@@ -0,0 +1,260 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Icon, SegmentedControl } from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
RerunViewport,
type RecordedPerceptionLoadState,
type RerunPlaybackController,
type RerunPlaybackState,
} from "../../components/RerunViewport";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
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";
const EMPTY_PERCEPTION_LOAD: RecordedPerceptionLoadState = {
phase: "idle",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: "",
};
export function CanonicalVegetationRerunReplay({
resultId,
review,
}: {
resultId: string;
review: VegetationFullRouteReview;
}) {
const {
mediaMode,
spatialMode,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange,
onExpandedChange,
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
initialMediaMode: "video",
initialSpatialMode: "3d",
});
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 [perceptionLoad, setPerceptionLoad] =
useState<RecordedPerceptionLoadState>(EMPTY_PERCEPTION_LOAD);
const [launch, setLaunch] = useState<Awaited<ReturnType<typeof resolveObservationSessionReplay>> | null>(null);
const [launchError, setLaunchError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
setLaunch(null);
setLaunchError(null);
void resolveObservationSessionReplay(review.sessionId, {
signal: controller.signal,
maximumWaitMs: 30 * 60 * 1000,
onUpdate: () => undefined,
}).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();
}, [review.sessionId]);
const splitView = mediaMode !== null && spatialMode !== null;
const sceneSettings = useMemo(() => ({
...defaultSceneSettings,
accumulationSeconds: spatialLayer === "local" ? 5 : 0,
showPoints: spatialMode !== null,
showTrajectory: spatialMode !== null,
showGrid: spatialMode !== null,
pointSize: 2.2,
}), [spatialLayer, spatialMode]);
const profile = launch ? recordedSessionRerunProfile({
sourceUrl: launch.sourceUrl,
artifact: {
sourceUrl: launch.sourceUrl,
viewerSourceUrl: launch.viewerSourceUrl,
byteLength: launch.byteLength,
sha256: launch.sha256,
},
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: launch.timelineStartSeconds,
expectedTimelineEndSeconds: launch.timelineEndSeconds,
initialPlaybackStartSeconds: review.timelineStartSeconds,
view: mediaMode !== null ? "perception" : "spatial",
viewResetGeneration,
followTrajectory: false,
perceptionSourceUrl:
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}` +
"/canonical-overlay.rrd",
semanticLayer,
unifiedPerception: splitView,
planView: spatialMode === "plan",
perceptionLayers: {
enabled: mediaMode !== null,
detections2d: mediaMode === "video",
segmentation: mediaMode === "video" && showSemantics,
cuboids3d: false,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: false,
}) : null;
const mediaLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<Button
size="compact"
shape="pill"
variant={showSemantics ? "primary" : "secondary"}
aria-pressed={showSemantics}
onClick={() => setShowSemantics((visible) => !visible)}
>
SEMANTICS
</Button>
<SegmentedControl
value={semanticLayer}
items={[
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
]}
label="Источник семантики"
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: "SOURCE POINTS" },
{ value: "local", label: "LOCAL SLAM" },
{ value: "tgs", label: "TGS COSTMAP", disabled: true },
{ value: "semantic", label: "SEMANTICS", disabled: true },
]}
label="Пространственные слои"
onChange={setSpatialLayer}
/>
</div>
);
const resetSpatialView = (
<Button
size="compact"
variant="ghost"
icon={<Icon name="refresh" size={14} />}
aria-label="Сбросить положение 3D камеры"
title="Сбросить положение 3D камеры"
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}
>
</Button>
);
const transport = playback && playbackController ? (
<ObservationTimeline
className="m4-replay-threat-visual__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;
const overlayMessage = launchError
?? (perceptionLoad.phase === "loading" ? perceptionLoad.message : null)
?? (perceptionLoad.phase === "error" ? perceptionLoad.message : null);
return (
<CanonicalRecordedLabReplay
label="RAVNOVES004TREE · upstream Rerun recorded replay"
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео и семантика"}
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer
unifiedContent={profile ? (
<RerunViewport
profile={profile}
sceneSettings={sceneSettings}
onPlaybackChange={setPlayback}
onPlaybackControllerChange={setPlaybackController}
onPerceptionLoadChange={setPerceptionLoad}
/>
) : (
<div className="l3-visual-audit__state" role={launchError ? "alert" : "status"}>
{launchError ?? "Открываем каноническую запись RAV004…"}
</div>
)}
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий Rerun-clock останется на месте."
deckOverlays={overlayMessage ? (
<div className="m4-replay-threat-visual__buffering" role="status">
{perceptionLoad.phase === "loading" ? (
<span className="busy-indicator" aria-hidden="true" />
) : (
<Icon name="alert" size={14} />
)}
<span>{overlayMessage}</span>
</div>
) : undefined}
transport={transport}
onMediaModeChange={onMediaModeChange}
onSpatialModeChange={onSpatialModeChange}
onExpandedChange={onExpandedChange}
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
/>
);
}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import {
LaboratoryEvidence,
@@ -7,7 +7,6 @@ import {
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import {
vegetationFullRouteMaskUrl,
vegetationVideoMaskUrl,
type VegetationFullRouteReview,
type VegetationShadowResult,
@@ -17,13 +16,7 @@ import {
type M49TgsFullShadowResult,
} from "../../core/laboratory/m49TgsFullShadow";
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
import {
M4ReplayThreatVisual,
type M4ReplayClassifiedSpatialLayer,
type M4ReplayThreatSemanticLayer,
} from "./M4ReplayThreatVisual";
const VEGETATION_TIMELINE_ENDPOINT = "/api/v1/laboratory/vegetation-shadow";
import { CanonicalVegetationRerunReplay } from "./CanonicalVegetationRerunReplay";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
@@ -36,56 +29,7 @@ function FullRouteReviewEvidence({
resultId: string;
review: VegetationFullRouteReview;
}) {
const semanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(() => ([
{
id: "city",
controlLabel: "ГОРОД · EoMT",
resultId,
spatialResultId: null,
taxonomy: review.city.taxonomy,
maskUrl: (sequence) => vegetationFullRouteMaskUrl(resultId, "city", sequence),
label: review.city.name,
maskAriaLabel: "EoMT city semantic prediction",
},
{
id: "vegetation",
controlLabel: "ПРИРОДА · DDRNet",
resultId,
spatialResultId: null,
taxonomy: review.vegetation.taxonomy,
maskUrl: (sequence) => vegetationFullRouteMaskUrl(resultId, "vegetation", sequence),
label: review.vegetation.name,
maskAriaLabel: "DDRNet nature semantic prediction",
},
]), [resultId, review.city, review.vegetation]);
const sealedSpatialGap = useMemo<M4ReplayClassifiedSpatialLayer>(() => ({
label: "RAVNOVES004TREE",
pointLayerLabel: "SOURCE POINTS",
cellLayerLabel: "TGS COSTMAP",
cellLayerAvailable: false,
expectedAtSequence: false,
frame: null,
loading: false,
error: null,
replacePointCloud: false,
}), []);
return (
<M4ReplayThreatVisual
resultId={resultId}
timelineEndpointRoot={VEGETATION_TIMELINE_ENDPOINT}
semanticLayers={semanticLayers}
initialSemanticLayerId="vegetation"
initialSpatialMode="3d"
classifiedSpatialLayer={sealedSpatialGap}
evidenceLabel="RAVNOVES004TREE"
playbackTransport="segmented"
spatialPlaybackTransport="sealed-binary"
recoverTimestampStalls
showReferenceMediaLayers
showSpatialOverlaySummary
/>
);
return <CanonicalVegetationRerunReplay resultId={resultId} review={review} />;
}
function FullRouteReviewResult({