refactor(lab): перевести RAV004 на канонический Rerun pipeline
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user