Стабилизация синхронного LAB-воспроизведения

This commit is contained in:
DCCONSTRUCTIONS
2026-08-23 18:56:06 +03:00
parent a3fc13ad63
commit 51bb1369eb
12 changed files with 1042 additions and 336 deletions
@@ -52,6 +52,7 @@ import {
buildLaboratoryCatalog,
buildLaboratoryProfiles,
experimentOptionsForProfile,
freshestLaboratorySelection,
workOptionsForExperiment,
} from "./laboratoryArchiveProfiles";
import {
@@ -481,7 +482,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const [workId, setWorkId] = useState<LaboratoryWorkId>(
"l34-right-yolox-truth-island-freeze",
);
const initialWorkSelectedRef = useRef(false);
const initialSelectionAppliedRef = useRef(false);
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
@@ -656,6 +657,15 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|| sessions.state === "idle"
|| sessions.state === "loading"
) return;
if (!initialSelectionAppliedRef.current) {
const freshest = freshestLaboratorySelection(catalog);
if (!freshest) return;
initialSelectionAppliedRef.current = true;
setProfileId(freshest.profileId);
setExperimentId(freshest.experimentId);
setWorkId(freshest.workId);
return;
}
if (!profiles.some((profile) => profile.id === profileId)) {
const firstProfile = profiles[0];
if (!firstProfile) return;
@@ -667,13 +677,6 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
if (firstExperiment) setExperimentId(firstExperiment.id);
return;
}
if (!initialWorkSelectedRef.current) {
const freshestWork = workOptions[0];
if (!freshestWork) return;
setWorkId(freshestWork.id);
initialWorkSelectedRef.current = true;
return;
}
if (!workOptions.some((work) => work.id === workId)) {
const firstWork = workOptions[0];
if (firstWork) setWorkId(firstWork.id);
@@ -681,6 +684,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
}, [
evidenceLoading,
advanced.indexLoading,
catalog,
experimentId,
experimentOptions,
profileId,
@@ -692,7 +696,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const selectProfile = (next: LaboratoryProfileId) => {
setProfileId(next);
initialWorkSelectedRef.current = true;
initialSelectionAppliedRef.current = true;
const firstExperiment = experimentOptionsForProfile(next, catalog)[0];
if (!firstExperiment) return;
setExperimentId(firstExperiment.id);
@@ -702,7 +706,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const selectExperiment = (next: LaboratoryExperimentId) => {
setExperimentId(next);
initialWorkSelectedRef.current = true;
initialSelectionAppliedRef.current = true;
const firstWork = workOptionsForExperiment(profileId, next, catalog)[0];
if (firstWork) selectWork(firstWork.id);
};
@@ -1,5 +1,12 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Icon, IconButton } from "@nodedc/ui-react";
import {
Button,
Icon,
IconButton,
SegmentedControl,
SplitPane,
type SplitPaneOrientation,
} from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
@@ -22,6 +29,7 @@ import { useRecordedEvidencePlayback } from "../../components/laboratory/useReco
import {
e47SemanticMaskUrl,
type E47SemanticClass,
type E47SemanticTimelineFrame,
} from "../../core/laboratory/e47SemanticSlam";
import type {
M4ThreatCameraProposal,
@@ -37,7 +45,9 @@ import {
} from "./useM4ThreatTimeline";
import { useE47SemanticTimelineFrame } from "./useE47SemanticTimeline";
type M4ThreatViewMode = "video" | "camera" | LaboratoryMetricSceneMode;
type M4ThreatMediaMode = "video" | "camera";
type M4ThreatMediaSelection = M4ThreatMediaMode | "none";
type M4ThreatSpatialSelection = LaboratoryMetricSceneMode | "none";
function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox["tone"] {
if (proposal.threatDecision === "threat") return "danger";
@@ -87,12 +97,19 @@ export function M4ReplayThreatVisual({
resultId: string;
semantic?: M4ReplayThreatSemanticLayer;
}) {
const [mode, setMode] = useState<M4ThreatViewMode>("video");
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode>("3d");
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
const [showLocalSurface, setShowLocalSurface] = useState(true);
const [showRollingMap, setShowRollingMap] = useState(true);
const [showSemantic, setShowSemantic] = useState(true);
const [showMediaSemantic, setShowMediaSemantic] = useState(true);
const [showSpatialSemantic, setShowSpatialSemantic] = 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 metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
const metadata = useM4ThreatTimelineMetadata(resultId);
@@ -115,6 +132,14 @@ 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 (!timeline || videoSource) return;
@@ -162,6 +187,18 @@ export function M4ReplayThreatVisual({
}, [resultId]);
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
const lastSpatialFrameRef = useRef<{
resultId: string;
frame: M4ThreatTimelineFrame;
} | null>(null);
if (frame?.spatialAvailable) {
lastSpatialFrameRef.current = { resultId, frame };
}
const spatialFrame = frame?.spatialAvailable
? frame
: lastSpatialFrameRef.current?.resultId === resultId
? lastSpatialFrameRef.current.frame
: null;
const semanticTimeline = useE47SemanticTimelineFrame({
resultId: semantic?.resultId ?? null,
activeSequence: frame?.sequence ?? timelineFrame.activeSequence,
@@ -174,33 +211,6 @@ export function M4ReplayThreatVisual({
&& frame.sequence !== timelineFrame.activeSequence,
);
const activeBoxes = useMemo(() => boxes(frame?.cameraProposals ?? []), [frame]);
const sceneObstacles = useMemo(() => frame?.metricObstacles.map((obstacle) => ({
id: obstacle.componentId,
decision: obstacle.assessment.decision,
state: obstacle.state,
centroidBodyXyzM: obstacle.centroidBodyXyzM,
cellCentersBodyXyzM: obstacle.cellCentersBodyXyzM,
})) ?? [], [frame]);
const currentIncrementObstacles = frame?.metricObstacles.filter(
(item) => item.state === "current",
) ?? [];
const rollingMapObstacles = frame?.metricObstacles.filter(
(item) => item.state === "retained",
) ?? [];
const nearest = frame?.metricObstacles
.map((item) => item.assessment.closestApproachM)
.filter((value): value is number => value !== null)
.sort((left, right) => left - right)[0] ?? null;
const localSurface = useMemo(() => buildM4LocalSurface(
timelineFrame.availableFrames,
frame,
metadata.timeline?.localSurfaceVisualization ?? {
windowSeconds: 2,
voxelSizeM: 0.1,
radiusM: 12,
pointLimit: 20_000,
},
), [frame, metadata.timeline, timelineFrame.availableFrames]);
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
() => semantic?.taxonomy.map((item) => ({
id: item.classId,
@@ -214,50 +224,95 @@ export function M4ReplayThreatVisual({
color: item.disposition === "ambiguous"
? { kind: "token" as const, token: "--nodedc-warning-rgb" as const }
: { kind: "diagnostic" as const, rgb: item.colorRgb },
opacity: item.disposition === "ambiguous" ? 0.22 : 0.56,
opacity: item.disposition === "ambiguous" ? 0.52 : 0.92,
})) ?? [],
[semantic?.taxonomy],
);
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
? semanticTimeline.activeFrame
: null;
const semanticIntegrityError = semantic && frame?.spatialAvailable && semanticFrame && (
semanticFrame.sourcePointCount !== frame.pointCloudSourceCount
|| frame.pointCloudSampleCount !== frame.pointCloudSourceCount
|| frame.pointCloudBodyXyzM.length !== frame.pointCloudSourceCount
const lastSpatialSemanticFrameRef = useRef<{
resultId: string;
frame: E47SemanticTimelineFrame;
} | null>(null);
if (frame?.spatialAvailable && semanticFrame) {
lastSpatialSemanticFrameRef.current = { resultId, frame: semanticFrame };
}
const spatialSemanticFrame = semanticFrame?.sequence === spatialFrame?.sequence
? semanticFrame
: lastSpatialSemanticFrameRef.current?.resultId === resultId
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
? lastSpatialSemanticFrameRef.current.frame
: null;
const semanticIntegrityError = semantic && spatialFrame && spatialSemanticFrame && (
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
)
? "E47 semantic point index space не совпал с exact current increment M4.6."
: null;
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
if (
!semantic
|| !showSemantic
|| !frame
|| !frame.spatialAvailable
|| !semanticFrame
|| !showSpatialSemantic
|| !spatialFrame
|| !spatialSemanticFrame
|| semanticIntegrityError
) return undefined;
return semanticFrame.classIds.map((classId, index) => {
const status = semanticFrame.statusCodes[index];
return spatialSemanticFrame.classIds.map((classId, index) => {
const status = spatialSemanticFrame.statusCodes[index];
return status === 2 || status === 3 ? classId : null;
});
}, [frame, semantic, semanticFrame, semanticIntegrityError, showSemantic]);
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
const sceneObstacles = useMemo(() => spatialFrame?.metricObstacles.map((obstacle) => ({
id: obstacle.componentId,
decision: obstacle.assessment.decision,
state: obstacle.state,
centroidBodyXyzM: obstacle.centroidBodyXyzM,
cellCentersBodyXyzM: obstacle.cellCentersBodyXyzM,
})) ?? [], [spatialFrame]);
const currentIncrementObstacles = spatialFrame?.metricObstacles.filter(
(item) => item.state === "current",
) ?? [];
const rollingMapObstacles = spatialFrame?.metricObstacles.filter(
(item) => item.state === "retained",
) ?? [];
const nearest = spatialFrame?.metricObstacles
.map((item) => item.assessment.closestApproachM)
.filter((value): value is number => value !== null)
.sort((left, right) => left - right)[0] ?? null;
const localSurface = useMemo(() => buildM4LocalSurface(
timelineFrame.availableFrames,
spatialFrame,
metadata.timeline?.localSurfaceVisualization ?? {
windowSeconds: 2,
voxelSizeM: 0.1,
radiusM: 12,
pointLimit: 20_000,
},
), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]);
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
semantic && showSemantic && frame
semantic && showMediaSemantic && frame
? {
src: e47SemanticMaskUrl(semantic.resultId, frame.sequence),
prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1)
.map((offset) => frame.sequence + offset)
.filter((sequence) => sequence < (metadata.timeline?.frameCount ?? 0))
.map((sequence) => e47SemanticMaskUrl(semantic.resultId, sequence)),
classes: semanticClasses,
palette: semanticPalette,
opacity: 0.48,
opacity: 0.9,
ariaLabel: `E47 semantic mask frame ${frame.sequence + 1}`,
}
: undefined;
const seek = (seconds: number) => playbackController.seek(seconds);
const handleModeChange = (next: M4ThreatViewMode) => {
if (next === "camera") playbackController.setPlaying(false);
if (next === "3d" || next === "plan") setSpatialMode(next);
setMode(next);
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(() => {
@@ -266,86 +321,124 @@ export function M4ReplayThreatVisual({
image.src = frame.cameraUrl;
}, [frame?.cameraUrl, playbackController.playback.playing]);
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton
label="Назад на 5 секунд"
disabled={!metadata.timeline}
onClick={() => seek(playbackController.playback.currentSeconds - 5)}
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 = semantic ? (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<Button
size="compact"
shape="pill"
variant={showMediaSemantic ? "primary" : "secondary"}
aria-pressed={showMediaSemantic}
onClick={() => setShowMediaSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
</div>
) : null;
const spatialLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои 3D и плана"
>
<Button
size="compact"
shape="pill"
variant={showCurrentIncrement ? "primary" : "secondary"}
aria-pressed={showCurrentIncrement}
onClick={() => setShowCurrentIncrement((visible) => !visible)}
>
CURRENT
</Button>
<Button
size="compact"
shape="pill"
variant={showLocalSurface ? "primary" : "secondary"}
aria-pressed={showLocalSurface}
title="Bounded local SLAM surface · visual-derived"
onClick={() => setShowLocalSurface((visible) => !visible)}
>
LOCAL SLAM
</Button>
<Button
size="compact"
shape="pill"
variant={showRollingMap ? "primary" : "secondary"}
aria-pressed={showRollingMap}
onClick={() => setShowRollingMap((visible) => !visible)}
>
ROLLING
</Button>
{semantic ? (
<Button
size="compact"
shape="pill"
variant={showSpatialSemantic ? "primary" : "secondary"}
aria-pressed={showSpatialSemantic}
onClick={() => setShowSpatialSemantic((visible) => !visible)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Вперёд на 5 секунд"
disabled={!metadata.timeline}
onClick={() => seek(playbackController.playback.currentSeconds + 5)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
{mode === "3d" || mode === "plan" || semantic ? (
<div
className="nodedc-segmented m4-replay-threat-visual__layer-controls"
role="group"
aria-label="Слои пространственного evidence"
>
{mode === "3d" || mode === "plan" ? (
<>
<button
type="button"
className="nodedc-segmented__item"
data-active={showCurrentIncrement ? "true" : undefined}
aria-pressed={showCurrentIncrement}
onClick={() => setShowCurrentIncrement((visible) => !visible)}
>
CURRENT
</button>
<button
type="button"
className="nodedc-segmented__item"
data-active={showLocalSurface ? "true" : undefined}
aria-pressed={showLocalSurface}
title="Bounded local SLAM surface · visual-derived"
onClick={() => setShowLocalSurface((visible) => !visible)}
>
LOCAL SLAM
</button>
<button
type="button"
className="nodedc-segmented__item"
data-active={showRollingMap ? "true" : undefined}
aria-pressed={showRollingMap}
onClick={() => setShowRollingMap((visible) => !visible)}
>
ROLLING
</button>
</>
) : null}
{semantic ? (
<button
type="button"
className="nodedc-segmented__item"
data-active={showSemantic ? "true" : undefined}
aria-pressed={showSemantic}
onClick={() => setShowSemantic((visible) => !visible)}
>
SEMANTICS
</button>
) : null}
</div>
SEMANTICS
</Button>
) : null}
</div>
);
const trailingActions = mode === "3d" || mode === "plan" ? (
const resetSpatialView = (
<IconButton
label="Сбросить ракурс"
onClick={() => metricSceneRef.current?.resetView()}
>
<Icon name="refresh" size={16} />
</IconButton>
) : null;
);
const singlePaneControls = !splitView
? spatialMode ? spatialLayerControls : mediaMode ? mediaLayerControls : null
: null;
const actions = singlePaneControls ? (
<div className="l3-visual-audit__actions">
<div className="m4-replay-threat-visual__single-pane-controls">
{singlePaneControls}
</div>
</div>
) : undefined;
const trailingActions = !splitView && spatialMode ? resetSpatialView : null;
const overlay = metadata.timeline && frame ? (
<div className="l3-visual-audit__overlay m4-replay-threat-visual__overlay">
@@ -365,18 +458,18 @@ export function M4ReplayThreatVisual({
{currentIncrementObstacles.length} current · {rollingMapObstacles.length} rolling
</strong>
<small>
{frame.spatialAvailable
? `${frame.pointCloudSampleCount}/${frame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
: "body frame / current increment unavailable"}
{semantic && semanticFrame
? ` · semantic L ${semanticFrame.counts.labeled} · A ${semanticFrame.counts.ambiguous} · U ${semanticFrame.counts.unprojected} · Ø ${semanticFrame.counts.absent}`
{spatialFrame
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
: "квалифицированный spatial frame ещё не получен"}
{semantic && spatialSemanticFrame
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
: semantic ? " · semantic buffer" : ""}
</small>
</div>
<div>
<span>Virtual corridor</span>
<strong>
{frame.decisionCounts.threat} threat · nearest {nearest === null ? "—" : `${nearest.toFixed(2)} м`}
{spatialFrame?.decisionCounts.threat ?? 0} threat · nearest {nearest === null ? "—" : `${nearest.toFixed(2)} м`}
</strong>
<small>
{metadata.timeline.corridor.forwardLengthM} м · body {metadata.timeline.rig.lengthM}×{metadata.timeline.rig.widthM} м · REPLAY-SIMULATED
@@ -397,74 +490,129 @@ export function M4ReplayThreatVisual({
</div>
);
} else {
content = (
<div className="m4-replay-threat-visual__deck">
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"
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
<div
className="m4-replay-threat-visual__layer"
data-active={mode === "video" ? "true" : undefined}
aria-hidden={mode !== "video"}
className="m4-replay-threat-visual__media-layer"
data-media="video"
hidden={mediaMode !== "video"}
>
{videoSource ? (
<RecordedEvidenceVideoScene
source={videoSource}
playback={playbackController.playback}
imageWidth={timeline.imageWidth}
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
semanticOverlay={mode === "video" ? semanticOverlay : undefined}
ariaLabel={`M4.6 recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
interactive={false}
/>
) : videoError ? (
<SpatialState message={videoError} />
) : (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>{videoLoading ? "Подготавливаем локальный видеобуфер" : "Открываем RIGHT-видео RAVNOVES00"}</span>
</div>
<RecordedEvidenceVideoScene
source={videoSource}
playback={playbackController.playback}
imageWidth={timeline.imageWidth}
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
semanticOverlay={mediaMode === "video" ? semanticOverlay : undefined}
ariaLabel={`M4.6 recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
interactive={false}
/>
) : videoError ? (
<SpatialState message={videoError} />
) : (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>{videoLoading ? "Подготавливаем локальный видеобуфер" : "Открываем RIGHT-видео RAVNOVES00"}</span>
</div>
)}
</div>
<div
className="m4-replay-threat-visual__layer"
data-active={mode === "camera" ? "true" : undefined}
aria-hidden={mode !== "camera"}
>
{mode === "camera" && frame ? (
<RecordedEvidenceImageScene
src={frame.cameraUrl}
imageWidth={timeline.imageWidth}
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
semanticOverlay={mode === "camera" ? semanticOverlay : undefined}
ariaLabel={`M4.6 exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
/>
) : null}
</div>
<div
className="m4-replay-threat-visual__layer"
data-active={mode === "3d" || mode === "plan" ? "true" : undefined}
aria-hidden={mode !== "3d" && mode !== "plan"}
>
{frame ? (
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={frame.pointCloudBodyXyzM}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
mode={spatialMode}
label="M4.6 exact current increment, bounded local SLAM surface and rolling occupancy"
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
pointSemanticClassIds={alignedSemanticPointIds}
semanticClasses={semanticClasses}
semanticPalette={semanticPalette}
/>
) : null}
</div>
{mediaMode === "camera" && frame ? (
<RecordedEvidenceImageScene
src={frame.cameraUrl}
imageWidth={timeline.imageWidth}
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
semanticOverlay={semanticOverlay}
ariaLabel={`M4.6 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}
{spatialFrame ? (
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={spatialFrame.pointCloudBodyXyzM}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
mode={spatialMode}
label="M4.6 exact current increment, bounded local SLAM surface and rolling occupancy"
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
pointSemanticClassIds={alignedSemanticPointIds}
semanticClasses={semanticClasses}
semanticPalette={semanticPalette}
/>
) : null}
{frame && !frame.spatialAvailable ? (
<div className="m4-replay-threat-visual__pane-status" role="status">
{spatialFrame
? `На кадре ${frame.sequence + 1} нет body frame; держим spatial evidence кадра ${spatialFrame.sequence + 1}.`
: `На кадре ${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}
{timelineFrame.loading || displayingBufferedFrame ? (
<div className="m4-replay-threat-visual__buffering" role="status">
<span className="busy-indicator" aria-hidden="true" />
@@ -495,11 +643,6 @@ export function M4ReplayThreatVisual({
<span>{semanticIntegrityError}</span>
</div>
) : null}
{frame && !frame.spatialAvailable && (mode === "3d" || mode === "plan") ? (
<div className="m4-replay-threat-visual__buffering" role="status">
На этом кадре нет квалифицированного body frame; сцена сохранена.
</div>
) : null}
</div>
);
}
@@ -533,16 +676,24 @@ export function M4ReplayThreatVisual({
? "E47 semantic + SLAM diagnostic replay"
: "M4.6 dual-evidence recorded-realtime replay"}
className="m4-replay-threat-evidence-viewer"
mode={mode}
mode={mediaMode ?? "none"}
modes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
secondaryMode={{
value: spatialMode ?? "none",
modes: [
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
],
label: "3D и план",
onChange: handleSpatialModeChange,
}}
expanded={expanded}
onModeChange={handleModeChange}
onModeChange={handleMediaModeChange}
onExpandedChange={setExpanded}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
@@ -46,6 +46,12 @@ export interface LaboratoryCatalogEntry {
variantName: string;
}
export interface LaboratorySelection {
profileId: LaboratoryProfileId;
experimentId: LaboratoryExperimentId;
workId: LaboratoryWorkId;
}
interface KnownWorkDefinition {
profileId: Exclude<LaboratoryProfileId, `published:${string}`>;
profileName: (rigLabel: string) => string;
@@ -478,3 +484,19 @@ export function workOptionsForExperiment(
label: `${laboratoryTimestamp(entry.createdAtUtc)} · ${entry.variantName}`,
}));
}
export function freshestLaboratorySelection(
catalog: readonly LaboratoryCatalogEntry[],
): LaboratorySelection | null {
const profile = buildLaboratoryProfiles(catalog)[0];
if (!profile) return null;
const experiment = experimentOptionsForProfile(profile.id, catalog)[0];
if (!experiment) return null;
const work = workOptionsForExperiment(profile.id, experiment.id, catalog)[0];
if (!work) return null;
return {
profileId: profile.id,
experimentId: experiment.id,
workId: work.id,
};
}