From b7a51e26e678e0ce2f47c34b84577fd717cf9c13 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Thu, 6 Aug 2026 09:41:25 +0300 Subject: [PATCH] feat(lab): add bounded local SLAM surface --- .../LaboratoryMetricEvidenceScene.tsx | 26 ++++ .../src/core/laboratory/m4LocalSurface.ts | 131 ++++++++++++++++++ .../src/core/laboratory/m4ReplayThreat.ts | 64 ++++++++- .../src/styles/m4-replay-threat.css | 9 ++ .../laboratory/M4ReplayThreatVisual.tsx | 32 ++++- .../laboratory/useM4ThreatTimeline.ts | 8 ++ .../test/m4ReplayThreat.test.mjs | 71 +++++++++- scripts/build_m4_worker_shadow_artifact.py | 2 +- src/k1link/perception/threat_timeline.py | 24 ++++ tests/test_m4_threat_replay_result.py | 10 ++ 10 files changed, 369 insertions(+), 8 deletions(-) create mode 100644 apps/control-station/src/core/laboratory/m4LocalSurface.ts diff --git a/apps/control-station/src/components/laboratory/LaboratoryMetricEvidenceScene.tsx b/apps/control-station/src/components/laboratory/LaboratoryMetricEvidenceScene.tsx index 0b575ec..aa32ed2 100644 --- a/apps/control-station/src/components/laboratory/LaboratoryMetricEvidenceScene.tsx +++ b/apps/control-station/src/components/laboratory/LaboratoryMetricEvidenceScene.tsx @@ -104,6 +104,7 @@ export const LaboratoryMetricEvidenceScene = forwardRef< LaboratoryMetricEvidenceSceneHandle, { pointCloudBodyXyzM: readonly LaboratoryMetricPoint3[]; + localSurfaceBodyXyzM: readonly LaboratoryMetricPoint3[]; obstacles: readonly LaboratoryMetricObstacleVisual[]; rig: LaboratoryMetricRigVisual; corridor: LaboratoryMetricCorridorVisual; @@ -111,10 +112,12 @@ LaboratoryMetricEvidenceSceneHandle, mode: LaboratoryMetricSceneMode; label: string; showCurrentIncrement: boolean; + showLocalSurface: boolean; showRollingMap: boolean; } >(function LaboratoryMetricEvidenceScene({ pointCloudBodyXyzM, + localSurfaceBodyXyzM, obstacles, rig, corridor, @@ -122,6 +125,7 @@ LaboratoryMetricEvidenceSceneHandle, mode, label, showCurrentIncrement, + showLocalSurface, showRollingMap, }, ref) { const hostRef = useRef(null); @@ -211,6 +215,25 @@ LaboratoryMetricEvidenceSceneHandle, if (!host || !content) return; clearGroup(content); + if (showLocalSurface) { + const localSurfaceGeometry = new THREE.BufferGeometry(); + localSurfaceGeometry.setAttribute( + "position", + new THREE.BufferAttribute(positions(localSurfaceBodyXyzM), 3), + ); + content.add(new THREE.Points( + localSurfaceGeometry, + new THREE.PointsMaterial({ + color: tokenColor(host, "--nodedc-accent-rgb", [247, 248, 244]), + size: 1.3, + sizeAttenuation: false, + transparent: true, + opacity: 0.42, + depthWrite: false, + }), + )); + } + if (showCurrentIncrement) { const contextGeometry = new THREE.BufferGeometry(); contextGeometry.setAttribute( @@ -296,8 +319,10 @@ LaboratoryMetricEvidenceSceneHandle, }, [ obstacles, occupiedVoxelSizeM, + localSurfaceBodyXyzM, pointCloudBodyXyzM, showCurrentIncrement, + showLocalSurface, showRollingMap, ]); @@ -396,6 +421,7 @@ LaboratoryMetricEvidenceSceneHandle, Вне коридора Неизвестно Current increment + Local SLAM surface Rolling-map occupied diff --git a/apps/control-station/src/core/laboratory/m4LocalSurface.ts b/apps/control-station/src/core/laboratory/m4LocalSurface.ts new file mode 100644 index 0000000..133d2ad --- /dev/null +++ b/apps/control-station/src/core/laboratory/m4LocalSurface.ts @@ -0,0 +1,131 @@ +import type { + M4Matrix3, + M4Point3, + M4ThreatTimelineFrame, +} from "./m4ReplayThreat"; + +export interface M4LocalSurfaceProfile { + windowSeconds: number; + voxelSizeM: number; + radiusM: number; + pointLimit: number; +} + +export interface M4LocalSurface { + pointsBodyXyzM: readonly M4Point3[]; + sourceFrameCount: number; + sourcePointCount: number; + voxelCount: number; +} + +function bodyPointToMap( + point: M4Point3, + origin: M4Point3, + basis: M4Matrix3, +): M4Point3 { + return [ + origin[0] + point[0] * basis[0][0] + point[1] * basis[0][1] + point[2] * basis[0][2], + origin[1] + point[0] * basis[1][0] + point[1] * basis[1][1] + point[2] * basis[1][2], + origin[2] + point[0] * basis[2][0] + point[1] * basis[2][1] + point[2] * basis[2][2], + ]; +} + +function mapPointToBody( + point: M4Point3, + origin: M4Point3, + basis: M4Matrix3, +): M4Point3 { + const delta: M4Point3 = [ + point[0] - origin[0], + point[1] - origin[1], + point[2] - origin[2], + ]; + return [ + delta[0] * basis[0][0] + delta[1] * basis[1][0] + delta[2] * basis[2][0], + delta[0] * basis[0][1] + delta[1] * basis[1][1] + delta[2] * basis[2][1], + delta[0] * basis[0][2] + delta[1] * basis[1][2] + delta[2] * basis[2][2], + ]; +} + +function emptySurface(): M4LocalSurface { + return { + pointsBodyXyzM: [], + sourceFrameCount: 0, + sourcePointCount: 0, + voxelCount: 0, + }; +} + +export function buildM4LocalSurface( + availableFrames: readonly M4ThreatTimelineFrame[], + activeFrame: M4ThreatTimelineFrame | null, + profile: M4LocalSurfaceProfile, +): M4LocalSurface { + const activeBody = activeFrame?.bodyFrame; + if ( + !activeFrame + || !activeBody + || profile.windowSeconds <= 0 + || profile.voxelSizeM <= 0 + || profile.radiusM <= 0 + || profile.pointLimit < 1 + ) { + return emptySurface(); + } + + const startTimeNs = activeFrame.sourceTimeNs - profile.windowSeconds * 1_000_000_000; + const frames = availableFrames + .filter((frame) => ( + frame.bodyFrame + && frame.sourceTimeNs >= startTimeNs + && frame.sourceTimeNs <= activeFrame.sourceTimeNs + )) + .sort((left, right) => left.sequence - right.sequence); + if (!frames.length) return emptySurface(); + + const radiusSquared = profile.radiusM * profile.radiusM; + const voxels = new Map(); + let sourcePointCount = 0; + for (const frame of frames) { + const sourceBody = frame.bodyFrame; + if (!sourceBody) continue; + sourcePointCount += frame.pointCloudBodyXyzM.length; + for (const sourcePoint of frame.pointCloudBodyXyzM) { + const mapPoint = bodyPointToMap( + sourcePoint, + sourceBody.originMapXyzM, + sourceBody.basisMapFromBody, + ); + const activePoint = mapPointToBody( + mapPoint, + activeBody.originMapXyzM, + activeBody.basisMapFromBody, + ); + if ( + activePoint[0] * activePoint[0] + + activePoint[1] * activePoint[1] + + activePoint[2] * activePoint[2] + > radiusSquared + ) { + continue; + } + const key = [ + Math.floor(mapPoint[0] / profile.voxelSizeM), + Math.floor(mapPoint[1] / profile.voxelSizeM), + Math.floor(mapPoint[2] / profile.voxelSizeM), + ].join(":"); + if (!voxels.has(key)) voxels.set(key, activePoint); + } + } + + const retained = [...voxels.values()]; + const stride = Math.max(1, Math.ceil(retained.length / profile.pointLimit)); + return { + pointsBodyXyzM: retained + .filter((_, index) => index % stride === 0) + .slice(0, profile.pointLimit), + sourceFrameCount: frames.length, + sourcePointCount, + voxelCount: voxels.size, + }; +} diff --git a/apps/control-station/src/core/laboratory/m4ReplayThreat.ts b/apps/control-station/src/core/laboratory/m4ReplayThreat.ts index ce3a4e4..cd8a290 100644 --- a/apps/control-station/src/core/laboratory/m4ReplayThreat.ts +++ b/apps/control-station/src/core/laboratory/m4ReplayThreat.ts @@ -1,6 +1,7 @@ export type M4ThreatDecision = "threat" | "not-threat" | "unknown"; export type M4ThreatMotion = "moving" | "stationary" | "unknown"; export type M4Point3 = readonly [number, number, number]; +export type M4Matrix3 = readonly [M4Point3, M4Point3, M4Point3]; export interface M4ThreatReplayResult { resultId: string; @@ -132,6 +133,10 @@ export interface M4ThreatTimelineFrame { sessionSeconds: number; sourceAvailable: boolean; spatialAvailable: boolean; + bodyFrame: { + originMapXyzM: M4Point3; + basisMapFromBody: M4Matrix3; + } | null; pointCloudBodyXyzM: readonly M4Point3[]; pointCloudSourceCount: number; pointCloudSampleCount: number; @@ -159,6 +164,14 @@ export interface M4ThreatTimeline { maximumSourcePointsPerFrame: number; pointDelivery: "exact-current-increment"; sourceRepresentationId: "registered-map-increment-v1"; + localSurfaceVisualization: { + derivation: "bounded-registered-increment-accumulation"; + windowSeconds: number; + voxelSizeM: number; + radiusM: number; + pointLimit: number; + authority: "visual-derived"; + }; occupiedVoxelSizeM: number; rig: M4ThreatVisualFrame["rig"]; corridor: M4ThreatVisualFrame["corridor"]; @@ -219,6 +232,10 @@ const vector = (value: unknown, size: number, label: string): number[] => { if (parsed.length !== size) throw new M4ThreatContractError(`${label}: неверная размерность.`); return parsed; }; +const point3 = (value: unknown, label: string): M4Point3 => { + const parsed = vector(value, 3, label); + return [parsed[0]!, parsed[1]!, parsed[2]!]; +}; const decision = (value: unknown, label: string): M4ThreatDecision => { if (value !== "threat" && value !== "not-threat" && value !== "unknown") { throw new M4ThreatContractError(`${label}: неизвестное решение.`); @@ -539,6 +556,10 @@ export async function fetchM4ThreatTimeline( } const rig = object(payload.rig, "M4.6 timeline rig"); const corridor = object(payload.corridor, "M4.6 timeline corridor"); + const localSurface = object( + payload.local_surface_visualization, + "M4.6 local surface profile", + ); return { resultId: result, recordedSourceSessionId: "20260720T065719Z_viewer_live", @@ -565,6 +586,22 @@ export async function fetchM4ThreatTimeline( "M4.6 point delivery", ), sourceRepresentationId: "registered-map-increment-v1", + localSurfaceVisualization: { + derivation: exact( + localSurface.derivation, + "bounded-registered-increment-accumulation", + "M4.6 local surface derivation", + ), + windowSeconds: number(localSurface.window_seconds, "M4.6 local surface window"), + voxelSizeM: number(localSurface.voxel_size_m, "M4.6 local surface voxel"), + radiusM: number(localSurface.radius_m, "M4.6 local surface radius"), + pointLimit: integer(localSurface.point_limit, "M4.6 local surface point limit"), + authority: exact( + localSurface.authority, + "visual-derived", + "M4.6 local surface authority", + ), + }, occupiedVoxelSizeM: number( corridor.occupied_voxel_size_m, "M4.6 occupied voxel size", @@ -647,6 +684,22 @@ function parseTimelineFrame( throw new M4ThreatContractError("M4.6 timeline frame order: нарушен контракт."); } const counts = object(item.decision_counts, "M4.6 timeline decisions"); + const spatialAvailable = typeof item.spatial_available === "boolean" + && item.spatial_available; + const bodyFrame = item.body_frame === null + ? null + : object(item.body_frame, "M4.6 timeline body frame"); + if (spatialAvailable !== (bodyFrame !== null)) { + throw new M4ThreatContractError("M4.6 timeline body frame: нарушена доступность."); + } + const basis = bodyFrame === null + ? null + : array(bodyFrame.basis_map_from_body, "M4.6 timeline body basis").map( + (row) => point3(row, "M4.6 timeline body basis row"), + ); + if (basis !== null && basis.length !== 3) { + throw new M4ThreatContractError("M4.6 timeline body basis: нарушен размер."); + } const cameraUrl = text(item.camera_url, "M4.6 timeline camera URL"); if (!cameraUrl.includes(`/results/${result}/timeline/frames/${sequence}/camera`)) { throw new M4ThreatContractError("M4.6 timeline camera URL: нарушена идентичность."); @@ -657,7 +710,16 @@ function parseTimelineFrame( sourceTimeNs: integer(item.source_time_ns, "M4.6 timeline source time"), sessionSeconds: number(item.session_seconds, "M4.6 timeline time"), sourceAvailable: typeof item.source_available === "boolean" && item.source_available, - spatialAvailable: typeof item.spatial_available === "boolean" && item.spatial_available, + spatialAvailable, + bodyFrame: bodyFrame === null || basis === null + ? null + : { + originMapXyzM: point3( + bodyFrame.origin_map_xyz_m, + "M4.6 timeline body origin", + ), + basisMapFromBody: [basis[0]!, basis[1]!, basis[2]!], + }, pointCloudBodyXyzM: array(item.point_cloud_body_xyz_m, "M4.6 timeline points").map( (point) => vector(point, 3, "M4.6 timeline point") as [number, number, number], ), diff --git a/apps/control-station/src/styles/m4-replay-threat.css b/apps/control-station/src/styles/m4-replay-threat.css index fbed7a0..10d3715 100644 --- a/apps/control-station/src/styles/m4-replay-threat.css +++ b/apps/control-station/src/styles/m4-replay-threat.css @@ -66,6 +66,10 @@ margin-left: auto; } +.m4-replay-threat-visual__layer-controls .nodedc-segmented__item { + padding-inline: 0.72rem; +} + .m4-replay-threat-evidence-viewer .laboratory-evidence-viewer__transport { bottom: 0.3rem; } @@ -155,3 +159,8 @@ border: 1px solid rgb(var(--nodedc-accent-rgb)); background: transparent; } + +.laboratory-metric-evidence-scene__legend span[data-decision="local-surface"]::before { + background: rgb(var(--nodedc-accent-rgb)); + opacity: 0.72; +} diff --git a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx index eeb3b24..fcc0fd3 100644 --- a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx +++ b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx @@ -18,6 +18,7 @@ import type { M4ThreatCameraProposal, M4ThreatTimelineFrame, } from "../../core/laboratory/m4ReplayThreat"; +import { buildM4LocalSurface } from "../../core/laboratory/m4LocalSurface"; import { recordedObservationSources } from "../../core/observation/recordedObservationSources"; import { replayObservationSession } from "../../core/observation/sessionArchive"; import type { ObservationSourceDescriptor } from "../../core/runtime/contracts"; @@ -68,6 +69,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { const [mode, setMode] = useState("video"); const [spatialMode, setSpatialMode] = useState("3d"); const [showCurrentIncrement, setShowCurrentIncrement] = useState(true); + const [showLocalSurface, setShowLocalSurface] = useState(true); const [showRollingMap, setShowRollingMap] = useState(true); const [expanded, setExpanded] = useState(false); const metricSceneRef = useRef(null); @@ -161,6 +163,16 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { .map((item) => item.assessment.closestApproachM) .filter((value): value is number => value !== null) .sort((left, right) => left - right)[0] ?? null; + const localSurface = useMemo(() => buildM4LocalSurface( + timelineFrame.availableFrames, + frame, + metadata.timeline?.localSurfaceVisualization ?? { + windowSeconds: 2, + voxelSizeM: 0.1, + radiusM: 12, + pointLimit: 20_000, + }, + ), [frame, metadata.timeline, timelineFrame.availableFrames]); const seek = (seconds: number) => playbackController.seek(seconds); const handleModeChange = (next: M4ThreatViewMode) => { @@ -206,7 +218,17 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { aria-pressed={showCurrentIncrement} onClick={() => setShowCurrentIncrement((visible) => !visible)} > - CURRENT INCREMENT + CURRENT + + ) : null} @@ -250,7 +272,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { {frame.spatialAvailable - ? `${frame.pointCloudSampleCount}/${frame.pointCloudSourceCount} exact lio_pcl increment` + ? `${frame.pointCloudSampleCount}/${frame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames` : "body frame / current increment unavailable"} @@ -328,13 +350,15 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) { ) : null} diff --git a/apps/control-station/src/workspaces/laboratory/useM4ThreatTimeline.ts b/apps/control-station/src/workspaces/laboratory/useM4ThreatTimeline.ts index 4fc79ee..54e14ff 100644 --- a/apps/control-station/src/workspaces/laboratory/useM4ThreatTimeline.ts +++ b/apps/control-station/src/workspaces/laboratory/useM4ThreatTimeline.ts @@ -146,10 +146,18 @@ export function useM4ThreatTimelineFrame({ (frame) => frame.sequence === activeSequence, ) ?? null; }, [activeChunkStart, activeSequence, chunks]); + const availableFrames = useMemo(() => { + const unique = new Map(); + for (const chunk of chunks.values()) { + for (const frame of chunk.frames) unique.set(frame.sequence, frame); + } + return [...unique.values()].sort((left, right) => left.sequence - right.sequence); + }, [chunks]); return { activeSequence, activeFrame, + availableFrames, loading: error === null && Boolean(timeline) && !activeFrame, error, }; diff --git a/apps/control-station/test/m4ReplayThreat.test.mjs b/apps/control-station/test/m4ReplayThreat.test.mjs index 003ef94..e66fc6b 100644 --- a/apps/control-station/test/m4ReplayThreat.test.mjs +++ b/apps/control-station/test/m4ReplayThreat.test.mjs @@ -13,6 +13,7 @@ let selectM4ThreatTimelineFrame; let selectM4ThreatTimelineSequence; let advanceRecordedEvidencePlayback; let m4ThreatChunkWindowStarts; +let buildM4LocalSurface; const resultId = `m4-threat-replay-${"a".repeat(64)}`; @@ -36,6 +37,9 @@ before(async () => { ({ m4ThreatChunkWindowStarts } = await server.ssrLoadModule( "/src/workspaces/laboratory/useM4ThreatTimeline.ts", )); + ({ buildM4LocalSurface } = await server.ssrLoadModule( + "/src/core/laboratory/m4LocalSurface.ts", + )); }); after(async () => { @@ -65,6 +69,10 @@ function timelineFrame(sequence, sessionSeconds, overrides = {}) { session_seconds: sessionSeconds, source_available: true, spatial_available: true, + body_frame: { + origin_map_xyz_m: [sequence, 0, 0], + basis_map_from_body: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + }, point_cloud_body_xyz_m: [[1, 0, 0.1]], point_cloud_source_count: 1, point_cloud_sample_count: 1, @@ -262,6 +270,14 @@ test("M4.6 timeline keeps only a compact index and decodes bounded spatial chunk point_sample_limit: 4096, maximum_source_points_per_frame: 3092, point_delivery: "exact-current-increment", + local_surface_visualization: { + derivation: "bounded-registered-increment-accumulation", + window_seconds: 2, + voxel_size_m: 0.1, + radius_m: 12, + point_limit: 20000, + authority: "visual-derived", + }, rig: { length_m: 1, width_m: 0.6, nominal_sensor_height_m: 1.25 }, corridor: { forward_length_m: 8, @@ -277,6 +293,8 @@ test("M4.6 timeline keeps only a compact index and decodes bounded spatial chunk assert.equal(timeline.pointDelivery, "exact-current-increment"); assert.equal(timeline.maximumSourcePointsPerFrame, 3092); assert.equal(timeline.occupiedVoxelSizeM, 0.45); + assert.equal(timeline.localSurfaceVisualization.windowSeconds, 2); + assert.equal(timeline.localSurfaceVisualization.authority, "visual-derived"); assert.equal(selectM4ThreatTimelineSequence(timeline.frameTimesNs, 35.50), 1); const chunk = await fetchM4ThreatTimelineChunk(resultId, 0, 2, { @@ -300,6 +318,54 @@ test("M4.6 timeline keeps only a compact index and decodes bounded spatial chunk assert.equal(selectM4ThreatTimelineFrame(chunk.frames, 35.50).sequence, 1); }); +test("M4.6 local SLAM surface reprojects registered increments into the active body frame", () => { + const frames = [ + timelineFrame(0, 10, { + body_frame: { + origin_map_xyz_m: [0, 0, 0], + basis_map_from_body: [[0, -1, 0], [1, 0, 0], [0, 0, 1]], + }, + point_cloud_body_xyz_m: [[1, 0, 0]], + }), + timelineFrame(1, 10.1, { + body_frame: { + origin_map_xyz_m: [0, 0, 0], + basis_map_from_body: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + }, + point_cloud_body_xyz_m: [[1, 0, 0]], + }), + ].map((raw, index) => ({ + sequence: raw.sequence, + frameId: raw.frame_id, + sourceTimeNs: raw.source_time_ns, + sessionSeconds: raw.session_seconds, + sourceAvailable: true, + spatialAvailable: true, + bodyFrame: { + originMapXyzM: raw.body_frame.origin_map_xyz_m, + basisMapFromBody: raw.body_frame.basis_map_from_body, + }, + pointCloudBodyXyzM: raw.point_cloud_body_xyz_m, + pointCloudSourceCount: 1, + pointCloudSampleCount: 1, + pointCloudLayer: "current-increment", + rollingMapComponentCount: 0, + metricObstacles: [], + cameraProposals: [], + decisionCounts: { threat: 0, "not-threat": 0, unknown: 0 }, + cameraUrl: raw.camera_url, + })); + const surface = buildM4LocalSurface(frames, frames[1], { + windowSeconds: 2, + voxelSizeM: 0.1, + radiusM: 12, + pointLimit: 20_000, + }); + assert.equal(surface.sourceFrameCount, 2); + assert.equal(surface.sourcePointCount, 2); + assert.deepEqual(surface.pointsBodyXyzM, [[0, 1, 0], [1, 0, 0]]); +}); + test("M4.6 spatial buffering keeps previous, active and two future chunks", () => { assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [24, 48, 72, 96]); assert.deepEqual(m4ThreatChunkWindowStarts(0, 24, 4489), [0, 24, 48]); @@ -345,8 +411,9 @@ test("M4.6 viewer reuses shared camera, video and metric evidence renderers", as assert.match(videoScene, / dict[str, int]: __all__ = [ "RECORDED_SPATIAL_CHUNK_SCHEMA", "RECORDED_SPATIAL_FRAME_SCHEMA", + "RECORDED_LOCAL_SURFACE_POINT_LIMIT", + "RECORDED_LOCAL_SURFACE_RADIUS_M", + "RECORDED_LOCAL_SURFACE_VOXEL_SIZE_M", + "RECORDED_LOCAL_SURFACE_WINDOW_SECONDS", "RECORDED_SPATIAL_MAX_CHUNK_FRAMES", "RECORDED_SPATIAL_POINT_LIMIT", "RECORDED_SPATIAL_TIMELINE_SCHEMA", diff --git a/tests/test_m4_threat_replay_result.py b/tests/test_m4_threat_replay_result.py index a8f81b8..204bec4 100644 --- a/tests/test_m4_threat_replay_result.py +++ b/tests/test_m4_threat_replay_result.py @@ -133,6 +133,14 @@ def test_m4_6_timeline_is_indexed_and_spatial_evidence_is_chunked() -> None: assert timeline["recorded_source"]["representation_id"] == "registered-map-increment-v1" assert timeline["point_delivery"] == "exact-current-increment" assert timeline["maximum_source_points_per_frame"] == 3092 + assert timeline["local_surface_visualization"] == { + "authority": "visual-derived", + "derivation": "bounded-registered-increment-accumulation", + "point_limit": 20000, + "radius_m": 12.0, + "voxel_size_m": 0.1, + "window_seconds": 2.0, + } assert "frames" not in timeline chunk = get_chunk(RESULT_ID, start=1880, count=12) @@ -143,6 +151,8 @@ def test_m4_6_timeline_is_indexed_and_spatial_evidence_is_chunked() -> None: first = chunk["frames"][0] assert first["schema_version"] == "missioncore.recorded-spatial-evidence-frame/v1" assert first["spatial_available"] is True + assert len(first["body_frame"]["origin_map_xyz_m"]) == 3 + assert len(first["body_frame"]["basis_map_from_body"]) == 3 assert first["point_cloud_layer"] == "current-increment" assert first["point_cloud_sample_count"] == first["point_cloud_source_count"] assert 0 < first["point_cloud_sample_count"] <= 4096