feat(lab): stabilize autonomous TGS playback
This commit is contained in:
@@ -8,13 +8,16 @@ import {
|
||||
fetchE47SemanticSlamResult,
|
||||
type E47SemanticSlamResult,
|
||||
} from "../../core/laboratory/e47SemanticSlam";
|
||||
import {
|
||||
fetchM49PhysicalSafetyPlaybackIdForSource,
|
||||
M49PhysicalSafetyPlaybackBuffer,
|
||||
type M49PhysicalSafetyPlaybackProgress,
|
||||
} from "../../core/laboratory/m49PhysicalSafetyPlayback";
|
||||
import {
|
||||
fetchM49TgsFullShadowPlaybackPack,
|
||||
type M49TgsFullShadowResult,
|
||||
type M49TgsFullShadowPlaybackPack,
|
||||
type M49TgsFullShadowPlaybackProgress,
|
||||
type M49TgsFullShadowSpatial,
|
||||
type M49TgsFullShadowStateCode,
|
||||
} from "../../core/laboratory/m49TgsFullShadow";
|
||||
import {
|
||||
M4ReplayThreatVisual,
|
||||
@@ -33,13 +36,6 @@ const PALETTE: readonly RecordedEvidenceSemanticPaletteEntry[] = [
|
||||
{ classId: 3, color: { kind: "token", token: "--nodedc-warning-rgb" } },
|
||||
];
|
||||
|
||||
function cellState(code: M49TgsFullShadowStateCode): M4ReplayClassifiedSpatialFrame["cellsMapGravityLocal"][number]["state"] {
|
||||
if (code === 1) return "ground-support";
|
||||
if (code === 2) return "nonground-occupied";
|
||||
if (code === 3) return "unknown-rejected";
|
||||
return "unobserved";
|
||||
}
|
||||
|
||||
function message(error: unknown): string {
|
||||
return error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
@@ -51,6 +47,9 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
||||
const [semantic, setSemantic] = useState<E47SemanticSlamResult | null>(null);
|
||||
const [semanticError, setSemanticError] = useState<string | null>(null);
|
||||
const [playbackPack, setPlaybackPack] = useState<M49TgsFullShadowPlaybackPack | null>(null);
|
||||
const [physicalPlayback, setPhysicalPlayback] = useState<M49PhysicalSafetyPlaybackBuffer | null>(null);
|
||||
const [physicalProgress, setPhysicalProgress] = useState<M49PhysicalSafetyPlaybackProgress | null>(null);
|
||||
const [physicalRevision, setPhysicalRevision] = useState(0);
|
||||
const [playbackProgress, setPlaybackProgress] = useState<M49TgsFullShadowPlaybackProgress>({
|
||||
phase: "manifest",
|
||||
trackId: null,
|
||||
@@ -83,12 +82,27 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setPlaybackPack(null);
|
||||
setPhysicalPlayback(null);
|
||||
setPhysicalProgress(null);
|
||||
setPhysicalRevision(0);
|
||||
setPlaybackProgress({ phase: "manifest", trackId: null, loadedBytes: 0, totalBytes: 0 });
|
||||
setError(null);
|
||||
void fetchM49TgsFullShadowPlaybackPack(result.resultId, {
|
||||
void fetchM49PhysicalSafetyPlaybackIdForSource(result.resultId, {
|
||||
signal: controller.signal,
|
||||
onProgress: setPlaybackProgress,
|
||||
}).then((pack) => {
|
||||
}).then(async (physicalResultId) => {
|
||||
if (controller.signal.aborted) return;
|
||||
if (physicalResultId) {
|
||||
const playback = await M49PhysicalSafetyPlaybackBuffer.open(physicalResultId, {
|
||||
signal: controller.signal,
|
||||
onProgress: setPhysicalProgress,
|
||||
});
|
||||
if (!controller.signal.aborted) setPhysicalPlayback(playback);
|
||||
return;
|
||||
}
|
||||
const pack = await fetchM49TgsFullShadowPlaybackPack(result.resultId, {
|
||||
signal: controller.signal,
|
||||
onProgress: setPlaybackProgress,
|
||||
});
|
||||
if (!controller.signal.aborted) setPlaybackPack(pack);
|
||||
}).catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) setError(message(caught));
|
||||
@@ -96,69 +110,84 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
||||
return () => controller.abort();
|
||||
}, [result.resultId]);
|
||||
|
||||
const spatial = useMemo<M49TgsFullShadowSpatial | null>(() => {
|
||||
if (activeSequence === null || !playbackPack) return null;
|
||||
useEffect(() => {
|
||||
if (activeSequence === null || !physicalPlayback) return;
|
||||
let active = true;
|
||||
const resident = physicalPlayback.frameIfResident(activeSequence);
|
||||
void physicalPlayback.prepare(activeSequence).then(() => {
|
||||
if (active && !resident) setPhysicalRevision((value) => value + 1);
|
||||
}).catch((caught: unknown) => {
|
||||
if (active) setError(message(caught));
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [activeSequence, physicalPlayback]);
|
||||
|
||||
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
|
||||
if (activeSequence === null) return null;
|
||||
if (physicalPlayback) {
|
||||
const frame = physicalPlayback.frameIfResident(activeSequence);
|
||||
if (!frame) return null;
|
||||
return {
|
||||
sourceSequence: activeSequence,
|
||||
sampleAvailable: frame.metadata.sampleAvailable,
|
||||
sourcePointCount: frame.metadata.metrics.eligible_point_count ?? 0,
|
||||
pointsMapGravityLocalXyzM: [],
|
||||
pointClassIds: [],
|
||||
cellsMapGravityLocal: [],
|
||||
packedCellsMapGravityLocal: {
|
||||
centersXyM: frame.centersXyM,
|
||||
stateCodes: frame.states,
|
||||
zBoundsM: frame.zBoundsM,
|
||||
},
|
||||
cellSizeM: frame.cellSizeM,
|
||||
classes: CLASSES,
|
||||
palette: PALETTE,
|
||||
};
|
||||
}
|
||||
if (!playbackPack) return null;
|
||||
const frame = playbackPack.frames[activeSequence];
|
||||
if (!frame) return null;
|
||||
const cellOffset = activeSequence * playbackPack.cellCount;
|
||||
const zOffset = cellOffset * 2;
|
||||
const states = Array.from(
|
||||
playbackPack.states.subarray(cellOffset, cellOffset + playbackPack.cellCount),
|
||||
(value) => value as M49TgsFullShadowStateCode,
|
||||
);
|
||||
const zBoundsM = Array.from({ length: playbackPack.cellCount }, (_, index) => {
|
||||
const minimum = playbackPack.zBoundsM[zOffset + index * 2]!;
|
||||
const maximum = playbackPack.zBoundsM[zOffset + index * 2 + 1]!;
|
||||
return [
|
||||
Number.isFinite(minimum) ? minimum : null,
|
||||
Number.isFinite(maximum) ? maximum : null,
|
||||
] as const;
|
||||
});
|
||||
return {
|
||||
resultId: playbackPack.resultId,
|
||||
sourceSequence: activeSequence,
|
||||
sourceFrameIndex: frame.sourceFrameIndex,
|
||||
sessionSeconds: frame.sessionSeconds,
|
||||
sampleAvailable: frame.sampleAvailable,
|
||||
costmap: {
|
||||
cellSizeM: result.configuration.cellSizeM,
|
||||
radiusM: result.configuration.radiusM,
|
||||
sourcePointCount: frame.metrics.eligiblePointCount,
|
||||
pointsMapGravityLocalXyzM: [],
|
||||
pointClassIds: [],
|
||||
cellsMapGravityLocal: [],
|
||||
packedCellsMapGravityLocal: {
|
||||
centersXyM: playbackPack.centersXyM,
|
||||
states,
|
||||
zBoundsM,
|
||||
stateCodes: playbackPack.states.subarray(
|
||||
cellOffset,
|
||||
cellOffset + playbackPack.cellCount,
|
||||
),
|
||||
zBoundsM: playbackPack.zBoundsM.subarray(
|
||||
zOffset,
|
||||
zOffset + playbackPack.cellCount * 2,
|
||||
),
|
||||
},
|
||||
metrics: frame.metrics,
|
||||
cellSizeM: result.configuration.cellSizeM,
|
||||
classes: CLASSES,
|
||||
palette: PALETTE,
|
||||
};
|
||||
}, [activeSequence, playbackPack, result.configuration.cellSizeM, result.configuration.radiusM]);
|
||||
const loading = activeSequence !== null && !spatial && !error;
|
||||
}, [activeSequence, physicalPlayback, physicalRevision, playbackPack, result.configuration.cellSizeM]);
|
||||
const loading = activeSequence !== null && !classifiedFrame && !error;
|
||||
const progressPercent = playbackProgress.totalBytes > 0
|
||||
? Math.min(100, Math.round((playbackProgress.loadedBytes / playbackProgress.totalBytes) * 100))
|
||||
: 0;
|
||||
const loadingLabel = playbackProgress.phase === "manifest"
|
||||
const loadingLabel = physicalProgress
|
||||
? physicalProgress.phase === "ready"
|
||||
? "Sealed TGS playback готов"
|
||||
: `Буферизуем sealed TGS · ${physicalProgress.residentChunkIndexes.length}/3 chunks · ${(physicalProgress.loadedBytes / 1_048_576).toFixed(1)} МБ`
|
||||
: playbackProgress.phase === "manifest"
|
||||
? "Проверяем playback manifest"
|
||||
: playbackProgress.phase === "ready"
|
||||
? "TGS playback готов"
|
||||
: `Подготавливаем TGS playback · ${progressPercent}% · ${(playbackProgress.loadedBytes / 1_048_576).toFixed(1)}/${(playbackProgress.totalBytes / 1_048_576).toFixed(1)} МБ`;
|
||||
|
||||
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
|
||||
if (!spatial) return null;
|
||||
return {
|
||||
sourceSequence: spatial.sourceSequence,
|
||||
sampleAvailable: spatial.sampleAvailable,
|
||||
sourcePointCount: spatial.metrics.eligiblePointCount,
|
||||
pointsMapGravityLocalXyzM: [],
|
||||
pointClassIds: [],
|
||||
cellsMapGravityLocal: spatial.costmap.centersXyM.map((center, index) => ({
|
||||
centerXyM: center,
|
||||
zBoundsM: spatial.costmap.zBoundsM[index]!,
|
||||
state: cellState(spatial.costmap.states[index]!),
|
||||
})),
|
||||
cellSizeM: spatial.costmap.cellSizeM,
|
||||
classes: CLASSES,
|
||||
palette: PALETTE,
|
||||
};
|
||||
}, [spatial]);
|
||||
|
||||
const handleSequenceChange = useCallback((sequence: number | null) => {
|
||||
setActiveSequence(sequence);
|
||||
}, []);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
LaboratoryMetricEvidenceScene,
|
||||
type LaboratoryMetricCellEvidence,
|
||||
type LaboratoryMetricEvidenceSceneHandle,
|
||||
type LaboratoryMetricPackedCellEvidence,
|
||||
type LaboratoryMetricSceneMode,
|
||||
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
@@ -118,6 +119,11 @@ export interface M4ReplayClassifiedSpatialFrame {
|
||||
zBoundsM: readonly [number | null, number | null];
|
||||
state: LaboratoryMetricCellEvidence["state"];
|
||||
}[];
|
||||
packedCellsMapGravityLocal?: {
|
||||
centersXyM: Float32Array;
|
||||
zBoundsM: Float32Array;
|
||||
stateCodes: Uint8Array;
|
||||
};
|
||||
cellSizeM: number;
|
||||
classes: readonly RecordedEvidenceSemanticClass[];
|
||||
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||
@@ -414,19 +420,41 @@ export function M4ReplayThreatVisual({
|
||||
resultId: string;
|
||||
frame: M4ReplayClassifiedSpatialFrame;
|
||||
} | null>(null);
|
||||
if (classifiedSpatialLayer?.frame) {
|
||||
lastClassifiedSpatialFrameRef.current = { resultId, frame: classifiedSpatialLayer.frame };
|
||||
const incomingClassifiedSpatialFrame = classifiedSpatialLayer?.frame ?? null;
|
||||
if (incomingClassifiedSpatialFrame && incomingClassifiedSpatialFrame.sampleAvailable !== false) {
|
||||
lastClassifiedSpatialFrameRef.current = { resultId, frame: incomingClassifiedSpatialFrame };
|
||||
}
|
||||
const lastAvailableClassifiedSpatialFrame = lastClassifiedSpatialFrameRef.current?.resultId === resultId
|
||||
? lastClassifiedSpatialFrameRef.current.frame
|
||||
: null;
|
||||
// `undefined !== false` is true, so the optional-chain form here used to
|
||||
// select `null` during every short chunk miss. That unmounted the WebGL
|
||||
// scene, flashed the alert state and recreated OrbitControls at the default
|
||||
// view on the next frame. Keep the last complete volume until the exact
|
||||
// classified frame is resident again.
|
||||
const displayedClassifiedSpatialFrame = classifiedSpatialFrame
|
||||
?? (lastClassifiedSpatialFrameRef.current?.resultId === resultId
|
||||
? lastClassifiedSpatialFrameRef.current.frame
|
||||
: null);
|
||||
&& classifiedSpatialFrame.sampleAvailable !== false
|
||||
? classifiedSpatialFrame
|
||||
: lastAvailableClassifiedSpatialFrame ?? classifiedSpatialFrame;
|
||||
const displayedClassifiedFrameHeld = Boolean(
|
||||
classifiedSpatialFrame
|
||||
&& displayedClassifiedSpatialFrame
|
||||
&& classifiedSpatialFrame.sourceSequence !== displayedClassifiedSpatialFrame.sourceSequence,
|
||||
);
|
||||
const classifiedContextSpatialFrame = displayedClassifiedSpatialFrame
|
||||
? timelineFrame.availableFrames.find(
|
||||
(candidate) => candidate.spatialAvailable
|
||||
&& candidate.sequence === displayedClassifiedSpatialFrame.sourceSequence,
|
||||
) ?? (spatialFrame?.sequence === displayedClassifiedSpatialFrame.sourceSequence
|
||||
? spatialFrame
|
||||
: null)
|
||||
: null;
|
||||
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
|
||||
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
|
||||
const mapGravityLocalSensorToBodyGround = useCallback((
|
||||
point: readonly [number, number, number],
|
||||
): readonly [number, number, number] => {
|
||||
const basis = activeSpatialFrame?.bodyFrame?.basisMapFromBody;
|
||||
const basis = classifiedContextSpatialFrame?.bodyFrame?.basisMapFromBody;
|
||||
const rotated: readonly [number, number, number] = basis ? [
|
||||
basis[0][0] * point[0] + basis[1][0] * point[1] + basis[2][0] * point[2],
|
||||
basis[0][1] * point[0] + basis[1][1] * point[1] + basis[2][1] * point[2],
|
||||
@@ -435,7 +463,7 @@ export function M4ReplayThreatVisual({
|
||||
// TGS evidence is translation-only map-gravity-local with the current LiDAR
|
||||
// as its origin. The metric scene uses the body ground projection as z=0.
|
||||
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
|
||||
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
|
||||
}, [classifiedContextSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
|
||||
const classifiedPointsBody = useMemo(
|
||||
() => displayedClassifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
||||
mapGravityLocalSensorToBodyGround,
|
||||
@@ -465,20 +493,58 @@ export function M4ReplayThreatVisual({
|
||||
}) ?? [],
|
||||
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
|
||||
);
|
||||
const classifiedCellCounts = useMemo(() => ({
|
||||
ground: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||
(cell) => cell.state === "ground-support",
|
||||
).length ?? 0,
|
||||
occupied: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||
(cell) => cell.state === "nonground-occupied",
|
||||
).length ?? 0,
|
||||
rejected: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||
(cell) => cell.state === "unknown-rejected",
|
||||
).length ?? 0,
|
||||
unobserved: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||
(cell) => cell.state === "unobserved",
|
||||
).length ?? 0,
|
||||
}), [classifiedSpatialFrame]);
|
||||
const classifiedPackedCellsBody = useMemo<LaboratoryMetricPackedCellEvidence | undefined>(() => {
|
||||
const packed = displayedClassifiedSpatialFrame?.packedCellsMapGravityLocal;
|
||||
if (!packed) return undefined;
|
||||
const cellCount = packed.stateCodes.length;
|
||||
if (packed.centersXyM.length !== cellCount * 2 || packed.zBoundsM.length !== cellCount * 2) {
|
||||
return undefined;
|
||||
}
|
||||
const centersBodyXyM = new Float32Array(cellCount * 2);
|
||||
const zBoundsM = new Float32Array(cellCount * 2);
|
||||
for (let index = 0; index < cellCount; index += 1) {
|
||||
const body = mapGravityLocalSensorToBodyGround([
|
||||
packed.centersXyM[index * 2]!,
|
||||
packed.centersXyM[index * 2 + 1]!,
|
||||
0,
|
||||
]);
|
||||
centersBodyXyM[index * 2] = body[0];
|
||||
centersBodyXyM[index * 2 + 1] = body[1];
|
||||
const minimum = packed.zBoundsM[index * 2]!;
|
||||
const maximum = packed.zBoundsM[index * 2 + 1]!;
|
||||
zBoundsM[index * 2] = Number.isFinite(minimum)
|
||||
? minimum + nominalSensorHeightM
|
||||
: Number.NaN;
|
||||
zBoundsM[index * 2 + 1] = Number.isFinite(maximum)
|
||||
? maximum + nominalSensorHeightM
|
||||
: Number.NaN;
|
||||
}
|
||||
return { centersBodyXyM, zBoundsM, stateCodes: packed.stateCodes };
|
||||
}, [displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM]);
|
||||
const classifiedCellCounts = useMemo(() => {
|
||||
const counts = { ground: 0, occupied: 0, rejected: 0, unobserved: 0 };
|
||||
const packed = classifiedSpatialFrame?.packedCellsMapGravityLocal;
|
||||
if (packed) {
|
||||
for (const code of packed.stateCodes) {
|
||||
if (code === 1) counts.ground += 1;
|
||||
else if (code === 2) counts.occupied += 1;
|
||||
else if (code === 3) counts.rejected += 1;
|
||||
else if (code === 0) counts.unobserved += 1;
|
||||
else counts.rejected += 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
for (const cell of classifiedSpatialFrame?.cellsMapGravityLocal ?? []) {
|
||||
if (cell.state === "ground-support") counts.ground += 1;
|
||||
else if (cell.state === "nonground-occupied") counts.occupied += 1;
|
||||
else if (cell.state === "unknown-rejected") counts.rejected += 1;
|
||||
else counts.unobserved += 1;
|
||||
}
|
||||
return counts;
|
||||
}, [classifiedSpatialFrame]);
|
||||
const classifiedCellCount = classifiedSpatialFrame?.packedCellsMapGravityLocal?.stateCodes.length
|
||||
?? classifiedSpatialFrame?.cellsMapGravityLocal.length
|
||||
?? 0;
|
||||
const sceneObstacles = useMemo(() => spatialFrame?.metricObstacles.map((obstacle) => ({
|
||||
id: obstacle.componentId,
|
||||
decision: obstacle.assessment.decision,
|
||||
@@ -840,14 +906,16 @@ export function M4ReplayThreatVisual({
|
||||
<strong>{classifiedSpatialLayer
|
||||
? classifiedSpatialFrame
|
||||
? replaceClassifiedPointCloud
|
||||
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} cells`
|
||||
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} TGS cells`
|
||||
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedCellCount.toLocaleString("ru-RU")} cells`
|
||||
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedCellCount.toLocaleString("ru-RU")} TGS cells`
|
||||
: "TGS spatial buffer"
|
||||
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
|
||||
<small>{classifiedSpatialLayer
|
||||
? classifiedSpatialFrame
|
||||
? classifiedSpatialFrame.sampleAvailable === false
|
||||
? "LiDAR отсутствует · все ячейки принудительно UNOBSERVED · causal rolling 1 s"
|
||||
? displayedClassifiedFrameHeld && displayedClassifiedSpatialFrame
|
||||
? `LiDAR отсутствует · current UNOBSERVED · объёмный контекст удержан с frame ${displayedClassifiedSpatialFrame.sourceSequence + 1}`
|
||||
: "LiDAR отсутствует · все ячейки принудительно UNOBSERVED · causal rolling 1 s"
|
||||
: activeSpatialFrame
|
||||
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
|
||||
: "TGS рассчитан · linked source cloud недоступен для этого кадра"
|
||||
@@ -987,37 +1055,36 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{(!classifiedSpatialLayer ? spatialFrame : displayedClassifiedSpatialFrame) ? (
|
||||
<LaboratoryMetricEvidenceScene
|
||||
ref={metricSceneRef}
|
||||
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedPointsBody
|
||||
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
|
||||
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
|
||||
obstacles={displayedClassifiedSpatialFrame ? [] : sceneObstacles}
|
||||
rig={timeline.rig}
|
||||
corridor={timeline.corridor}
|
||||
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
|
||||
mode={spatialMode}
|
||||
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
||||
showCurrentIncrement={showCurrentIncrement}
|
||||
showLocalSurface={showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
showLowStep={displayedClassifiedSpatialFrame ? false : showLowStep}
|
||||
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.pointClassIds
|
||||
: alignedSemanticPointIds}
|
||||
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.classes
|
||||
: semanticClasses}
|
||||
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.palette
|
||||
: semanticPalette}
|
||||
classifiedCells={classifiedCellsBody}
|
||||
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
|
||||
showClassifiedCells={showRollingMap}
|
||||
/>
|
||||
) : null}
|
||||
<LaboratoryMetricEvidenceScene
|
||||
ref={metricSceneRef}
|
||||
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedPointsBody
|
||||
: classifiedContextSpatialFrame?.pointCloudBodyXyzM ?? []}
|
||||
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
|
||||
obstacles={classifiedSpatialLayer ? [] : sceneObstacles}
|
||||
rig={timeline.rig}
|
||||
corridor={timeline.corridor}
|
||||
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
|
||||
mode={spatialMode}
|
||||
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
||||
showCurrentIncrement={showCurrentIncrement}
|
||||
showLocalSurface={showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
showLowStep={classifiedSpatialLayer ? false : showLowStep}
|
||||
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.pointClassIds
|
||||
: alignedSemanticPointIds}
|
||||
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.classes
|
||||
: semanticClasses}
|
||||
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.palette
|
||||
: semanticPalette}
|
||||
classifiedCells={classifiedCellsBody}
|
||||
classifiedPackedCells={classifiedPackedCellsBody}
|
||||
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
|
||||
showClassifiedCells={showRollingMap}
|
||||
/>
|
||||
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
|
||||
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
|
||||
{classifiedSpatialLayer.loading || displayingBufferedFrame
|
||||
@@ -1033,7 +1100,10 @@ export function M4ReplayThreatVisual({
|
||||
) : null}
|
||||
{classifiedSpatialFrame?.sampleAvailable === false ? (
|
||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||
Кадр {classifiedSpatialFrame.sourceSequence + 1}: LiDAR отсутствует; все 2 244 TGS-ячейки явно UNOBSERVED.
|
||||
Кадр {classifiedSpatialFrame.sourceSequence + 1}: LiDAR отсутствует; current safety — все {classifiedCellCount.toLocaleString("ru-RU")} TGS-ячейки UNOBSERVED
|
||||
{displayedClassifiedFrameHeld && displayedClassifiedSpatialFrame
|
||||
? `; для ориентации удержан последний объёмный контекст кадра ${displayedClassifiedSpatialFrame.sourceSequence + 1}.`
|
||||
: "."}
|
||||
</div>
|
||||
) : classifiedSpatialFrame && !activeSpatialFrame ? (
|
||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||
|
||||
@@ -2,13 +2,14 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
fetchM4ThreatCameraPointOverlay,
|
||||
fetchM4ThreatPlaybackPointPack,
|
||||
fetchM4ThreatPlaybackManifest,
|
||||
fetchM4ThreatPlaybackPointChunk,
|
||||
fetchM4ThreatTimeline,
|
||||
fetchM4ThreatTimelineChunk,
|
||||
M4_THREAT_TIMELINE_ENDPOINT_ROOT,
|
||||
selectM4ThreatTimelineSequence,
|
||||
type M4ThreatCameraPointOverlay,
|
||||
type M4ThreatPlaybackPointPack,
|
||||
type M4ThreatPlaybackManifest,
|
||||
type M4ThreatPlaybackProgress,
|
||||
type M4ThreatTimeline,
|
||||
type M4ThreatTimelineChunk,
|
||||
@@ -86,7 +87,7 @@ export function useM4ThreatTimelineFrame({
|
||||
() => new Map(),
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pointPack, setPointPack] = useState<M4ThreatPlaybackPointPack | null>(null);
|
||||
const [playbackManifest, setPlaybackManifest] = useState<M4ThreatPlaybackManifest | null>(null);
|
||||
const [playbackProgress, setPlaybackProgress] = useState<M4ThreatPlaybackProgress>({
|
||||
phase: "manifest",
|
||||
loadedBytes: 0,
|
||||
@@ -102,7 +103,7 @@ export function useM4ThreatTimelineFrame({
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setPointPack(null);
|
||||
setPlaybackManifest(null);
|
||||
setPlaybackError(null);
|
||||
setPlaybackProgress({ phase: "manifest", loadedBytes: 0, totalBytes: 0 });
|
||||
if (!timeline) return () => controller.abort();
|
||||
@@ -110,15 +111,15 @@ export function useM4ThreatTimelineFrame({
|
||||
setPlaybackProgress({ phase: "ready", loadedBytes: 0, totalBytes: 0 });
|
||||
return () => controller.abort();
|
||||
}
|
||||
void fetchM4ThreatPlaybackPointPack(resultId, {
|
||||
void fetchM4ThreatPlaybackManifest(resultId, {
|
||||
signal: controller.signal,
|
||||
endpointRoot,
|
||||
onProgress: (progress) => {
|
||||
if (!controller.signal.aborted) setPlaybackProgress(progress);
|
||||
},
|
||||
})
|
||||
.then((pack) => {
|
||||
if (!controller.signal.aborted) setPointPack(pack);
|
||||
.then((manifest) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setPlaybackManifest(manifest);
|
||||
setPlaybackProgress({ phase: "ready", loadedBytes: 0, totalBytes: 0 });
|
||||
}
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
@@ -157,7 +158,7 @@ export function useM4ThreatTimelineFrame({
|
||||
activeChunkStartRef.current = activeChunkStart;
|
||||
|
||||
useEffect(() => {
|
||||
if (!timeline || activeChunkStart === null || (binaryPlayback && !pointPack)) return;
|
||||
if (!timeline || activeChunkStart === null || (binaryPlayback && !playbackManifest)) return;
|
||||
const starts = m4ThreatChunkWindowStarts(
|
||||
activeChunkStart,
|
||||
chunkSize,
|
||||
@@ -168,12 +169,28 @@ export function useM4ThreatTimelineFrame({
|
||||
if (chunksRef.current.has(start) || inFlight.current.has(start)) continue;
|
||||
const controller = new AbortController();
|
||||
inFlight.current.set(start, controller);
|
||||
void fetchM4ThreatTimelineChunk(resultId, start, chunkSize, {
|
||||
signal: controller.signal,
|
||||
endpointRoot,
|
||||
cameraObstacleProjectionDelivery: timeline.cameraObstacleProjectionDelivery,
|
||||
playbackPointPack: binaryPlayback ? pointPack ?? undefined : undefined,
|
||||
})
|
||||
void (async () => {
|
||||
const playbackPointPack = binaryPlayback && playbackManifest
|
||||
? await fetchM4ThreatPlaybackPointChunk(
|
||||
playbackManifest,
|
||||
Math.floor(start / playbackManifest.chunkFrameCount),
|
||||
{
|
||||
signal: controller.signal,
|
||||
onProgress: (progress) => {
|
||||
if (!controller.signal.aborted && start === activeChunkStartRef.current) {
|
||||
setPlaybackProgress(progress);
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
return fetchM4ThreatTimelineChunk(resultId, start, chunkSize, {
|
||||
signal: controller.signal,
|
||||
endpointRoot,
|
||||
cameraObstacleProjectionDelivery: timeline.cameraObstacleProjectionDelivery,
|
||||
playbackPointPack,
|
||||
});
|
||||
})()
|
||||
.then((chunk) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setChunks((current) => {
|
||||
@@ -203,7 +220,7 @@ export function useM4ThreatTimelineFrame({
|
||||
// loaded first, then the next chunk is prefetched on the following render.
|
||||
break;
|
||||
}
|
||||
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, pointPack, resultId, timeline]);
|
||||
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, playbackManifest, resultId, timeline]);
|
||||
|
||||
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
|
||||
if (activeSequence === null || activeChunkStart === null) return null;
|
||||
@@ -230,8 +247,8 @@ export function useM4ThreatTimelineFrame({
|
||||
? "Проверяем M4 playback manifest"
|
||||
: playbackProgress.phase === "ready"
|
||||
? "Подготавливаем текущий spatial-кадр"
|
||||
: `${playbackProgress.phase === "verify" ? "Проверяем" : "Загружаем"} M4 spatial playback · ${playbackProgress.totalBytes > 0
|
||||
? `${Math.min(100, Math.round(playbackProgress.loadedBytes / playbackProgress.totalBytes * 100))}% · ${(playbackProgress.loadedBytes / 1_048_576).toFixed(1)}/${(playbackProgress.totalBytes / 1_048_576).toFixed(1)} МБ`
|
||||
: `${playbackProgress.phase === "verify" ? "Проверяем" : "Буферизуем"} текущий spatial chunk · ${playbackProgress.totalBytes > 0
|
||||
? `${Math.min(100, Math.round(playbackProgress.loadedBytes / playbackProgress.totalBytes * 100))}% · ${(playbackProgress.loadedBytes / 1_048_576).toFixed(1)} МБ`
|
||||
: "0%"}`,
|
||||
error: playbackError ?? error,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user