diff --git a/apps/control-station/src/core/laboratory/m49TgsFullShadow.ts b/apps/control-station/src/core/laboratory/m49TgsFullShadow.ts index 0853824..be1fd60 100644 --- a/apps/control-station/src/core/laboratory/m49TgsFullShadow.ts +++ b/apps/control-station/src/core/laboratory/m49TgsFullShadow.ts @@ -1,6 +1,7 @@ import type { LaboratoryFetch } from "./advancedResults"; const RESULT_ID = /^m49-tgs-full-shadow-[a-f0-9]{64}$/; +const SEMANTIC_RESULT_ID = /^e47-semantic-slam-[a-f0-9]{64}$/; export type M49TgsFullShadowStateCode = 0 | 1 | 2 | 3; @@ -10,6 +11,7 @@ export interface M49TgsFullShadowResult { source: { sourcePackSha256: string; linkedVisualResultId: string; + linkedSemanticResultId: string; }; configuration: { configSha256: string; @@ -158,12 +160,20 @@ export async function fetchM49TgsFullShadowResult( const performance = objectValue(payload.performance, "M49 full performance"); const acceptance = objectValue(payload.acceptance, "M49 full acceptance"); const decision = objectValue(payload.decision, "M49 full decision"); + const linkedSemanticResultId = text( + source.linked_semantic_result_id, + "M49 full semantic result", + ); + if (!SEMANTIC_RESULT_ID.test(linkedSemanticResultId)) { + throw new M49TgsFullShadowContractError("M49 full semantic result: нарушена идентичность."); + } return { resultId: text(payload.result_id, "M49 full result"), createdAtUtc: text(payload.created_at_utc, "M49 full created"), source: { sourcePackSha256: text(source.source_pack_sha256, "M49 full source pack"), linkedVisualResultId: text(source.linked_visual_result_id, "M49 full visual result"), + linkedSemanticResultId, }, configuration: { configSha256: text(configuration.config_sha256, "M49 full config"), diff --git a/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx b/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx index 4c97c99..4c5d34a 100644 --- a/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx +++ b/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx @@ -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(null); + const [semantic, setSemantic] = useState(null); + const [semanticError, setSemanticError] = useState(null); const [chunks, setChunks] = useState>( () => 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 ( - + <> + + {semanticError ? ( +
+ Semantic overlay недоступен: {semanticError} +
+ ) : null} + ); } diff --git a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx index a880072..6bab793 100644 --- a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx +++ b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx @@ -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( - () => 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} + + {semantic ? ( + + ) : null} ) : (
) : null} - {(!classifiedSpatialLayer ? spatialFrame : classifiedSpatialFrame) ? ( + {(!classifiedSpatialLayer ? spatialFrame : displayedClassifiedSpatialFrame) ? ( ) : null} - {classifiedSpatialLayer && !classifiedSpatialFrame ? ( + {classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
{classifiedSpatialLayer.loading || displayingBufferedFrame ?