fix(lab): stabilize replay and densify LiDAR overlay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-25 17:51:50 +03:00
parent 15be698097
commit b5080671c9
8 changed files with 520 additions and 52 deletions
@@ -6,15 +6,20 @@ export interface RecordedEvidencePointCloudOverlayData {
pointsXyd: readonly RecordedEvidenceProjectedPoint[];
sourcePointCount: number;
projectedPointCount: number;
projection: "factory-kb4-exact";
projection:
| "factory-kb4-exact"
| "factory-kb4-causal-registered-accumulation";
ariaLabel: string;
}
const DEPTH_BUCKETS = 64;
function depthColor(depthM: number): string {
function depthBucket(depthM: number): number {
const normalized = Math.max(0, Math.min(1, (depthM - 0.5) / 24));
const bucket = Math.round(normalized * (DEPTH_BUCKETS - 1));
return Math.round(normalized * (DEPTH_BUCKETS - 1));
}
function depthColor(bucket: number): string {
const hue = 18 + bucket / (DEPTH_BUCKETS - 1) * 190;
return `hsla(${hue}, 96%, 62%, 0.86)`;
}
@@ -52,16 +57,26 @@ export function RecordedEvidencePointCloudOverlay({
const offsetX = (width - imageWidth * scale) / 2;
const offsetY = (height - imageHeight * scale) / 2;
const radius = Math.max(0.8, Math.min(2.2, scale * 1.45));
const buckets = Array.from(
{ length: DEPTH_BUCKETS },
(): [number, number][] => [],
);
for (const [imageX, imageY, depthM] of overlay.pointsXyd) {
context.beginPath();
context.arc(
const bucket = depthBucket(depthM);
buckets[bucket]!.push([
offsetX + imageX * scale,
offsetY + imageY * scale,
radius,
0,
Math.PI * 2,
);
context.fillStyle = depthColor(depthM);
]);
}
for (let bucket = 0; bucket < buckets.length; bucket += 1) {
const points = buckets[bucket];
if (!points?.length) continue;
context.beginPath();
for (const [x, y] of points) {
context.moveTo(x + radius, y);
context.arc(x, y, radius, 0, Math.PI * 2);
}
context.fillStyle = depthColor(bucket);
context.fill();
}
};
@@ -170,7 +170,8 @@ export interface M4ThreatTimeline {
pointSampleLimit: number;
maximumSourcePointsPerFrame: number;
pointDelivery: "exact-current-increment";
cameraPointDelivery: "factory-kb4-projected-current-increment" | null;
cameraPointDelivery: "factory-kb4-causal-registered-accumulation" | null;
cameraPointWindowSeconds: number;
cameraPointSampleLimit: number;
worldStateDelivery: "source-paced-latest-wins" | null;
worldStateFrameCount: number;
@@ -189,6 +190,21 @@ export interface M4ThreatTimeline {
corridor: M4ThreatVisualFrame["corridor"];
}
export interface M4ThreatCameraPointOverlay {
resultId: string;
sequence: number;
sourceTimeNs: number;
pointsXyd: readonly (readonly [number, number, number])[];
sourceFrameCount: number;
sourcePointCount: number;
frontPointCount: number;
projectedPointCount: number;
sampleCount: number;
windowSeconds: number;
projection: "factory-kb4-causal-registered-accumulation";
authority: "visual-derived";
}
export interface M4ThreatTimelineChunk {
resultId: string;
startSequence: number;
@@ -651,9 +667,12 @@ export async function fetchM4ThreatTimeline(
? null
: exact(
payload.camera_point_delivery,
"factory-kb4-projected-current-increment",
"factory-kb4-causal-registered-accumulation",
"M4.6 camera point delivery",
),
cameraPointWindowSeconds: payload.camera_point_window_seconds === undefined
? 0
: number(payload.camera_point_window_seconds, "M4.6 camera point window"),
cameraPointSampleLimit: payload.camera_point_sample_limit === undefined
? 0
: integer(payload.camera_point_sample_limit, "M4.6 camera point limit"),
@@ -760,6 +779,62 @@ export async function fetchM4ThreatTimelineChunk(
};
}
export async function fetchM4ThreatCameraPointOverlay(
result: string,
sequence: number,
{
fetcher = fetch,
signal,
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
endpointRoot?: string;
} = {},
): Promise<M4ThreatCameraPointOverlay> {
const response = await fetcher(
`${endpointRoot}/${result}/timeline/frames/${sequence}/camera-points`,
{ headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new M4ThreatContractError(`M4.6 camera points: HTTP ${response.status}.`);
}
const payload = object(await response.json(), "M4.6 camera points");
exact(
payload.schema_version,
"missioncore.m48s-camera-point-overlay/v1",
"M4.6 camera point schema",
);
exact(payload.result_id, result, "M4.6 camera point result");
exact(payload.sequence, sequence, "M4.6 camera point sequence");
exact(payload.authority, "visual-derived", "M4.6 camera point authority");
const points = array(payload.points_xyd, "M4.6 accumulated camera points").map(
(point) => vector(point, 3, "M4.6 accumulated camera point") as [number, number, number],
);
const sampleCount = integer(payload.sample_count, "M4.6 camera point sample count");
if (points.length !== sampleCount) {
throw new M4ThreatContractError("M4.6 camera point sample count: нарушен контракт.");
}
return {
resultId: result,
sequence,
sourceTimeNs: integer(payload.source_time_ns, "M4.6 camera point source time"),
pointsXyd: points,
sourceFrameCount: integer(payload.source_frame_count, "M4.6 camera source frames"),
sourcePointCount: integer(payload.source_point_count, "M4.6 camera source points"),
frontPointCount: integer(payload.front_point_count, "M4.6 camera front points"),
projectedPointCount: integer(payload.projected_point_count, "M4.6 camera projected points"),
sampleCount,
windowSeconds: number(payload.window_seconds, "M4.6 camera point window"),
projection: exact(
payload.projection,
"factory-kb4-causal-registered-accumulation",
"M4.6 camera point projection",
),
authority: "visual-derived",
};
}
function parseTimelineFrame(
value: unknown,
result: string,
@@ -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,
};
}