fix(lab): preserve replay view and restore SLAM semantics
This commit is contained in:
@@ -4,6 +4,10 @@ import type {
|
||||
RecordedEvidenceSemanticClass,
|
||||
RecordedEvidenceSemanticPaletteEntry,
|
||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||
import {
|
||||
fetchE47SemanticSlamResult,
|
||||
type E47SemanticSlamResult,
|
||||
} from "../../core/laboratory/e47SemanticSlam";
|
||||
import {
|
||||
fetchM49TgsFullShadowSpatialChunk,
|
||||
type M49TgsFullShadowResult,
|
||||
@@ -46,6 +50,8 @@ function message(error: unknown): string {
|
||||
|
||||
export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowResult }) {
|
||||
const [activeSequence, setActiveSequence] = useState<number | null>(null);
|
||||
const [semantic, setSemantic] = useState<E47SemanticSlamResult | null>(null);
|
||||
const [semanticError, setSemanticError] = useState<string | null>(null);
|
||||
const [chunks, setChunks] = useState<ReadonlyMap<number, M49TgsFullShadowSpatialChunk>>(
|
||||
() => new Map(),
|
||||
);
|
||||
@@ -57,6 +63,27 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
||||
const activeChunkStartRef = useRef(activeChunkStart);
|
||||
activeChunkStartRef.current = activeChunkStart;
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setSemantic(null);
|
||||
setSemanticError(null);
|
||||
void fetchE47SemanticSlamResult({
|
||||
resultId: result.source.linkedSemanticResultId,
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((next) => {
|
||||
if (controller.signal.aborted) return;
|
||||
if (!next || next.baseM4ResultId !== result.source.linkedVisualResultId) {
|
||||
throw new Error("Semantic archive не совпал с исходным M4 timeline.");
|
||||
}
|
||||
setSemantic(next);
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) setSemanticError(message(caught));
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const controller of inFlightRef.current.values()) controller.abort();
|
||||
inFlightRef.current.clear();
|
||||
@@ -142,23 +169,34 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<M4ReplayThreatVisual
|
||||
resultId={result.source.linkedVisualResultId}
|
||||
showReviewAnchorBoxes={false}
|
||||
reviewLabel="4 489 source-paced TGS frames"
|
||||
evidenceLabel="M49 · full TGS shadow"
|
||||
initialSpatialMode="3d"
|
||||
onActiveSequenceChange={handleSequenceChange}
|
||||
classifiedSpatialLayer={{
|
||||
label: "TGS full shadow · causal rolling 1 s",
|
||||
pointLayerLabel: "SOURCE POINTS",
|
||||
cellLayerLabel: "TGS COSTMAP",
|
||||
expectedAtSequence: true,
|
||||
frame: classifiedFrame,
|
||||
loading,
|
||||
error,
|
||||
replacePointCloud: false,
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<M4ReplayThreatVisual
|
||||
resultId={result.source.linkedVisualResultId}
|
||||
semantic={semantic ? {
|
||||
resultId: semantic.resultId,
|
||||
taxonomy: semantic.taxonomy,
|
||||
} : undefined}
|
||||
showReviewAnchorBoxes={false}
|
||||
reviewLabel="4 489 source-paced TGS frames"
|
||||
evidenceLabel="M49 · full TGS shadow"
|
||||
initialSpatialMode="3d"
|
||||
onActiveSequenceChange={handleSequenceChange}
|
||||
classifiedSpatialLayer={{
|
||||
label: "TGS full shadow · causal rolling 1 s",
|
||||
pointLayerLabel: "SOURCE POINTS",
|
||||
cellLayerLabel: "TGS COSTMAP",
|
||||
expectedAtSequence: true,
|
||||
frame: classifiedFrame,
|
||||
loading,
|
||||
error,
|
||||
replacePointCloud: false,
|
||||
}}
|
||||
/>
|
||||
{semanticError ? (
|
||||
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
||||
Semantic overlay недоступен: {semanticError}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -409,6 +409,17 @@ export function M4ReplayThreatVisual({
|
||||
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
|
||||
? classifiedSpatialLayer?.frame ?? null
|
||||
: null;
|
||||
const lastClassifiedSpatialFrameRef = useRef<{
|
||||
resultId: string;
|
||||
frame: M4ReplayClassifiedSpatialFrame;
|
||||
} | null>(null);
|
||||
if (classifiedSpatialLayer?.frame) {
|
||||
lastClassifiedSpatialFrameRef.current = { resultId, frame: classifiedSpatialLayer.frame };
|
||||
}
|
||||
const displayedClassifiedSpatialFrame = classifiedSpatialFrame
|
||||
?? (lastClassifiedSpatialFrameRef.current?.resultId === resultId
|
||||
? lastClassifiedSpatialFrameRef.current.frame
|
||||
: null);
|
||||
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
|
||||
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
|
||||
const mapGravityLocalSensorToBodyGround = useCallback((
|
||||
@@ -425,13 +436,13 @@ export function M4ReplayThreatVisual({
|
||||
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
|
||||
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
|
||||
const classifiedPointsBody = useMemo(
|
||||
() => classifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
||||
() => displayedClassifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
||||
mapGravityLocalSensorToBodyGround,
|
||||
) ?? [],
|
||||
[classifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
|
||||
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
|
||||
);
|
||||
const classifiedCellsBody = useMemo<readonly LaboratoryMetricCellEvidence[]>(
|
||||
() => classifiedSpatialFrame?.cellsMapGravityLocal.map((cell) => {
|
||||
() => displayedClassifiedSpatialFrame?.cellsMapGravityLocal.map((cell) => {
|
||||
const body = mapGravityLocalSensorToBodyGround([
|
||||
cell.centerXyM[0],
|
||||
cell.centerXyM[1],
|
||||
@@ -451,7 +462,7 @@ export function M4ReplayThreatVisual({
|
||||
state: cell.state,
|
||||
};
|
||||
}) ?? [],
|
||||
[classifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
|
||||
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
|
||||
);
|
||||
const classifiedCellCounts = useMemo(() => ({
|
||||
ground: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||
@@ -641,6 +652,16 @@ export function M4ReplayThreatVisual({
|
||||
>
|
||||
{classifiedSpatialLayer.pointLayerLabel}
|
||||
</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"
|
||||
@@ -650,6 +671,17 @@ export function M4ReplayThreatVisual({
|
||||
>
|
||||
{classifiedSpatialLayer.cellLayerLabel}
|
||||
</Button>
|
||||
{semantic ? (
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
variant={showSpatialSemantic ? "primary" : "secondary"}
|
||||
aria-pressed={showSpatialSemantic}
|
||||
onClick={() => setShowSpatialSemantic((visible) => !visible)}
|
||||
>
|
||||
SEMANTICS
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
@@ -952,38 +984,38 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{(!classifiedSpatialLayer ? spatialFrame : classifiedSpatialFrame) ? (
|
||||
{(!classifiedSpatialLayer ? spatialFrame : displayedClassifiedSpatialFrame) ? (
|
||||
<LaboratoryMetricEvidenceScene
|
||||
ref={metricSceneRef}
|
||||
pointCloudBodyXyzM={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedPointsBody
|
||||
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
|
||||
localSurfaceBodyXyzM={classifiedSpatialFrame ? [] : localSurface.pointsBodyXyzM}
|
||||
obstacles={classifiedSpatialFrame ? [] : sceneObstacles}
|
||||
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
|
||||
obstacles={displayedClassifiedSpatialFrame ? [] : sceneObstacles}
|
||||
rig={timeline.rig}
|
||||
corridor={timeline.corridor}
|
||||
occupiedVoxelSizeM={classifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
|
||||
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
|
||||
mode={spatialMode}
|
||||
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
||||
showCurrentIncrement={showCurrentIncrement}
|
||||
showLocalSurface={classifiedSpatialFrame ? false : showLocalSurface}
|
||||
showLocalSurface={showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
showLowStep={classifiedSpatialFrame ? false : showLowStep}
|
||||
pointSemanticClassIds={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedSpatialFrame.pointClassIds
|
||||
showLowStep={displayedClassifiedSpatialFrame ? false : showLowStep}
|
||||
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.pointClassIds
|
||||
: alignedSemanticPointIds}
|
||||
semanticClasses={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedSpatialFrame.classes
|
||||
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.classes
|
||||
: semanticClasses}
|
||||
semanticPalette={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedSpatialFrame.palette
|
||||
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? displayedClassifiedSpatialFrame.palette
|
||||
: semanticPalette}
|
||||
classifiedCells={classifiedCellsBody}
|
||||
classifiedCellSizeM={classifiedSpatialFrame?.cellSizeM}
|
||||
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
|
||||
showClassifiedCells={showRollingMap}
|
||||
/>
|
||||
) : null}
|
||||
{classifiedSpatialLayer && !classifiedSpatialFrame ? (
|
||||
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
|
||||
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
|
||||
{classifiedSpatialLayer.loading || displayingBufferedFrame
|
||||
? <span className="busy-indicator" aria-hidden="true" />
|
||||
|
||||
Reference in New Issue
Block a user