fix(lab): enforce canonical replay runtime

This commit is contained in:
DCCONSTRUCTIONS
2026-08-29 22:42:30 +03:00
parent 525ab74168
commit bd2892140f
17 changed files with 1765 additions and 507 deletions
@@ -781,6 +781,8 @@ export function RecordedFmp4Player({
onAdmissionChange,
onPlaybackChange,
onPlayingRejected,
playbackAuthority = "media",
playbackTransport = "segmented",
}: {
source: ObservationSourceDescriptor;
playback?: RecordedObservationPlayback | null;
@@ -793,6 +795,8 @@ export function RecordedFmp4Player({
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
onPlayingRejected?: () => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
}) {
const videoRef = useRef<HTMLVideoElement>(null);
const onAdmissionChangeRef = useRef(onAdmissionChange);
@@ -888,7 +892,8 @@ export function RecordedFmp4Player({
&& effectiveSegmentSequence !== requestedSegmentSequence,
);
const segmented = Boolean(
requestedSegmentSequence !== null
playbackTransport === "segmented"
&& requestedSegmentSequence !== null
&& Number.isInteger(requestedSegmentSequence)
&& requestedSegmentSequence >= 1
&&
@@ -1201,6 +1206,11 @@ export function RecordedFmp4Player({
|| segmentedRuntimeRef.current !== runtime
|| runtime.target?.revision !== revision
) return;
// Canonical recorded LABs run one host-owned clock for camera and
// spatial evidence. A transient MSE play() rejection (commonly a
// pause/reset race while the next fragment is admitted) must stay a
// decoder concern: the rolling target will retry and catch up.
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
setReadyGeneration(null);
setErrorMessage("Запуск записанной камеры отклонён браузером.");
@@ -1311,7 +1321,13 @@ export function RecordedFmp4Player({
if (targetReadyAbortRef.current === targetReadyAbort) targetReadyAbortRef.current = null;
if (runtime.onTargetBuffered === markBuffered) runtime.onTargetBuffered = null;
};
}, [archive?.byteLength, effectiveSegmentSequence, segmented, segmentedRuntimeGeneration]);
}, [
archive?.byteLength,
effectiveSegmentSequence,
playbackAuthority,
segmented,
segmentedRuntimeGeneration,
]);
useEffect(() => {
const video = videoRef.current;
@@ -1407,6 +1423,7 @@ export function RecordedFmp4Player({
if (playback?.playing && !holdingForSegmentRecovery) {
void video.play().catch(() => {
if (playAttemptRevisionRef.current !== playAttemptRevision) return;
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
setReadyGeneration(null);
setErrorMessage("Запуск записанной камеры отклонён браузером.");
@@ -1427,6 +1444,7 @@ export function RecordedFmp4Player({
epoch,
holdingForSegmentRecovery,
playback?.playing,
playbackAuthority,
playbackRate,
segmented,
visualState,
@@ -1454,6 +1472,7 @@ export function RecordedFmp4Player({
video.playbackRate = playbackRateRef.current;
void video.play().catch(() => {
if (cancelled || playAttemptRevisionRef.current !== playAttemptRevision) return;
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
});
};
@@ -1465,7 +1484,7 @@ export function RecordedFmp4Player({
cancelled = true;
for (const event of events) video.removeEventListener(event, queueResume);
};
}, [bufferRevision, playback?.playing, segmented, visualState]);
}, [bufferRevision, playback?.playing, playbackAuthority, segmented, visualState]);
useEffect(() => {
const video = videoRef.current;
@@ -0,0 +1,245 @@
import { useCallback, useEffect, useState, type ReactNode } from "react";
import { SegmentedControl, SplitPane, type SplitPaneOrientation } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "./LaboratoryEvidenceViewer";
export const CANONICAL_RECORDED_LAB_REPLAY_CONTRACT =
"missioncore.canonical-recorded-lab-replay/v1";
export interface CanonicalRecordedLabMode<T extends string> {
value: T;
label: string;
}
/**
* The interaction contract from the accepted recorded-LAB instrument.
*
* Keeping pane selection, collapse semantics, responsive split orientation,
* expansion and splitter state here prevents individual experiments from
* quietly growing their own replay behaviour. Experiments provide evidence
* layers; they do not reimplement the laboratory shell.
*/
export function useCanonicalRecordedLabReplayState<
TMedia extends string,
TSpatial extends string,
>({
initialMediaMode,
initialSpatialMode,
}: {
initialMediaMode: TMedia;
initialSpatialMode: TSpatial | null;
}) {
const [mediaMode, setMediaMode] = useState<TMedia | null>(initialMediaMode);
const [spatialMode, setSpatialMode] = useState<TSpatial | null>(initialSpatialMode);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
? "horizontal"
: "vertical"
));
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const query = window.matchMedia("(max-width: 900px)");
const update = () => setSplitOrientation(query.matches ? "horizontal" : "vertical");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
const onMediaModeChange = useCallback((next: TMedia | "none") => {
if (next === "none") return;
setMediaMode((current) => current === next ? null : next);
}, []);
const onSpatialModeChange = useCallback((next: TSpatial | "none") => {
if (next === "none") return;
setSpatialMode((current) => current === next ? null : next);
}, []);
return {
mediaMode,
spatialMode,
splitView: mediaMode !== null && spatialMode !== null,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
};
}
export function CanonicalRecordedLabReplay<
TMedia extends string,
TSpatial extends string,
>({
label,
mediaMode,
mediaModes,
spatialMode,
spatialModes,
expanded,
splitPrimarySize,
splitOrientation,
mediaAriaLabel,
spatialAriaLabel,
mediaLayerControls,
spatialLayerControls,
spatialLeadingControl,
mediaMultiLayer = false,
mediaContent,
spatialContent,
emptyMessage,
deckOverlays,
actions,
overlay,
transport,
trailingActions,
onMediaModeChange,
onSpatialModeChange,
onExpandedChange,
onSplitPrimarySizeChange,
}: {
label: string;
mediaMode: TMedia;
mediaModes: readonly CanonicalRecordedLabMode<TMedia>[];
spatialMode: TSpatial;
spatialModes: readonly CanonicalRecordedLabMode<TSpatial>[];
expanded: boolean;
splitPrimarySize: number;
splitOrientation: SplitPaneOrientation;
mediaAriaLabel: string;
spatialAriaLabel: string;
mediaLayerControls?: ReactNode;
spatialLayerControls?: ReactNode;
spatialLeadingControl?: ReactNode;
mediaMultiLayer?: boolean;
mediaContent: ReactNode;
spatialContent: ReactNode;
emptyMessage: string;
deckOverlays?: ReactNode;
actions?: ReactNode;
overlay?: ReactNode;
transport?: ReactNode;
trailingActions?: ReactNode;
onMediaModeChange: (mode: TMedia) => void;
onSpatialModeChange: (mode: TSpatial) => void;
onExpandedChange: (expanded: boolean) => void;
onSplitPrimarySizeChange: (size: number) => void;
}) {
const splitView = mediaMode !== "none" && spatialMode !== "none";
const mediaModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="media">
<SegmentedControl
value={mediaMode}
items={[...mediaModes]}
label="Видео и камера"
onChange={onMediaModeChange}
/>
</div>
);
const spatialModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="spatial">
<SegmentedControl
value={spatialMode}
items={[...spatialModes]}
label="3D и план"
onChange={onSpatialModeChange}
/>
</div>
);
const mediaPane = (
<section
className="m4-replay-threat-visual__pane"
data-pane="media"
aria-label={mediaAriaLabel}
hidden={mediaMode === "none"}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={mediaMultiLayer ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
{mediaContent}
</section>
);
const spatialPane = spatialMode !== "none" ? (
<section
className="m4-replay-threat-visual__pane"
data-pane="spatial"
aria-label={spatialAriaLabel}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
>
{spatialLeadingControl}
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
</div>
</div>
) : null}
{spatialContent}
</section>
) : null;
return (
<div
className="l3-visual-audit m4-replay-threat-visual"
data-contract={CANONICAL_RECORDED_LAB_REPLAY_CONTRACT}
>
<LaboratoryEvidenceViewer
label={label}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode}
modes={[...mediaModes]}
secondaryMode={{
value: spatialMode,
modes: [...spatialModes],
label: "3D и план",
onChange: onSpatialModeChange,
}}
expanded={expanded}
onModeChange={onMediaModeChange}
onExpandedChange={onExpandedChange}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
>
<div
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={mediaMode === "none" && spatialMode === "none" ? "true" : undefined}
>
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
primarySize={splitView ? splitPrimarySize : mediaMode !== "none" ? 100 : 0}
onPrimarySizeChange={onSplitPrimarySizeChange}
orientation={splitOrientation}
minPrimarySize={splitView ? 24 : 0}
minSecondarySize={splitView ? 24 : 0}
resizable={splitView}
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
/>
{mediaMode === "none" && spatialMode === "none" ? (
<div className="l3-visual-audit__state" role="status">
{emptyMessage}
</div>
) : null}
{deckOverlays}
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -81,9 +81,7 @@ export function LaboratoryRecordedClipPlayer({
sourceCount,
cameraRef,
cameraOverlay,
cameraControls,
alternativeScene,
spatialControls,
onSequenceChange,
onPlayingChange,
onPlaybackRateChange,
@@ -99,9 +97,7 @@ export function LaboratoryRecordedClipPlayer({
sourceCount: number;
cameraRef?: RefObject<HTMLDivElement | null>;
cameraOverlay?: ReactNode;
cameraControls?: ReactNode;
alternativeScene?: ReactNode;
spatialControls?: ReactNode;
onSequenceChange: (sequence: number) => void;
onPlayingChange: (playing: boolean) => void;
onPlaybackRateChange: (rate: number) => void;
@@ -177,14 +173,6 @@ export function LaboratoryRecordedClipPlayer({
aria-hidden={cameraPresentation === "primary"}
>
{alternativeScene}
{spatialControls ? (
<div
className="laboratory-recorded-clip-player__pane-controls"
data-pane="spatial"
>
{spatialControls}
</div>
) : null}
</div>
);
const cameraPane = (
@@ -207,14 +195,6 @@ export function LaboratoryRecordedClipPlayer({
/>
) : null}
{cameraPresentation !== "hidden" ? cameraOverlay : null}
{cameraPresentation !== "hidden" && cameraControls ? (
<div
className="laboratory-recorded-clip-player__pane-controls"
data-pane="camera"
>
{cameraControls}
</div>
) : null}
</div>
);
return (
@@ -33,6 +33,8 @@ export function RecordedEvidenceVideoScene({
segmentCount,
onPlaybackChange,
onPlayingRejected,
playbackAuthority = "media",
playbackTransport = "segmented",
}: {
source: ObservationSourceDescriptor;
playback: RecordedObservationPlayback;
@@ -47,6 +49,8 @@ export function RecordedEvidenceVideoScene({
segmentCount?: number;
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
onPlayingRejected?: () => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
}) {
return (
<div className="recorded-evidence-video-scene">
@@ -59,6 +63,8 @@ export function RecordedEvidenceVideoScene({
segmentCount={segmentCount}
onPlaybackChange={onPlaybackChange}
onPlayingRejected={onPlayingRejected}
playbackAuthority={playbackAuthority}
playbackTransport={playbackTransport}
/>
{semanticOverlay ? (
<RecordedEvidenceSemanticMaskOverlay
@@ -130,8 +130,12 @@ export function useRecordedEvidencePlayback(
const synchronize = useCallback((next: RecordedObservationPlayback) => {
if (!validRange(range) || !Number.isFinite(next.currentSeconds)) return;
// The canonical LAB host clock is authoritative. Native media callbacks
// are observational only in this mode: a stalled decoder must never stop
// the common timeline or let an independently playing spatial view drift.
if (clock === "animation") return;
setPlayback((current) => synchronizeRecordedEvidencePlayback(current, next, range));
}, [range]);
}, [clock, range]);
return useMemo(() => ({
playback,
@@ -106,6 +106,36 @@ export interface VegetationMixedRouteReview {
cases: readonly VegetationMixedRouteCase[];
}
export interface VegetationRouteTgsAnchor {
sourceSequence: number;
slot: number;
currentPointsXyzM: readonly (readonly [number, number, number])[];
costmap: {
cellSizeM: 0.45;
centersXyM: readonly (readonly [number, number])[];
stateCodes: readonly number[];
zBoundsM: readonly (readonly [number | null, number | null])[];
};
}
export interface CanonicalRecordedLabSpatialFrame {
targetTimeNs: number;
sourceTimeNs: number;
poseTimeNs: number;
trajectoryTimeNs: number;
sourcePointCount: number;
bodyFrame: {
originMapXyzM: readonly [number, number, number];
basisMapFromBody: readonly [
readonly [number, number, number],
readonly [number, number, number],
readonly [number, number, number],
];
};
sourcePointsBodyXyzM: readonly (readonly [number, number, number])[];
localSlamBodyXyzM: readonly (readonly [number, number, number])[];
}
export interface VegetationFullRouteLayer {
name: string;
resultId: string;
@@ -942,6 +972,172 @@ export function vegetationFullRouteMaskUrl(
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-masks/${layer}/${sequence}`;
}
export async function fetchVegetationRouteTgsAnchor(
resultId: string,
sourceSequence: number,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<VegetationRouteTgsAnchor> {
if (!RESULT_ID.test(resultId) || !Number.isInteger(sourceSequence) || sourceSequence < 1) {
throw new VegetationShadowContractError("Vegetation TGS anchor identity недопустима.");
}
const response = await fetcher(
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`
+ `/route-tgs-anchor/${sourceSequence}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new VegetationShadowContractError(`Vegetation TGS anchor недоступен: HTTP ${response.status}.`);
}
const payload = objectValue(await response.json(), "vegetation.route_tgs_anchor");
exact(
payload.schema_version,
"missioncore.lab-v1-route-tgs-anchor/v1",
"vegetation.route_tgs_anchor.schema_version",
);
exact(payload.source_sequence, sourceSequence, "vegetation.route_tgs_anchor.source_sequence");
const pointValue = (value: unknown, label: string): readonly number[] => {
const point = arrayValue(value, label).map((item, index) => numberValue(item, `${label}[${index}]`));
if (point.length !== 2 && point.length !== 3) {
throw new VegetationShadowContractError(`${label}: размер изменён.`);
}
return point;
};
const points = arrayValue(payload.current_points_xyz_m, "vegetation.route_tgs_anchor.points")
.map((value, index) => pointValue(value, `vegetation.route_tgs_anchor.points[${index}]`));
const costmap = objectValue(payload.costmap, "vegetation.route_tgs_anchor.costmap");
exact(costmap.cell_size_m, 0.45, "vegetation.route_tgs_anchor.costmap.cell_size_m");
const centers = arrayValue(costmap.centers_xy_m, "vegetation.route_tgs_anchor.costmap.centers")
.map((value, index) => pointValue(value, `vegetation.route_tgs_anchor.costmap.centers[${index}]`));
const stateCodes = arrayValue(costmap.state_codes, "vegetation.route_tgs_anchor.costmap.states")
.map((value, index) => integerValue(value, `vegetation.route_tgs_anchor.costmap.states[${index}]`));
const zBounds = arrayValue(costmap.z_bounds_m, "vegetation.route_tgs_anchor.costmap.z_bounds")
.map((value, index) => {
const row = arrayValue(value, `vegetation.route_tgs_anchor.costmap.z_bounds[${index}]`);
if (row.length !== 2 || row.some((item) => item !== null && (typeof item !== "number" || !Number.isFinite(item)))) {
throw new VegetationShadowContractError("vegetation.route_tgs_anchor.costmap.z_bounds: контракт изменён.");
}
return row as readonly [number | null, number | null];
});
if (
centers.length !== 2244
|| stateCodes.length !== 2244
|| zBounds.length !== 2244
|| stateCodes.some((value) => value > 3)
|| points.some((point) => point.length !== 3)
|| centers.some((point) => point.length !== 2)
) {
throw new VegetationShadowContractError("Vegetation TGS anchor shape изменён.");
}
return {
sourceSequence,
slot: integerValue(payload.slot, "vegetation.route_tgs_anchor.slot"),
currentPointsXyzM: points.map((point) => [point[0]!, point[1]!, point[2]!] as const),
costmap: {
cellSizeM: 0.45,
centersXyM: centers.map((point) => [point[0]!, point[1]!] as const),
stateCodes,
zBoundsM: zBounds,
},
};
}
export async function fetchCanonicalRecordedLabSpatialFrame(
sessionId: string,
generationSha256: string,
targetTimeNs: number,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<CanonicalRecordedLabSpatialFrame> {
if (
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(sessionId)
|| !SHA256.test(generationSha256)
|| !Number.isSafeInteger(targetTimeNs)
|| targetTimeNs < 0
) {
throw new VegetationShadowContractError("Canonical LAB spatial identity недопустима.");
}
const query = new URLSearchParams({
generation: generationSha256,
time_ns: String(targetTimeNs),
});
const response = await fetcher(
`/api/v1/observation-sessions/${encodeURIComponent(sessionId)}`
+ `/canonical-lab/spatial-frame?${query.toString()}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new VegetationShadowContractError(
`Canonical LAB spatial frame недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "canonical_lab.spatial_frame");
exact(
payload.schema_version,
"missioncore.canonical-recorded-lab-spatial-frame/v1",
"canonical_lab.spatial_frame.schema_version",
);
exact(payload.target_time_ns, targetTimeNs, "canonical_lab.spatial_frame.target_time_ns");
const pointList = (value: unknown, label: string) => arrayValue(value, label).map(
(entry, index) => {
const point = arrayValue(entry, `${label}[${index}]`).map(
(channel, channelIndex) => numberValue(channel, `${label}[${index}][${channelIndex}]`),
);
if (point.length !== 3) {
throw new VegetationShadowContractError(`${label}[${index}]: размер изменён.`);
}
return [point[0]!, point[1]!, point[2]!] as const;
},
);
const sourcePoints = pointList(
payload.source_points_body_xyz_m,
"canonical_lab.spatial_frame.source_points",
);
const localSlam = pointList(
payload.local_slam_body_xyz_m,
"canonical_lab.spatial_frame.local_slam",
);
const sourcePointCount = integerValue(
payload.source_point_count,
"canonical_lab.spatial_frame.source_point_count",
);
if (sourcePointCount !== sourcePoints.length || sourcePointCount > 100_000 || localSlam.length > 10_000) {
throw new VegetationShadowContractError("Canonical LAB spatial accounting изменён.");
}
const bodyFrame = objectValue(payload.body_frame, "canonical_lab.spatial_frame.body_frame");
const origin = pointList(
[bodyFrame.origin_map_xyz_m],
"canonical_lab.spatial_frame.body_frame.origin",
)[0]!;
const basisRows = pointList(
bodyFrame.basis_map_from_body,
"canonical_lab.spatial_frame.body_frame.basis",
);
if (basisRows.length !== 3) {
throw new VegetationShadowContractError("Canonical LAB spatial basis изменён.");
}
return {
targetTimeNs,
sourceTimeNs: integerValue(payload.source_time_ns, "canonical_lab.spatial_frame.source_time_ns"),
poseTimeNs: integerValue(payload.pose_time_ns, "canonical_lab.spatial_frame.pose_time_ns"),
trajectoryTimeNs: integerValue(
payload.trajectory_time_ns,
"canonical_lab.spatial_frame.trajectory_time_ns",
),
sourcePointCount,
bodyFrame: {
originMapXyzM: origin,
basisMapFromBody: [basisRows[0]!, basisRows[1]!, basisRows[2]!],
},
sourcePointsBodyXyzM: sourcePoints,
localSlamBodyXyzM: localSlam,
};
}
export async function fetchVegetationShadowResult(
resultId: string,
{
@@ -33,29 +33,6 @@
pointer-events: auto;
}
.laboratory-recorded-clip-player__pane-controls {
position: absolute;
z-index: 6;
top: 0.6rem;
display: flex;
max-width: calc(100% - 1.2rem);
flex-wrap: wrap;
align-items: center;
gap: 0.38rem;
border-radius: var(--nodedc-radius-control-pill);
background: var(--nodedc-floating-surface);
padding: 0.28rem;
backdrop-filter: blur(var(--nodedc-blur-control));
}
.laboratory-recorded-clip-player__pane-controls[data-pane="spatial"] {
right: 0.6rem;
}
.laboratory-recorded-clip-player__pane-controls[data-pane="camera"] {
left: 0.6rem;
}
.laboratory-recorded-clip-player__split
> .nodedc-split-pane__separator::before {
background: transparent;
@@ -1,15 +1,25 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import {
Button,
Icon,
IconButton,
Select,
SegmentedControl,
SplitPane,
type SplitPaneOrientation,
} from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricCellEvidence,
@@ -53,8 +63,6 @@ import { useE47SemanticTimelineFrame } from "./useE47SemanticTimeline";
import { buildM4StaticObstacleBoxes } from "./m4StaticObstacleBoxes";
type M4ThreatMediaMode = "video" | "camera";
type M4ThreatMediaSelection = M4ThreatMediaMode | "none";
type M4ThreatSpatialSelection = LaboratoryMetricSceneMode | "none";
function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox["tone"] {
if (proposal.threatDecision === "threat") return "danger";
@@ -188,10 +196,21 @@ export function M4ReplayThreatVisual({
showSpatialOverlaySummary?: boolean;
onActiveSequenceChange?: (sequence: number | null) => void;
}) {
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(
const {
mediaMode,
spatialMode,
splitView,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange: handleMediaModeChange,
onSpatialModeChange: handleSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
} = useCanonicalRecordedLabReplayState<M4ThreatMediaMode, LaboratoryMetricSceneMode>({
initialMediaMode: "video",
initialSpatialMode,
);
});
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
const [showLocalSurface, setShowLocalSurface] = useState(true);
const [showRollingMap, setShowRollingMap] = useState(true);
@@ -200,13 +219,6 @@ export function M4ReplayThreatVisual({
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
const [showMediaPoints, setShowMediaPoints] = useState(false);
const [showStaticObstacles, setShowStaticObstacles] = useState(true);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
? "horizontal"
: "vertical"
));
const [expanded, setExpanded] = useState(false);
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
const availableSemanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(
() => semanticLayers?.length ? semanticLayers : semantic ? [semantic] : [],
@@ -266,7 +278,7 @@ export function M4ReplayThreatVisual({
endSeconds: metadata.timeline.timelineEndSeconds,
}) : null, [metadata.timeline]);
const playbackController = useRecordedEvidencePlayback(playbackRange, {
clock: mediaMode === "video" ? "external" : "animation",
clock: "animation",
});
const seekPlayback = playbackController.seek;
const setPlaybackPlaying = playbackController.setPlaying;
@@ -286,14 +298,6 @@ export function M4ReplayThreatVisual({
setVideoError(null);
}, [resultId]);
useEffect(() => {
const query = window.matchMedia("(max-width: 900px)");
const update = () => setSplitOrientation(query.matches ? "horizontal" : "vertical");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
useEffect(() => {
const timeline = metadata.timeline;
if (!evidenceDemand.recordedVideo) {
@@ -739,15 +743,6 @@ export function M4ReplayThreatVisual({
}
: undefined;
const handleMediaModeChange = (next: M4ThreatMediaSelection) => {
if (next === "none") return;
setMediaMode((current) => current === next ? null : next);
};
const handleSpatialModeChange = (next: M4ThreatSpatialSelection) => {
if (next === "none") return;
setSpatialMode((current) => current === next ? null : next);
};
useEffect(() => {
if (
playbackController.playback.playing
@@ -758,36 +753,6 @@ export function M4ReplayThreatVisual({
image.src = frame.cameraUrl;
}, [evidenceDemand.exactCameraFrame, frame?.cameraUrl, playbackController.playback.playing]);
const splitView = mediaMode !== null && spatialMode !== null;
const mediaModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="media">
<SegmentedControl
value={mediaMode ?? "none"}
items={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
label="Видео и камера"
onChange={handleMediaModeChange}
/>
</div>
);
const spatialModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="spatial">
<SegmentedControl
value={spatialMode ?? "none"}
items={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
label="3D и план"
onChange={handleSpatialModeChange}
/>
</div>
);
const mediaLayerControls = activeSemantic
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
|| (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
@@ -1110,7 +1075,12 @@ export function M4ReplayThreatVisual({
) : undefined;
const timeline = metadata.timeline;
let content;
let content: ReactNode = null;
let canonicalContent: {
mediaContent: ReactNode;
spatialContent: ReactNode;
deckOverlays: ReactNode;
} | null = null;
if (metadata.error) {
content = <SpatialState message={metadata.error} />;
} else if (!timeline) {
@@ -1121,23 +1091,8 @@ export function M4ReplayThreatVisual({
</div>
);
} else {
const mediaPane = (
<section
className="m4-replay-threat-visual__pane"
data-pane="media"
aria-label={mediaMode === "camera" ? "Камера" : "Видео"}
hidden={!mediaMode}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={availableSemanticLayers.length > 1 ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
const mediaContent = (
<>
<div
className="m4-replay-threat-visual__media-layer"
data-media="video"
@@ -1161,7 +1116,8 @@ export function M4ReplayThreatVisual({
}
segmentCount={timeline.frameCount}
onPlaybackChange={playbackController.synchronize}
onPlayingRejected={() => playbackController.setPlaying(false)}
playbackAuthority="host"
playbackTransport="epoch-stream"
/>
) : videoError ? (
<SpatialState message={videoError} />
@@ -1183,27 +1139,11 @@ export function M4ReplayThreatVisual({
ariaLabel={`${evidenceLabel} exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
/>
) : null}
</section>
</>
);
const spatialPane = spatialMode ? (
<section
className="m4-replay-threat-visual__pane"
data-pane="spatial"
aria-label={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
>
{resetSpatialView}
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
</div>
</div>
) : null}
const spatialContent = spatialMode ? (
<>
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
@@ -1265,31 +1205,11 @@ export function M4ReplayThreatVisual({
: `На кадре ${frame.sequence + 1} нет body frame; ждём первый квалифицированный spatial evidence.`}
</div>
) : null}
</section>
</>
) : null;
content = (
<div
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={!mediaMode && !spatialMode ? "true" : undefined}
>
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
primarySize={splitView ? splitPrimarySize : mediaMode ? 100 : 0}
onPrimarySizeChange={setSplitPrimarySize}
orientation={splitOrientation}
minPrimarySize={splitView ? 24 : 0}
minSecondarySize={splitView ? 24 : 0}
resizable={splitView}
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
/>
{!mediaMode && !spatialMode ? (
<div className="l3-visual-audit__state" role="status">
Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте.
</div>
) : null}
const deckOverlays = (
<>
{timelineFrame.loading || displayingBufferedFrame ? (
<div className="m4-replay-threat-visual__buffering" role="status">
<span className="busy-indicator" aria-hidden="true" />
@@ -1320,8 +1240,9 @@ export function M4ReplayThreatVisual({
<span>{semanticIntegrityError}</span>
</div>
) : null}
</div>
</>
);
canonicalContent = { mediaContent, spatialContent, deckOverlays };
}
const transport = timeline ? (
@@ -1346,38 +1267,59 @@ export function M4ReplayThreatVisual({
/>
) : undefined;
if (!timeline || !canonicalContent) {
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={`${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode="video"
modes={[{ value: "video", label: "VIDEO" }]}
expanded={expanded}
onModeChange={() => undefined}
onExpandedChange={setExpanded}
>
{content}
</LaboratoryEvidenceViewer>
</div>
);
}
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={activeSemantic
? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode ?? "none"}
modes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
secondaryMode={{
value: spatialMode ?? "none",
modes: [
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
],
label: "3D и план",
onChange: handleSpatialModeChange,
}}
expanded={expanded}
onModeChange={handleMediaModeChange}
onExpandedChange={setExpanded}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
>
{content}
</LaboratoryEvidenceViewer>
</div>
<CanonicalRecordedLabReplay
label={activeSemantic
? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime 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 === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer={availableSemanticLayers.length > 1}
mediaContent={canonicalContent.mediaContent}
spatialContent={canonicalContent.spatialContent}
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте."
deckOverlays={canonicalContent.deckOverlays}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
onMediaModeChange={handleMediaModeChange}
onSpatialModeChange={handleSpatialModeChange}
onExpandedChange={setExpanded}
onSplitPrimarySizeChange={setSplitPrimarySize}
/>
);
}
@@ -1,9 +1,29 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button, SegmentedControl } from "@nodedc/ui-react";
import {
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from "react";
import {
Button,
Icon,
IconButton,
SegmentedControl,
} from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
import { RerunViewport } from "../../components/RerunViewport";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricEvidenceSceneHandle,
type LaboratoryMetricPackedCellEvidence,
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
import { RecordedEvidenceVideoScene } from "../../components/laboratory/RecordedEvidenceVideoScene";
import { useRecordedEvidencePlayback } from "../../components/laboratory/useRecordedEvidencePlayback";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
@@ -11,18 +31,21 @@ import {
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import {
RecordedEvidenceSemanticMaskOverlay,
type RecordedEvidenceSemanticClass,
type RecordedEvidenceSemanticPaletteEntry,
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
import {
fetchCanonicalRecordedLabSpatialFrame,
fetchVegetationShadowResult,
fetchVegetationRouteTgsAnchor,
vegetationFullRouteMaskUrl,
vegetationVideoMaskUrl,
type CanonicalRecordedLabSpatialFrame,
type VegetationFullRouteLayer,
type VegetationFullRouteReview,
type VegetationMixedRouteCase,
type VegetationMixedRouteReview,
type VegetationRouteTgsAnchor,
type VegetationShadowResult,
} from "../../core/laboratory/vegetationShadow";
import {
@@ -33,16 +56,7 @@ import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import {
recordedSessionRerunProfile,
type RerunPlaybackController,
} from "../../core/observation/viewerProfile";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import { defaultSceneSettings, type SceneSettings } from "../../sceneSettings";
import {
M48EvidenceModeRail,
type M48BlindEvidenceMode,
} from "./annotation/M48EvidenceModeControls";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
@@ -53,6 +67,19 @@ const FULL_ROUTE_SEMANTIC_MODES = [
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
] as const;
type FullRouteMediaMode = "video" | "camera";
type FullRouteSpatialMode = "3d" | "plan";
const FULL_ROUTE_MEDIA_MODES = [
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
] as const;
const FULL_ROUTE_SPATIAL_MODES = [
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
] as const;
function semanticPresentation(layer: VegetationFullRouteLayer): {
classes: readonly RecordedEvidenceSemanticClass[];
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
@@ -68,17 +95,125 @@ function semanticPresentation(layer: VegetationFullRouteLayer): {
};
}
function nearestTgsCase(
function causalTgsCase(
cases: readonly VegetationMixedRouteCase[],
sequence: number,
): VegetationMixedRouteCase | null {
return cases.reduce<VegetationMixedRouteCase | null>((nearest, candidate) => (
!nearest
|| Math.abs(candidate.sourceSequence - sequence)
< Math.abs(nearest.sourceSequence - sequence)
if (!cases.length) return null;
return cases.reduce<VegetationMixedRouteCase | null>((latest, candidate) => (
candidate.sourceSequence <= sequence
&& (!latest || candidate.sourceSequence > latest.sourceSequence)
? candidate
: nearest
), null);
: latest
), null) ?? cases.reduce((first, candidate) => (
candidate.sourceSequence < first.sourceSequence ? candidate : first
));
}
function nearestFullRouteFrameIndex(
frameSourceTimesNs: readonly number[],
sourceTimeNs: number,
): number {
if (!frameSourceTimesNs.length) return 0;
let low = 0;
let high = frameSourceTimesNs.length - 1;
while (low < high) {
const middle = Math.floor((low + high) / 2);
if ((frameSourceTimesNs[middle] ?? 0) < sourceTimeNs) low = middle + 1;
else high = middle;
}
if (low === 0) return 0;
const previous = frameSourceTimesNs[low - 1] ?? frameSourceTimesNs[0] ?? 0;
const current = frameSourceTimesNs[low] ?? previous;
return Math.abs(sourceTimeNs - previous) <= Math.abs(current - sourceTimeNs)
? low - 1
: low;
}
function useCanonicalRavSpatialFrame(
review: VegetationFullRouteReview,
replayLaunch: ObservationSessionReplayLaunch | null,
targetTimeNs: number,
) {
const [frame, setFrame] = useState<CanonicalRecordedLabSpatialFrame | null>(null);
const [error, setError] = useState<string | null>(null);
const desiredRef = useRef<number | null>(null);
const runningRef = useRef(false);
const mountedRef = useRef(true);
const cacheRef = useRef(new Map<number, CanonicalRecordedLabSpatialFrame>());
const pumpRef = useRef<() => void>(() => undefined);
pumpRef.current = () => {
if (runningRef.current || desiredRef.current === null || !replayLaunch) return;
runningRef.current = true;
let settledTimeNs: number | null = null;
void (async () => {
while (mountedRef.current && desiredRef.current !== null) {
const requestedTimeNs = desiredRef.current;
const cached = cacheRef.current.get(requestedTimeNs);
try {
const next = cached ?? await fetchCanonicalRecordedLabSpatialFrame(
review.sessionId,
replayLaunch.sha256,
requestedTimeNs,
);
if (!cached) {
cacheRef.current.set(requestedTimeNs, next);
while (cacheRef.current.size > 12) {
const oldest = cacheRef.current.keys().next().value as number | undefined;
if (oldest === undefined) break;
cacheRef.current.delete(oldest);
}
}
if (!mountedRef.current) break;
setFrame(next);
setError(null);
} catch (caught: unknown) {
if (!mountedRef.current) break;
setError(caught instanceof Error ? caught.message : "Spatial-слои RAV004 недоступны.");
}
settledTimeNs = requestedTimeNs;
if (desiredRef.current === requestedTimeNs) break;
}
})().finally(() => {
runningRef.current = false;
if (
mountedRef.current
&& desiredRef.current !== null
&& desiredRef.current !== settledTimeNs
) {
pumpRef.current();
}
});
};
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
desiredRef.current = null;
};
}, []);
useEffect(() => {
cacheRef.current.clear();
setFrame(null);
setError(null);
}, [replayLaunch?.sha256, review.sessionId]);
useEffect(() => {
if (!replayLaunch) return;
desiredRef.current = targetTimeNs;
const cached = cacheRef.current.get(targetTimeNs);
if (cached) {
setFrame(cached);
setError(null);
return;
}
pumpRef.current();
}, [replayLaunch, targetTimeNs]);
return { frame, error, loading: Boolean(replayLaunch) && !frame && !error };
}
function FullRouteReviewEvidence({
@@ -88,41 +223,57 @@ function FullRouteReviewEvidence({
resultId: string;
review: VegetationFullRouteReview;
}) {
const [sequence, setSequence] = useState(1);
const [playing, setPlaying] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const {
mediaMode,
spatialMode,
splitView,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange: handleMediaModeChange,
onSpatialModeChange: handleSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
} = useCanonicalRecordedLabReplayState<FullRouteMediaMode, FullRouteSpatialMode>({
initialMediaMode: "video",
initialSpatialMode: "3d",
});
const [semanticLayer, setSemanticLayer] = useState<"city" | "vegetation">("vegetation");
const [showCameraSemantic, setShowCameraSemantic] = useState(true);
const [expanded, setExpanded] = useState(false);
const [evidenceMode, setEvidenceMode] = useState<M48BlindEvidenceMode>("3d");
const [cameraVisible, setCameraVisible] = useState(true);
const [sceneSettings, setSceneSettings] = useState<SceneSettings>(() => ({
...defaultSceneSettings,
accumulationSeconds: 12,
showPoints: true,
showTrajectory: true,
}));
const [showSourcePoints, setShowSourcePoints] = useState(true);
const [showLocalSlam, setShowLocalSlam] = useState(true);
const [showTgs, setShowTgs] = useState(true);
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [replayLaunch, setReplayLaunch] = useState<ObservationSessionReplayLaunch | null>(null);
const [videoError, setVideoError] = useState<string | null>(null);
const [linkedReview, setLinkedReview] = useState<VegetationMixedRouteReview | null>(null);
const [linkedReviewError, setLinkedReviewError] = useState<string | null>(null);
const spatialControllerRef = useRef<RerunPlaybackController | null>(null);
const frames = useMemo(
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
sequence: index + 1,
sourceTimeNs,
})),
[review.frameSourceTimesNs],
const [tgsAnchor, setTgsAnchor] = useState<VegetationRouteTgsAnchor | null>(null);
const [tgsAnchorLoading, setTgsAnchorLoading] = useState(false);
const [tgsAnchorError, setTgsAnchorError] = useState<string | null>(null);
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
const playbackRange = useMemo(() => ({
startSeconds: review.timelineStartSeconds,
endSeconds: review.timelineEndSeconds,
}), [review.timelineEndSeconds, review.timelineStartSeconds]);
const playbackController = useRecordedEvidencePlayback(playbackRange, { clock: "animation" });
const sequenceIndex = nearestFullRouteFrameIndex(
review.frameSourceTimesNs,
Math.round(playbackController.playback.currentSeconds * 1_000_000_000),
);
const sequence = sequenceIndex + 1;
const spatialRequestIndex = Math.floor(sequenceIndex / 5) * 5;
const spatialRequestTimeNs = review.frameSourceTimesNs[spatialRequestIndex]
?? review.frameSourceTimesNs[sequenceIndex]
?? Math.round(playbackController.playback.currentSeconds * 1_000_000_000);
const spatialEvidence = useCanonicalRavSpatialFrame(review, replayLaunch, spatialRequestTimeNs);
const layer = review[semanticLayer];
const semantic = useMemo(() => semanticPresentation(layer), [layer]);
const maskSequence = sequence - 1;
const prefetchSrcs = useMemo(() => showCameraSemantic
? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1)
? Array.from({ length: 8 }, (_, offset) => sequenceIndex + offset + 1)
.filter((candidate) => candidate < review.frameCount)
.map((candidate) => vegetationFullRouteMaskUrl(resultId, semanticLayer, candidate))
: [], [maskSequence, resultId, review.frameCount, semanticLayer, showCameraSemantic]);
: [], [resultId, review.frameCount, semanticLayer, sequenceIndex, showCameraSemantic]);
useEffect(() => {
const controller = new AbortController();
@@ -185,246 +336,326 @@ function FullRouteReviewEvidence({
return () => controller.abort();
}, [review.linkedRouteReviewResultId, review.sessionId, review.sourceId]);
const spatialProfile = useMemo(() => replayLaunch ? recordedSessionRerunProfile({
sourceUrl: replayLaunch.viewerSourceUrl,
artifact: {
sourceUrl: replayLaunch.sourceUrl,
viewerSourceUrl: replayLaunch.viewerSourceUrl,
byteLength: replayLaunch.byteLength,
sha256: replayLaunch.sha256,
},
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: replayLaunch.timelineStartSeconds,
expectedTimelineEndSeconds: replayLaunch.timelineEndSeconds,
initialPlaybackStartSeconds: review.timelineStartSeconds,
view: "spatial",
viewResetGeneration: 0,
followTrajectory: false,
perceptionLayers: {
enabled: false,
detections2d: false,
segmentation: false,
cuboids3d: false,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: false,
}) : null, [replayLaunch, review.timelineStartSeconds]);
const activeFrame = frames.find((candidate) => candidate.sequence === sequence)
?? frames[0]
?? null;
const activeSourceTimeNsRef = useRef(activeFrame?.sourceTimeNs ?? null);
activeSourceTimeNsRef.current = activeFrame?.sourceTimeNs ?? null;
const handleSpatialControllerChange = useCallback((controller: RerunPlaybackController | null) => {
spatialControllerRef.current = controller;
const sourceTimeNs = activeSourceTimeNsRef.current;
if (!controller || sourceTimeNs === null) return;
controller.setPlaying(false);
controller.seek(sourceTimeNs);
}, []);
const selectedTgsCase = linkedReview
? causalTgsCase(linkedReview.cases, sequence)
: null;
const selectedTgsTimeNs = selectedTgsCase
? review.frameSourceTimesNs[selectedTgsCase.sourceSequence - 1]
?? Math.round(selectedTgsCase.sessionSeconds * 1_000_000_000)
: spatialRequestTimeNs;
const tgsReferenceEvidence = useCanonicalRavSpatialFrame(
review,
replayLaunch,
selectedTgsTimeNs,
);
useEffect(() => {
const controller = spatialControllerRef.current;
if (!controller || !activeFrame) return;
controller.setPlaying(false);
controller.seek(activeFrame.sourceTimeNs);
}, [activeFrame]);
const selectedTgsCase = linkedReview
? nearestTgsCase(linkedReview.cases, sequence)
: null;
const selectTgsPlan = useCallback(() => {
if (!linkedReview) return;
const item = nearestTgsCase(linkedReview.cases, sequence);
if (!item) return;
setPlaying(false);
setSequence(item.sourceSequence);
setEvidenceMode("plan");
}, [linkedReview, sequence]);
const handleEvidenceModeChange = useCallback((nextMode: M48BlindEvidenceMode) => {
if (nextMode === "plan") {
selectTgsPlan();
if (!showTgs || !selectedTgsCase) {
setTgsAnchor(null);
setTgsAnchorLoading(false);
setTgsAnchorError(null);
return;
}
setEvidenceMode(nextMode);
}, [selectTgsPlan]);
const cameraPresentation = evidenceMode === "camera"
? "primary"
: cameraVisible ? "companion" : "hidden";
const spatialScene = evidenceMode === "plan" ? (
selectedTgsCase ? (
<div className="recorded-evidence-image-scene">
<img
src={selectedTgsCase.assets.tgs}
alt={`TGS costmap · sequence ${selectedTgsCase.sourceSequence}`}
draggable={false}
const controller = new AbortController();
setTgsAnchorLoading(true);
setTgsAnchorError(null);
void fetchVegetationRouteTgsAnchor(
review.linkedRouteReviewResultId,
selectedTgsCase.sourceSequence,
{ signal: controller.signal },
).then((anchor) => {
if (!controller.signal.aborted) setTgsAnchor(anchor);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setTgsAnchorError(caught instanceof Error ? caught.message : "TGS anchor недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setTgsAnchorLoading(false);
});
return () => controller.abort();
}, [review.linkedRouteReviewResultId, selectedTgsCase?.sourceSequence, showTgs]);
const packedTgsCells = useMemo<LaboratoryMetricPackedCellEvidence | undefined>(() => {
if (!tgsAnchor) return undefined;
const currentBody = spatialEvidence.frame?.bodyFrame;
const anchorBody = tgsReferenceEvidence.frame?.bodyFrame;
const transformPoint = (point: readonly [number, number, number]) => {
if (!currentBody || !anchorBody) return point;
const map = [0, 1, 2].map((row) => (
anchorBody.originMapXyzM[row]!
+ anchorBody.basisMapFromBody[row]!.reduce(
(sum, coefficient, column) => sum + coefficient * point[column]!,
0,
)
));
const delta = map.map((value, index) => value - currentBody.originMapXyzM[index]!);
return [0, 1, 2].map((column) => (
currentBody.basisMapFromBody.reduce(
(sum, row, rowIndex) => sum + row[column]! * delta[rowIndex]!,
0,
)
)) as [number, number, number];
};
const centers: number[] = [];
const zBounds: number[] = [];
tgsAnchor.costmap.centersXyM.forEach(([x, y], index) => {
const bounds = tgsAnchor.costmap.zBoundsM[index] ?? [null, null];
const center = transformPoint([x, y, 0]);
centers.push(center[0], center[1]);
if (bounds[0] === null || bounds[1] === null) {
zBounds.push(Number.NaN, Number.NaN);
} else {
const bottom = transformPoint([x, y, bounds[0]]);
const top = transformPoint([x, y, bounds[1]]);
zBounds.push(Math.min(bottom[2], top[2]), Math.max(bottom[2], top[2]));
}
});
return {
centersBodyXyM: Float32Array.from(centers),
zBoundsM: Float32Array.from(zBounds),
stateCodes: Uint8Array.from(tgsAnchor.costmap.stateCodes),
};
}, [spatialEvidence.frame?.bodyFrame, tgsAnchor, tgsReferenceEvidence.frame?.bodyFrame]);
const semanticOverlay = showCameraSemantic ? {
src: vegetationFullRouteMaskUrl(resultId, semanticLayer, sequenceIndex),
prefetchSrcs,
classes: semantic.classes,
palette: semantic.palette,
opacity: 0.76,
ariaLabel: `${layer.name} semantic prediction frame ${sequence}`,
} : undefined;
const mediaContent = (
<div className="m4-replay-threat-visual__media-layer" data-media={mediaMode ?? "none"}>
{videoSource ? (
<RecordedEvidenceVideoScene
source={videoSource}
playback={playbackController.playback}
imageWidth={review.width}
imageHeight={review.height}
boxes={[]}
semanticOverlay={semanticOverlay}
ariaLabel={`RAVNOVES004TREE recorded frame ${sequence}`}
interactive={false}
segmentSequence={sequence}
segmentCount={review.frameCount}
onPlaybackChange={playbackController.synchronize}
playbackAuthority="host"
playbackTransport="epoch-stream"
/>
<div className="m48-clip-player__pane-label" data-pane="spatial">
TGS COSTMAP · ЯКОРЬ {selectedTgsCase.sourceSequence} · {selectedTgsCase.tgs.occupiedCells} OCCUPIED
) : (
<div className="l3-visual-audit__state" role={videoError ? "alert" : "status"}>
{videoError ?? "Открываем автономный RAVNOVES004TREE source…"}
</div>
</div>
) : (
<div className="m4-replay-threat-visual__pane-status" role="status">
{linkedReviewError ?? "Открываем sealed TGS anchors…"}
</div>
)
) : spatialProfile ? (
<RerunViewport
profile={spatialProfile}
sceneSettings={sceneSettings}
onPlaybackControllerChange={handleSpatialControllerChange}
/>
) : (
<div className="m4-replay-threat-visual__pane-status" role="status">
Открываем sealed RRD, source points и SLAM trajectory
)}
</div>
);
return (
<LaboratoryEvidenceViewer
label="RAVNOVES004TREE full recorded review"
className="m48-atlas-visual"
mode={semanticLayer}
modes={FULL_ROUTE_SEMANTIC_MODES}
expanded={expanded}
onModeChange={setSemanticLayer}
onExpandedChange={setExpanded}
modeControlsVisible={false}
chromeLayout="stacked"
const spatialContent = spatialMode ? (
<>
{spatialEvidence.frame ? (
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={showSourcePoints
? spatialEvidence.frame.sourcePointsBodyXyzM
: []}
localSurfaceBodyXyzM={showLocalSlam
? spatialEvidence.frame.localSlamBodyXyzM
: []}
obstacles={[]}
rig={{ lengthM: 1, widthM: 0.8, nominalSensorHeightM: 0.4 }}
corridor={{ forwardLengthM: 12, rearMarginM: 1, halfWidthM: 0.4 }}
occupiedVoxelSizeM={tgsAnchor?.costmap.cellSizeM ?? 0.45}
mode={spatialMode}
label="RAV004 canonical source points, local SLAM and TGS costmap"
showCurrentIncrement={showSourcePoints}
showLocalSurface={showLocalSlam}
showRollingMap={showTgs}
showLowStep={false}
classifiedPackedCells={packedTgsCells}
classifiedCellSizeM={tgsAnchor?.costmap.cellSizeM}
showClassifiedCells={showTgs && Boolean(packedTgsCells)}
/>
) : (
<div className="l3-visual-audit__state" role={spatialEvidence.error ? "alert" : "status"}>
{spatialEvidence.loading ? <span className="busy-indicator" aria-hidden="true" /> : null}
<span>{spatialEvidence.error ?? "Открываем source points и Local SLAM из sealed RRD…"}</span>
</div>
)}
{showTgs && selectedTgsCase ? (
<div className="m4-replay-threat-visual__pane-status" role="status">
{tgsAnchorError ?? linkedReviewError ?? (tgsAnchorLoading
? `Открываем sealed TGS anchor ${selectedTgsCase.sourceSequence}; source/SLAM и общий clock продолжаются.`
: `TGS anchor ${selectedTgsCase.sourceSequence} из 10; source/SLAM и общий clock продолжаются.`)}
</div>
) : null}
</>
) : null;
const mediaLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<div className="m48-evidence-stage">
{videoSource ? (
<LaboratoryRecordedClipPlayer
source={videoSource}
segmentCount={review.frameCount}
frames={frames}
sequence={sequence}
playing={playing}
playbackRate={playbackRate}
cameraPresentation={cameraPresentation}
continuousPlayback
sourceCount={4}
onSequenceChange={setSequence}
onPlayingChange={setPlaying}
onPlaybackRateChange={setPlaybackRate}
alternativeScene={spatialScene}
cameraControls={(
<>
<Button
size="compact"
shape="pill"
variant={showCameraSemantic ? "primary" : "secondary"}
aria-pressed={showCameraSemantic}
onClick={() => setShowCameraSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
<SegmentedControl
value={semanticLayer}
items={[...FULL_ROUTE_SEMANTIC_MODES]}
label="Источник семантики"
onChange={(value) => {
setSemanticLayer(value);
setShowCameraSemantic(true);
}}
/>
</>
)}
spatialControls={(
<>
<Button
size="compact"
shape="pill"
variant={sceneSettings.showPoints ? "primary" : "secondary"}
aria-pressed={sceneSettings.showPoints}
onClick={() => {
setEvidenceMode("3d");
setSceneSettings((settings) => ({
...settings,
showPoints: !settings.showPoints,
}));
}}
>
SOURCE POINTS
</Button>
<Button
size="compact"
shape="pill"
variant={sceneSettings.showTrajectory ? "primary" : "secondary"}
aria-pressed={sceneSettings.showTrajectory}
onClick={() => {
setEvidenceMode("3d");
setSceneSettings((settings) => ({
...settings,
showTrajectory: !settings.showTrajectory,
}));
}}
>
LOCAL SLAM
</Button>
<Button
size="compact"
shape="pill"
variant={evidenceMode === "plan" ? "primary" : "secondary"}
aria-pressed={evidenceMode === "plan"}
disabled={!linkedReview}
title={linkedReviewError ?? "10 sealed causal TGS anchors; continuous TGS отсутствует"}
onClick={selectTgsPlan}
>
TGS COSTMAP
</Button>
<Button
size="compact"
shape="pill"
variant="secondary"
disabled
title="Для RAV004 не опубликован point-aligned 3D semantic archive"
>
SEMANTICS
</Button>
</>
)}
cameraOverlay={(
<>
<div className="m48-clip-player__pane-label" data-pane="camera">
{showCameraSemantic
? `${semanticLayer === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`
: `SOURCE · КАДР ${sequence}/${review.frameCount}`}
</div>
{showCameraSemantic ? (
<div className="m48-clip-player__overlay">
<RecordedEvidenceSemanticMaskOverlay
src={vegetationFullRouteMaskUrl(resultId, semanticLayer, maskSequence)}
prefetchSrcs={prefetchSrcs}
imageWidth={review.width}
imageHeight={review.height}
classes={semantic.classes}
palette={semantic.palette}
opacity={0.76}
ariaLabel={`${layer.name} semantic prediction`}
/>
</div>
) : null}
</>
)}
/>
) : (
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
{videoError ?? "Открываем автономный RAVNOVES004TREE source…"}
</div>
)}
{videoSource ? (
<M48EvidenceModeRail
mode={evidenceMode}
cameraVisible={cameraVisible}
spatialAvailable={Boolean(spatialProfile)}
planAvailable={Boolean(linkedReview)}
onModeChange={handleEvidenceModeChange}
onCameraVisibleChange={setCameraVisible}
/>
) : null}
<Button
size="compact"
shape="pill"
variant={showCameraSemantic ? "primary" : "secondary"}
aria-pressed={showCameraSemantic}
onClick={() => setShowCameraSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
<SegmentedControl
value={semanticLayer}
items={[...FULL_ROUTE_SEMANTIC_MODES]}
label="Источник семантики"
onChange={(value) => {
setSemanticLayer(value);
setShowCameraSemantic(true);
}}
/>
</div>
);
const spatialLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои 3D и плана"
>
<Button
size="compact"
shape="pill"
variant={showSourcePoints ? "primary" : "secondary"}
aria-pressed={showSourcePoints}
onClick={() => setShowSourcePoints((visible) => !visible)}
>
SOURCE POINTS
</Button>
<Button
size="compact"
shape="pill"
variant={showLocalSlam ? "primary" : "secondary"}
aria-pressed={showLocalSlam}
onClick={() => setShowLocalSlam((visible) => !visible)}
>
LOCAL SLAM
</Button>
<Button
size="compact"
shape="pill"
variant={showTgs ? "primary" : "secondary"}
aria-pressed={showTgs}
disabled={!linkedReview}
title={linkedReviewError ?? "10 sealed causal TGS anchors; continuous TGS отсутствует"}
onClick={() => setShowTgs((visible) => !visible)}
>
TGS COSTMAP
</Button>
<Button
size="compact"
shape="pill"
variant={showCameraSemantic ? "primary" : "secondary"}
aria-pressed={showCameraSemantic}
title="Recorded semantic layer; camera-aligned prediction, без выдуманной 3D-проекции"
onClick={() => setShowCameraSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
</div>
);
const resetSpatialView = (
<IconButton
label="Сбросить ракурс"
onClick={() => metricSceneRef.current?.resetView()}
>
<Icon name="refresh" size={16} />
</IconButton>
);
const overlayPanePercent = splitView && splitOrientation === "vertical"
? splitPrimarySize
: 100;
const overlay = (
<div
className="l3-visual-audit__overlay m4-replay-threat-visual__overlay"
style={{
"--m4-replay-threat-overlay-pane-width": `${overlayPanePercent}%`,
} as CSSProperties}
>
<div>
<span>RAVNOVES004TREE · recorded realtime</span>
<strong>frame {sequence}/{review.frameCount}</strong>
<small>
+{(playbackController.playback.currentSeconds - review.timelineStartSeconds).toFixed(3)} с
· {playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
</small>
</div>
</LaboratoryEvidenceViewer>
<div>
<span>Spatial evidence</span>
<strong>{showTgs && selectedTgsCase
? `TGS anchor ${selectedTgsCase.sourceSequence} · ${selectedTgsCase.tgs.occupiedCells} occupied`
: "source RRD · points + SLAM"}</strong>
<small>{showTgs
? "latest causal of 10 sealed anchors · continuous playback retained"
: "causal 1 s view · grayscale intensity · recorded source identity"}</small>
</div>
</div>
);
const transport = (
<ObservationTimeline
className="m4-replay-threat-visual__timeline"
active
sourceCount={4}
mode="recorded"
seekable
synchronization="host-arrival-best-effort"
rangeNs={{
min: Math.round(review.timelineStartSeconds * 1_000_000_000),
max: Math.round(review.timelineEndSeconds * 1_000_000_000),
}}
currentNs={Math.round(playbackController.playback.currentSeconds * 1_000_000_000)}
playing={playbackController.playback.playing}
playbackRate={playbackController.playback.rate ?? 1}
onSeek={(timeNs) => playbackController.seek(timeNs / 1_000_000_000)}
onPlayingChange={playbackController.setPlaying}
onPlaybackRateChange={playbackController.setRate}
showJumpToEnd={false}
/>
);
return (
<CanonicalRecordedLabReplay
label="RAVNOVES004TREE full recorded review"
mediaMode={mediaMode ?? "none"}
mediaModes={FULL_ROUTE_MEDIA_MODES}
spatialMode={spatialMode ?? "none"}
spatialModes={FULL_ROUTE_SPATIAL_MODES}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео"}
spatialAriaLabel={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer
mediaContent={mediaContent}
spatialContent={spatialContent}
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте."
overlay={overlay}
transport={transport}
trailingActions={!splitView && spatialMode ? resetSpatialView : null}
onMediaModeChange={handleMediaModeChange}
onSpatialModeChange={handleSpatialModeChange}
onExpandedChange={setExpanded}
onSplitPrimarySizeChange={setSplitPrimarySize}
/>
);
}