feat(lab): add bounded local SLAM surface
This commit is contained in:
@@ -104,6 +104,7 @@ export const LaboratoryMetricEvidenceScene = forwardRef<
|
|||||||
LaboratoryMetricEvidenceSceneHandle,
|
LaboratoryMetricEvidenceSceneHandle,
|
||||||
{
|
{
|
||||||
pointCloudBodyXyzM: readonly LaboratoryMetricPoint3[];
|
pointCloudBodyXyzM: readonly LaboratoryMetricPoint3[];
|
||||||
|
localSurfaceBodyXyzM: readonly LaboratoryMetricPoint3[];
|
||||||
obstacles: readonly LaboratoryMetricObstacleVisual[];
|
obstacles: readonly LaboratoryMetricObstacleVisual[];
|
||||||
rig: LaboratoryMetricRigVisual;
|
rig: LaboratoryMetricRigVisual;
|
||||||
corridor: LaboratoryMetricCorridorVisual;
|
corridor: LaboratoryMetricCorridorVisual;
|
||||||
@@ -111,10 +112,12 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
mode: LaboratoryMetricSceneMode;
|
mode: LaboratoryMetricSceneMode;
|
||||||
label: string;
|
label: string;
|
||||||
showCurrentIncrement: boolean;
|
showCurrentIncrement: boolean;
|
||||||
|
showLocalSurface: boolean;
|
||||||
showRollingMap: boolean;
|
showRollingMap: boolean;
|
||||||
}
|
}
|
||||||
>(function LaboratoryMetricEvidenceScene({
|
>(function LaboratoryMetricEvidenceScene({
|
||||||
pointCloudBodyXyzM,
|
pointCloudBodyXyzM,
|
||||||
|
localSurfaceBodyXyzM,
|
||||||
obstacles,
|
obstacles,
|
||||||
rig,
|
rig,
|
||||||
corridor,
|
corridor,
|
||||||
@@ -122,6 +125,7 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
mode,
|
mode,
|
||||||
label,
|
label,
|
||||||
showCurrentIncrement,
|
showCurrentIncrement,
|
||||||
|
showLocalSurface,
|
||||||
showRollingMap,
|
showRollingMap,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -211,6 +215,25 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
if (!host || !content) return;
|
if (!host || !content) return;
|
||||||
clearGroup(content);
|
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) {
|
if (showCurrentIncrement) {
|
||||||
const contextGeometry = new THREE.BufferGeometry();
|
const contextGeometry = new THREE.BufferGeometry();
|
||||||
contextGeometry.setAttribute(
|
contextGeometry.setAttribute(
|
||||||
@@ -296,8 +319,10 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
}, [
|
}, [
|
||||||
obstacles,
|
obstacles,
|
||||||
occupiedVoxelSizeM,
|
occupiedVoxelSizeM,
|
||||||
|
localSurfaceBodyXyzM,
|
||||||
pointCloudBodyXyzM,
|
pointCloudBodyXyzM,
|
||||||
showCurrentIncrement,
|
showCurrentIncrement,
|
||||||
|
showLocalSurface,
|
||||||
showRollingMap,
|
showRollingMap,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -396,6 +421,7 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
<span data-decision="not-threat">Вне коридора</span>
|
<span data-decision="not-threat">Вне коридора</span>
|
||||||
<span data-decision="unknown">Неизвестно</span>
|
<span data-decision="unknown">Неизвестно</span>
|
||||||
<span data-decision="context">Current increment</span>
|
<span data-decision="context">Current increment</span>
|
||||||
|
<span data-decision="local-surface">Local SLAM surface</span>
|
||||||
<span data-decision="rolling">Rolling-map occupied</span>
|
<span data-decision="rolling">Rolling-map occupied</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<string, M4Point3>();
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export type M4ThreatDecision = "threat" | "not-threat" | "unknown";
|
export type M4ThreatDecision = "threat" | "not-threat" | "unknown";
|
||||||
export type M4ThreatMotion = "moving" | "stationary" | "unknown";
|
export type M4ThreatMotion = "moving" | "stationary" | "unknown";
|
||||||
export type M4Point3 = readonly [number, number, number];
|
export type M4Point3 = readonly [number, number, number];
|
||||||
|
export type M4Matrix3 = readonly [M4Point3, M4Point3, M4Point3];
|
||||||
|
|
||||||
export interface M4ThreatReplayResult {
|
export interface M4ThreatReplayResult {
|
||||||
resultId: string;
|
resultId: string;
|
||||||
@@ -132,6 +133,10 @@ export interface M4ThreatTimelineFrame {
|
|||||||
sessionSeconds: number;
|
sessionSeconds: number;
|
||||||
sourceAvailable: boolean;
|
sourceAvailable: boolean;
|
||||||
spatialAvailable: boolean;
|
spatialAvailable: boolean;
|
||||||
|
bodyFrame: {
|
||||||
|
originMapXyzM: M4Point3;
|
||||||
|
basisMapFromBody: M4Matrix3;
|
||||||
|
} | null;
|
||||||
pointCloudBodyXyzM: readonly M4Point3[];
|
pointCloudBodyXyzM: readonly M4Point3[];
|
||||||
pointCloudSourceCount: number;
|
pointCloudSourceCount: number;
|
||||||
pointCloudSampleCount: number;
|
pointCloudSampleCount: number;
|
||||||
@@ -159,6 +164,14 @@ export interface M4ThreatTimeline {
|
|||||||
maximumSourcePointsPerFrame: number;
|
maximumSourcePointsPerFrame: number;
|
||||||
pointDelivery: "exact-current-increment";
|
pointDelivery: "exact-current-increment";
|
||||||
sourceRepresentationId: "registered-map-increment-v1";
|
sourceRepresentationId: "registered-map-increment-v1";
|
||||||
|
localSurfaceVisualization: {
|
||||||
|
derivation: "bounded-registered-increment-accumulation";
|
||||||
|
windowSeconds: number;
|
||||||
|
voxelSizeM: number;
|
||||||
|
radiusM: number;
|
||||||
|
pointLimit: number;
|
||||||
|
authority: "visual-derived";
|
||||||
|
};
|
||||||
occupiedVoxelSizeM: number;
|
occupiedVoxelSizeM: number;
|
||||||
rig: M4ThreatVisualFrame["rig"];
|
rig: M4ThreatVisualFrame["rig"];
|
||||||
corridor: M4ThreatVisualFrame["corridor"];
|
corridor: M4ThreatVisualFrame["corridor"];
|
||||||
@@ -219,6 +232,10 @@ const vector = (value: unknown, size: number, label: string): number[] => {
|
|||||||
if (parsed.length !== size) throw new M4ThreatContractError(`${label}: неверная размерность.`);
|
if (parsed.length !== size) throw new M4ThreatContractError(`${label}: неверная размерность.`);
|
||||||
return parsed;
|
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 => {
|
const decision = (value: unknown, label: string): M4ThreatDecision => {
|
||||||
if (value !== "threat" && value !== "not-threat" && value !== "unknown") {
|
if (value !== "threat" && value !== "not-threat" && value !== "unknown") {
|
||||||
throw new M4ThreatContractError(`${label}: неизвестное решение.`);
|
throw new M4ThreatContractError(`${label}: неизвестное решение.`);
|
||||||
@@ -539,6 +556,10 @@ export async function fetchM4ThreatTimeline(
|
|||||||
}
|
}
|
||||||
const rig = object(payload.rig, "M4.6 timeline rig");
|
const rig = object(payload.rig, "M4.6 timeline rig");
|
||||||
const corridor = object(payload.corridor, "M4.6 timeline corridor");
|
const corridor = object(payload.corridor, "M4.6 timeline corridor");
|
||||||
|
const localSurface = object(
|
||||||
|
payload.local_surface_visualization,
|
||||||
|
"M4.6 local surface profile",
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
resultId: result,
|
resultId: result,
|
||||||
recordedSourceSessionId: "20260720T065719Z_viewer_live",
|
recordedSourceSessionId: "20260720T065719Z_viewer_live",
|
||||||
@@ -565,6 +586,22 @@ export async function fetchM4ThreatTimeline(
|
|||||||
"M4.6 point delivery",
|
"M4.6 point delivery",
|
||||||
),
|
),
|
||||||
sourceRepresentationId: "registered-map-increment-v1",
|
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(
|
occupiedVoxelSizeM: number(
|
||||||
corridor.occupied_voxel_size_m,
|
corridor.occupied_voxel_size_m,
|
||||||
"M4.6 occupied voxel size",
|
"M4.6 occupied voxel size",
|
||||||
@@ -647,6 +684,22 @@ function parseTimelineFrame(
|
|||||||
throw new M4ThreatContractError("M4.6 timeline frame order: нарушен контракт.");
|
throw new M4ThreatContractError("M4.6 timeline frame order: нарушен контракт.");
|
||||||
}
|
}
|
||||||
const counts = object(item.decision_counts, "M4.6 timeline decisions");
|
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");
|
const cameraUrl = text(item.camera_url, "M4.6 timeline camera URL");
|
||||||
if (!cameraUrl.includes(`/results/${result}/timeline/frames/${sequence}/camera`)) {
|
if (!cameraUrl.includes(`/results/${result}/timeline/frames/${sequence}/camera`)) {
|
||||||
throw new M4ThreatContractError("M4.6 timeline camera URL: нарушена идентичность.");
|
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"),
|
sourceTimeNs: integer(item.source_time_ns, "M4.6 timeline source time"),
|
||||||
sessionSeconds: number(item.session_seconds, "M4.6 timeline time"),
|
sessionSeconds: number(item.session_seconds, "M4.6 timeline time"),
|
||||||
sourceAvailable: typeof item.source_available === "boolean" && item.source_available,
|
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(
|
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],
|
(point) => vector(point, 3, "M4.6 timeline point") as [number, number, number],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -66,6 +66,10 @@
|
|||||||
margin-left: auto;
|
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 {
|
.m4-replay-threat-evidence-viewer .laboratory-evidence-viewer__transport {
|
||||||
bottom: 0.3rem;
|
bottom: 0.3rem;
|
||||||
}
|
}
|
||||||
@@ -155,3 +159,8 @@
|
|||||||
border: 1px solid rgb(var(--nodedc-accent-rgb));
|
border: 1px solid rgb(var(--nodedc-accent-rgb));
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.laboratory-metric-evidence-scene__legend span[data-decision="local-surface"]::before {
|
||||||
|
background: rgb(var(--nodedc-accent-rgb));
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
M4ThreatCameraProposal,
|
M4ThreatCameraProposal,
|
||||||
M4ThreatTimelineFrame,
|
M4ThreatTimelineFrame,
|
||||||
} from "../../core/laboratory/m4ReplayThreat";
|
} from "../../core/laboratory/m4ReplayThreat";
|
||||||
|
import { buildM4LocalSurface } from "../../core/laboratory/m4LocalSurface";
|
||||||
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
|
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
|
||||||
import { replayObservationSession } from "../../core/observation/sessionArchive";
|
import { replayObservationSession } from "../../core/observation/sessionArchive";
|
||||||
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
||||||
@@ -68,6 +69,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
|||||||
const [mode, setMode] = useState<M4ThreatViewMode>("video");
|
const [mode, setMode] = useState<M4ThreatViewMode>("video");
|
||||||
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode>("3d");
|
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode>("3d");
|
||||||
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
|
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
|
||||||
|
const [showLocalSurface, setShowLocalSurface] = useState(true);
|
||||||
const [showRollingMap, setShowRollingMap] = useState(true);
|
const [showRollingMap, setShowRollingMap] = useState(true);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
||||||
@@ -161,6 +163,16 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
|||||||
.map((item) => item.assessment.closestApproachM)
|
.map((item) => item.assessment.closestApproachM)
|
||||||
.filter((value): value is number => value !== null)
|
.filter((value): value is number => value !== null)
|
||||||
.sort((left, right) => left - right)[0] ?? 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 seek = (seconds: number) => playbackController.seek(seconds);
|
||||||
const handleModeChange = (next: M4ThreatViewMode) => {
|
const handleModeChange = (next: M4ThreatViewMode) => {
|
||||||
@@ -206,7 +218,17 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
|||||||
aria-pressed={showCurrentIncrement}
|
aria-pressed={showCurrentIncrement}
|
||||||
onClick={() => setShowCurrentIncrement((visible) => !visible)}
|
onClick={() => setShowCurrentIncrement((visible) => !visible)}
|
||||||
>
|
>
|
||||||
CURRENT INCREMENT
|
CURRENT
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="nodedc-segmented__item"
|
||||||
|
data-active={showLocalSurface ? "true" : undefined}
|
||||||
|
aria-pressed={showLocalSurface}
|
||||||
|
title="Bounded local SLAM surface · visual-derived"
|
||||||
|
onClick={() => setShowLocalSurface((visible) => !visible)}
|
||||||
|
>
|
||||||
|
LOCAL SLAM
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -215,7 +237,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
|||||||
aria-pressed={showRollingMap}
|
aria-pressed={showRollingMap}
|
||||||
onClick={() => setShowRollingMap((visible) => !visible)}
|
onClick={() => setShowRollingMap((visible) => !visible)}
|
||||||
>
|
>
|
||||||
ROLLING MAP
|
ROLLING
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -250,7 +272,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
|||||||
</strong>
|
</strong>
|
||||||
<small>
|
<small>
|
||||||
{frame.spatialAvailable
|
{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"}
|
: "body frame / current increment unavailable"}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,13 +350,15 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
|||||||
<LaboratoryMetricEvidenceScene
|
<LaboratoryMetricEvidenceScene
|
||||||
ref={metricSceneRef}
|
ref={metricSceneRef}
|
||||||
pointCloudBodyXyzM={frame.pointCloudBodyXyzM}
|
pointCloudBodyXyzM={frame.pointCloudBodyXyzM}
|
||||||
|
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
|
||||||
obstacles={sceneObstacles}
|
obstacles={sceneObstacles}
|
||||||
rig={timeline.rig}
|
rig={timeline.rig}
|
||||||
corridor={timeline.corridor}
|
corridor={timeline.corridor}
|
||||||
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
|
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
|
||||||
mode={spatialMode}
|
mode={spatialMode}
|
||||||
label="M4.6 recorded-realtime current increment and rolling occupancy"
|
label="M4.6 exact current increment, bounded local SLAM surface and rolling occupancy"
|
||||||
showCurrentIncrement={showCurrentIncrement}
|
showCurrentIncrement={showCurrentIncrement}
|
||||||
|
showLocalSurface={showLocalSurface}
|
||||||
showRollingMap={showRollingMap}
|
showRollingMap={showRollingMap}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -146,10 +146,18 @@ export function useM4ThreatTimelineFrame({
|
|||||||
(frame) => frame.sequence === activeSequence,
|
(frame) => frame.sequence === activeSequence,
|
||||||
) ?? null;
|
) ?? null;
|
||||||
}, [activeChunkStart, activeSequence, chunks]);
|
}, [activeChunkStart, activeSequence, chunks]);
|
||||||
|
const availableFrames = useMemo(() => {
|
||||||
|
const unique = new Map<number, M4ThreatTimelineFrame>();
|
||||||
|
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 {
|
return {
|
||||||
activeSequence,
|
activeSequence,
|
||||||
activeFrame,
|
activeFrame,
|
||||||
|
availableFrames,
|
||||||
loading: error === null && Boolean(timeline) && !activeFrame,
|
loading: error === null && Boolean(timeline) && !activeFrame,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ let selectM4ThreatTimelineFrame;
|
|||||||
let selectM4ThreatTimelineSequence;
|
let selectM4ThreatTimelineSequence;
|
||||||
let advanceRecordedEvidencePlayback;
|
let advanceRecordedEvidencePlayback;
|
||||||
let m4ThreatChunkWindowStarts;
|
let m4ThreatChunkWindowStarts;
|
||||||
|
let buildM4LocalSurface;
|
||||||
|
|
||||||
const resultId = `m4-threat-replay-${"a".repeat(64)}`;
|
const resultId = `m4-threat-replay-${"a".repeat(64)}`;
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ before(async () => {
|
|||||||
({ m4ThreatChunkWindowStarts } = await server.ssrLoadModule(
|
({ m4ThreatChunkWindowStarts } = await server.ssrLoadModule(
|
||||||
"/src/workspaces/laboratory/useM4ThreatTimeline.ts",
|
"/src/workspaces/laboratory/useM4ThreatTimeline.ts",
|
||||||
));
|
));
|
||||||
|
({ buildM4LocalSurface } = await server.ssrLoadModule(
|
||||||
|
"/src/core/laboratory/m4LocalSurface.ts",
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
@@ -65,6 +69,10 @@ function timelineFrame(sequence, sessionSeconds, overrides = {}) {
|
|||||||
session_seconds: sessionSeconds,
|
session_seconds: sessionSeconds,
|
||||||
source_available: true,
|
source_available: true,
|
||||||
spatial_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_body_xyz_m: [[1, 0, 0.1]],
|
||||||
point_cloud_source_count: 1,
|
point_cloud_source_count: 1,
|
||||||
point_cloud_sample_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,
|
point_sample_limit: 4096,
|
||||||
maximum_source_points_per_frame: 3092,
|
maximum_source_points_per_frame: 3092,
|
||||||
point_delivery: "exact-current-increment",
|
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 },
|
rig: { length_m: 1, width_m: 0.6, nominal_sensor_height_m: 1.25 },
|
||||||
corridor: {
|
corridor: {
|
||||||
forward_length_m: 8,
|
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.pointDelivery, "exact-current-increment");
|
||||||
assert.equal(timeline.maximumSourcePointsPerFrame, 3092);
|
assert.equal(timeline.maximumSourcePointsPerFrame, 3092);
|
||||||
assert.equal(timeline.occupiedVoxelSizeM, 0.45);
|
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);
|
assert.equal(selectM4ThreatTimelineSequence(timeline.frameTimesNs, 35.50), 1);
|
||||||
|
|
||||||
const chunk = await fetchM4ThreatTimelineChunk(resultId, 0, 2, {
|
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);
|
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", () => {
|
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(48, 24, 4489), [24, 48, 72, 96]);
|
||||||
assert.deepEqual(m4ThreatChunkWindowStarts(0, 24, 4489), [0, 24, 48]);
|
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, /<RecordedFmp4Player/);
|
assert.match(videoScene, /<RecordedFmp4Player/);
|
||||||
assert.match(imageScene, /<RecordedEvidenceBoxOverlay/);
|
assert.match(imageScene, /<RecordedEvidenceBoxOverlay/);
|
||||||
assert.match(metricScene, /OrbitControls/);
|
assert.match(metricScene, /OrbitControls/);
|
||||||
assert.match(visual, /CURRENT INCREMENT/);
|
assert.match(visual, /LOCAL SLAM/);
|
||||||
assert.match(visual, /ROLLING MAP/);
|
assert.match(visual, /showLocalSurface/);
|
||||||
|
assert.match(metricScene, /Local SLAM surface/);
|
||||||
assert.match(visual, /showJumpToEnd=\{false\}/);
|
assert.match(visual, /showJumpToEnd=\{false\}/);
|
||||||
assert.doesNotMatch(metricScene, /ЛКМ · вращение/);
|
assert.doesNotMatch(metricScene, /ЛКМ · вращение/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ WHEEL_NAME = "nodedc_mission_core-0.1.0-py3-none-any.whl"
|
|||||||
RUNNER_NAME = RUNNER.name
|
RUNNER_NAME = RUNNER.name
|
||||||
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
|
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
|
||||||
EXPECTED_BASELINE_SHA256 = "ea10359339e6cce31b5780a2710299771cab7cc0c1c2a2b56a1621f786b31fa8"
|
EXPECTED_BASELINE_SHA256 = "ea10359339e6cce31b5780a2710299771cab7cc0c1c2a2b56a1621f786b31fa8"
|
||||||
EXPECTED_WHEEL_SHA256 = "9d203362cebb383fd5394e60dbcaa4cea6d332de9c0aa411cbc23825a4e53868"
|
EXPECTED_WHEEL_SHA256 = "2fc53bf3c2cd82a33e62b158a455d813bca64b844707e758792b4bac263b2543"
|
||||||
PAYLOAD_FILES = (
|
PAYLOAD_FILES = (
|
||||||
RUNNER_NAME,
|
RUNNER_NAME,
|
||||||
WHEEL_NAME,
|
WHEEL_NAME,
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ RECORDED_SPATIAL_CHUNK_SCHEMA: Final = "missioncore.recorded-spatial-evidence-ch
|
|||||||
RECORDED_SPATIAL_FRAME_SCHEMA: Final = "missioncore.recorded-spatial-evidence-frame/v1"
|
RECORDED_SPATIAL_FRAME_SCHEMA: Final = "missioncore.recorded-spatial-evidence-frame/v1"
|
||||||
RECORDED_SPATIAL_POINT_LIMIT: Final = 4_096
|
RECORDED_SPATIAL_POINT_LIMIT: Final = 4_096
|
||||||
RECORDED_SPATIAL_MAX_CHUNK_FRAMES: Final = 24
|
RECORDED_SPATIAL_MAX_CHUNK_FRAMES: Final = 24
|
||||||
|
RECORDED_LOCAL_SURFACE_WINDOW_SECONDS: Final = 2.0
|
||||||
|
RECORDED_LOCAL_SURFACE_VOXEL_SIZE_M: Final = 0.10
|
||||||
|
RECORDED_LOCAL_SURFACE_RADIUS_M: Final = 12.0
|
||||||
|
RECORDED_LOCAL_SURFACE_POINT_LIMIT: Final = 20_000
|
||||||
_EXPECTED_FRAME_COUNT: Final = 4_489
|
_EXPECTED_FRAME_COUNT: Final = 4_489
|
||||||
_SOURCE_TIME = re.compile(rb'"source_time_ns":([0-9]+)')
|
_SOURCE_TIME = re.compile(rb'"source_time_ns":([0-9]+)')
|
||||||
|
|
||||||
@@ -117,6 +121,14 @@ class RecordedThreatTimeline:
|
|||||||
"point_sample_limit": RECORDED_SPATIAL_POINT_LIMIT,
|
"point_sample_limit": RECORDED_SPATIAL_POINT_LIMIT,
|
||||||
"point_delivery": "exact-current-increment",
|
"point_delivery": "exact-current-increment",
|
||||||
"maximum_source_points_per_frame": self.store.maximum_current_point_count,
|
"maximum_source_points_per_frame": self.store.maximum_current_point_count,
|
||||||
|
"local_surface_visualization": {
|
||||||
|
"derivation": "bounded-registered-increment-accumulation",
|
||||||
|
"window_seconds": RECORDED_LOCAL_SURFACE_WINDOW_SECONDS,
|
||||||
|
"voxel_size_m": RECORDED_LOCAL_SURFACE_VOXEL_SIZE_M,
|
||||||
|
"radius_m": RECORDED_LOCAL_SURFACE_RADIUS_M,
|
||||||
|
"point_limit": RECORDED_LOCAL_SURFACE_POINT_LIMIT,
|
||||||
|
"authority": "visual-derived",
|
||||||
|
},
|
||||||
"image_width": 800,
|
"image_width": 800,
|
||||||
"image_height": 600,
|
"image_height": 600,
|
||||||
"rig": {
|
"rig": {
|
||||||
@@ -203,6 +215,14 @@ class RecordedThreatTimeline:
|
|||||||
"session_seconds": self.index.source_times_ns[sequence] / 1_000_000_000,
|
"session_seconds": self.index.source_times_ns[sequence] / 1_000_000_000,
|
||||||
"source_available": source_available,
|
"source_available": source_available,
|
||||||
"spatial_available": body_frame is not None,
|
"spatial_available": body_frame is not None,
|
||||||
|
"body_frame": None
|
||||||
|
if body_frame is None
|
||||||
|
else {
|
||||||
|
"origin_map_xyz_m": list(body_frame.origin_map_xyz_m),
|
||||||
|
"basis_map_from_body": [
|
||||||
|
list(row) for row in body_frame.basis_map_from_body
|
||||||
|
],
|
||||||
|
},
|
||||||
"point_cloud_body_xyz_m": point_cloud,
|
"point_cloud_body_xyz_m": point_cloud,
|
||||||
"point_cloud_source_count": point_source_count,
|
"point_cloud_source_count": point_source_count,
|
||||||
"point_cloud_sample_count": len(point_cloud),
|
"point_cloud_sample_count": len(point_cloud),
|
||||||
@@ -285,6 +305,10 @@ def _decision_counts(assessments: list[dict[str, object]]) -> dict[str, int]:
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"RECORDED_SPATIAL_CHUNK_SCHEMA",
|
"RECORDED_SPATIAL_CHUNK_SCHEMA",
|
||||||
"RECORDED_SPATIAL_FRAME_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_MAX_CHUNK_FRAMES",
|
||||||
"RECORDED_SPATIAL_POINT_LIMIT",
|
"RECORDED_SPATIAL_POINT_LIMIT",
|
||||||
"RECORDED_SPATIAL_TIMELINE_SCHEMA",
|
"RECORDED_SPATIAL_TIMELINE_SCHEMA",
|
||||||
|
|||||||
@@ -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["recorded_source"]["representation_id"] == "registered-map-increment-v1"
|
||||||
assert timeline["point_delivery"] == "exact-current-increment"
|
assert timeline["point_delivery"] == "exact-current-increment"
|
||||||
assert timeline["maximum_source_points_per_frame"] == 3092
|
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
|
assert "frames" not in timeline
|
||||||
|
|
||||||
chunk = get_chunk(RESULT_ID, start=1880, count=12)
|
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]
|
first = chunk["frames"][0]
|
||||||
assert first["schema_version"] == "missioncore.recorded-spatial-evidence-frame/v1"
|
assert first["schema_version"] == "missioncore.recorded-spatial-evidence-frame/v1"
|
||||||
assert first["spatial_available"] is True
|
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_layer"] == "current-increment"
|
||||||
assert first["point_cloud_sample_count"] == first["point_cloud_source_count"]
|
assert first["point_cloud_sample_count"] == first["point_cloud_source_count"]
|
||||||
assert 0 < first["point_cloud_sample_count"] <= 4096
|
assert 0 < first["point_cloud_sample_count"] <= 4096
|
||||||
|
|||||||
Reference in New Issue
Block a user