fix(lab): stabilize replay and densify LiDAR overlay
This commit is contained in:
@@ -42,6 +42,7 @@ import { recordedObservationSources } from "../../core/observation/recordedObser
|
||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
||||
import {
|
||||
useM4ThreatCameraPointOverlay,
|
||||
useM4ThreatTimelineFrame,
|
||||
useM4ThreatTimelineMetadata,
|
||||
} from "./useM4ThreatTimeline";
|
||||
@@ -224,6 +225,12 @@ export function M4ReplayThreatVisual({
|
||||
: lastSpatialFrameRef.current?.resultId === resultId
|
||||
? lastSpatialFrameRef.current.frame
|
||||
: null;
|
||||
const cameraPointOverlay = useM4ThreatCameraPointOverlay({
|
||||
enabled: showMediaPoints,
|
||||
resultId,
|
||||
sequence: frame?.sequence ?? null,
|
||||
endpointRoot: timelineEndpointRoot,
|
||||
});
|
||||
const semanticTimeline = useE47SemanticTimelineFrame({
|
||||
resultId: semantic?.resultId ?? null,
|
||||
activeSequence: frame?.sequence ?? timelineFrame.activeSequence,
|
||||
@@ -373,16 +380,27 @@ export function M4ReplayThreatVisual({
|
||||
ariaLabel: `E47 semantic mask frame ${frame.sequence + 1}`,
|
||||
}
|
||||
: undefined;
|
||||
const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence
|
||||
? cameraPointOverlay.overlay
|
||||
: null;
|
||||
const pointCloudOverlay: RecordedEvidencePointCloudOverlayData | undefined =
|
||||
showMediaPoints && frame?.cameraProjection === "factory-kb4-exact"
|
||||
showMediaPoints && accumulatedCameraPoints
|
||||
? {
|
||||
pointsXyd: frame.cameraProjectedPointsXyd,
|
||||
sourcePointCount: frame.cameraProjectedSourceCount,
|
||||
projectedPointCount: frame.cameraProjectedPointCount,
|
||||
projection: "factory-kb4-exact",
|
||||
ariaLabel: `${evidenceLabel} LiDAR projection: ${frame.cameraProjectedSampleCount} points`,
|
||||
pointsXyd: accumulatedCameraPoints.pointsXyd,
|
||||
sourcePointCount: accumulatedCameraPoints.sourcePointCount,
|
||||
projectedPointCount: accumulatedCameraPoints.projectedPointCount,
|
||||
projection: accumulatedCameraPoints.projection,
|
||||
ariaLabel: `${evidenceLabel} causal registered LiDAR accumulation: ${accumulatedCameraPoints.sampleCount} points`,
|
||||
}
|
||||
: undefined;
|
||||
: showMediaPoints && frame?.cameraProjection === "factory-kb4-exact"
|
||||
? {
|
||||
pointsXyd: frame.cameraProjectedPointsXyd,
|
||||
sourcePointCount: frame.cameraProjectedSourceCount,
|
||||
projectedPointCount: frame.cameraProjectedPointCount,
|
||||
projection: "factory-kb4-exact",
|
||||
ariaLabel: `${evidenceLabel} current LiDAR projection: ${frame.cameraProjectedSampleCount} points`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const handleMediaModeChange = (next: M4ThreatMediaSelection) => {
|
||||
if (next === "none") return;
|
||||
@@ -452,7 +470,7 @@ export function M4ReplayThreatVisual({
|
||||
shape="pill"
|
||||
variant={showMediaPoints ? "primary" : "secondary"}
|
||||
aria-pressed={showMediaPoints}
|
||||
title="Exact LiDAR increment · factory KB4 camera projection"
|
||||
title="Actual registered LiDAR · causal accumulation · factory KB4 projection"
|
||||
onClick={() => setShowMediaPoints((visible) => !visible)}
|
||||
>
|
||||
POINTS
|
||||
@@ -608,9 +626,13 @@ export function M4ReplayThreatVisual({
|
||||
{frame.worldStateAvailable
|
||||
? " · world-state delivered"
|
||||
: ` · world-state gap (${frame.terminalOutcome})`}
|
||||
{pointCloudOverlay
|
||||
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount}`
|
||||
: ""}
|
||||
{accumulatedCameraPoints
|
||||
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
|
||||
: pointCloudOverlay
|
||||
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
|
||||
: showMediaPoints && cameraPointOverlay.error
|
||||
? " · накопленное camera cloud недоступно"
|
||||
: ""}
|
||||
{semantic && spatialSemanticFrame
|
||||
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
||||
: semantic ? " · semantic buffer" : ""}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
fetchM4ThreatCameraPointOverlay,
|
||||
fetchM4ThreatTimeline,
|
||||
fetchM4ThreatTimelineChunk,
|
||||
selectM4ThreatTimelineSequence,
|
||||
type M4ThreatCameraPointOverlay,
|
||||
type M4ThreatTimeline,
|
||||
type M4ThreatTimelineChunk,
|
||||
type M4ThreatTimelineFrame,
|
||||
} from "../../core/laboratory/m4ReplayThreat";
|
||||
|
||||
const REQUESTED_CHUNK_FRAMES = 24;
|
||||
const RETAINED_CHUNK_COUNT = 8;
|
||||
const PREFETCH_CHUNKS_AHEAD = 2;
|
||||
const RETAINED_CHUNK_COUNT = 4;
|
||||
const PREFETCH_CHUNKS_AHEAD = 1;
|
||||
const RETAINED_CAMERA_POINT_OVERLAYS = 12;
|
||||
|
||||
function errorMessage(error: unknown, fallback: string): string {
|
||||
return error instanceof Error && error.message.trim() ? error.message : fallback;
|
||||
@@ -24,8 +27,8 @@ export function m4ThreatChunkWindowStarts(
|
||||
): readonly number[] {
|
||||
if (chunkSize < 1 || frameCount < 1) return [];
|
||||
return Array.from(
|
||||
{ length: PREFETCH_CHUNKS_AHEAD + 2 },
|
||||
(_, index) => activeChunkStart + (index - 1) * chunkSize,
|
||||
{ length: PREFETCH_CHUNKS_AHEAD + 1 },
|
||||
(_, index) => activeChunkStart + index * chunkSize,
|
||||
).filter((start) => start >= 0 && start < frameCount);
|
||||
}
|
||||
|
||||
@@ -153,8 +156,11 @@ export function useM4ThreatTimelineFrame({
|
||||
.finally(() => {
|
||||
if (inFlight.current.get(start) === controller) inFlight.current.delete(start);
|
||||
});
|
||||
// Keep server-side JSON work strictly serialized: the active chunk is
|
||||
// loaded first, then the next chunk is prefetched on the following render.
|
||||
break;
|
||||
}
|
||||
}, [activeChunkStart, chunkSize, endpointRoot, resultId, timeline]);
|
||||
}, [activeChunkStart, chunkSize, chunks, endpointRoot, resultId, timeline]);
|
||||
|
||||
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
|
||||
if (activeSequence === null || activeChunkStart === null) return null;
|
||||
@@ -178,3 +184,127 @@ export function useM4ThreatTimelineFrame({
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useM4ThreatCameraPointOverlay({
|
||||
enabled,
|
||||
resultId,
|
||||
sequence,
|
||||
endpointRoot,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
resultId: string;
|
||||
sequence: number | null;
|
||||
endpointRoot?: string;
|
||||
}) {
|
||||
const [overlay, setOverlay] = useState<M4ThreatCameraPointOverlay | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const desiredRef = useRef<{
|
||||
resultId: string;
|
||||
sequence: number;
|
||||
endpointRoot?: string;
|
||||
} | null>(null);
|
||||
const cacheRef = useRef(new Map<string, M4ThreatCameraPointOverlay>());
|
||||
const runningRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const pumpRef = useRef<() => void>(() => undefined);
|
||||
|
||||
pumpRef.current = () => {
|
||||
if (runningRef.current || !desiredRef.current) return;
|
||||
runningRef.current = true;
|
||||
let settledKey: string | null = null;
|
||||
void (async () => {
|
||||
while (mountedRef.current) {
|
||||
const target = desiredRef.current;
|
||||
if (!target) break;
|
||||
const key = `${target.endpointRoot ?? ""}:${target.resultId}:${target.sequence}`;
|
||||
const cached = cacheRef.current.get(key);
|
||||
if (cached) {
|
||||
setOverlay(cached);
|
||||
setError(null);
|
||||
settledKey = key;
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const next = await fetchM4ThreatCameraPointOverlay(
|
||||
target.resultId,
|
||||
target.sequence,
|
||||
{ endpointRoot: target.endpointRoot },
|
||||
);
|
||||
cacheRef.current.set(key, next);
|
||||
while (cacheRef.current.size > RETAINED_CAMERA_POINT_OVERLAYS) {
|
||||
const oldest = cacheRef.current.keys().next().value as string | undefined;
|
||||
if (oldest === undefined) break;
|
||||
cacheRef.current.delete(oldest);
|
||||
}
|
||||
const desired = desiredRef.current;
|
||||
if (
|
||||
desired
|
||||
&& desired.resultId === target.resultId
|
||||
&& desired.sequence === target.sequence
|
||||
&& desired.endpointRoot === target.endpointRoot
|
||||
) {
|
||||
setOverlay(next);
|
||||
setError(null);
|
||||
settledKey = key;
|
||||
break;
|
||||
}
|
||||
} catch (caught: unknown) {
|
||||
const desired = desiredRef.current;
|
||||
if (
|
||||
desired
|
||||
&& desired.resultId === target.resultId
|
||||
&& desired.sequence === target.sequence
|
||||
&& desired.endpointRoot === target.endpointRoot
|
||||
) {
|
||||
setError(errorMessage(caught, "Накопленное LiDAR-облако камеры недоступно."));
|
||||
settledKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})().finally(() => {
|
||||
runningRef.current = false;
|
||||
const desired = desiredRef.current;
|
||||
const desiredKey = desired
|
||||
? `${desired.endpointRoot ?? ""}:${desired.resultId}:${desired.sequence}`
|
||||
: null;
|
||||
if (mountedRef.current && desiredKey && desiredKey !== settledKey) pumpRef.current();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
desiredRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
cacheRef.current.clear();
|
||||
setOverlay(null);
|
||||
setError(null);
|
||||
}, [resultId]);
|
||||
|
||||
useEffect(() => {
|
||||
desiredRef.current = enabled && sequence !== null
|
||||
? { resultId, sequence, endpointRoot }
|
||||
: null;
|
||||
if (!desiredRef.current) {
|
||||
setOverlay(null);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
const key = `${endpointRoot ?? ""}:${resultId}:${sequence}`;
|
||||
const cached = cacheRef.current.get(key);
|
||||
setOverlay(cached ?? null);
|
||||
if (cached) setError(null);
|
||||
pumpRef.current();
|
||||
}, [enabled, endpointRoot, resultId, sequence]);
|
||||
|
||||
return {
|
||||
overlay,
|
||||
loading: enabled && sequence !== null && overlay?.sequence !== sequence && !error,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user