feat(lidar): add RAVNOVES field review

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 10:26:07 +03:00
parent 2dfb34ef21
commit 3333e9ac0f
13 changed files with 2775 additions and 14 deletions
@@ -147,6 +147,106 @@ export interface LidarGroundFrame {
groundTruth: false;
}
export interface LidarFieldReviewWindowSummary {
index: number;
key: string;
label: string;
startSeconds: number;
endSeconds: number;
midpointSeconds: number;
sourceLidarSamples: number;
sourcePointCount: number;
displayPointCount: number;
sourceFrameStart: number;
sourceFrameEnd: number;
previewSourceFrameIndex: number;
previewSessionSeconds: number;
}
export interface LidarFieldReview {
reviewId: string;
displayName: string;
sessionId: string;
sourcePackId: string;
status: "diagnostic-only";
source: {
timelineStartSeconds: number;
timelineEndSeconds: number;
availableLidarFrames: number;
pointCount: number;
representation: "legacy-e10-vendor-map-with-pose";
intensityAvailable: false;
rawScanAccepted: false;
};
selection: {
purpose: "operator-readable-central-urban-field-review";
defaultWindowIndex: number;
accumulation: "per-source-frame masks accumulated in map frame";
maximumPointsPerWindow: number;
};
windows: LidarFieldReviewWindowSummary[];
metrics: {
sourceSamples: number;
current: {
provider: LidarGroundProviderSummary;
groundFraction: LidarDistribution;
latencyMs: LidarDistribution;
};
candidate: {
provider: LidarGroundProviderSummary;
groundFraction: LidarDistribution;
latencyMs: LidarDistribution;
};
comparison: {
algorithmGroundIou: LidarDistribution;
groundDisagreementFraction: LidarDistribution;
isAccuracyMetric: false;
};
};
decision: {
status: "visual-review-only";
productionPromotion: false;
reasons: string[];
};
createdAtUtc: string | null;
groundTruth: false;
authority: {
commandsEnabled: false;
navigationOrSafetyAccepted: false;
};
}
export interface LidarFieldReviewCatalog {
configured: boolean;
validTotal: number;
invalidTotal: number;
items: LidarFieldReview[];
}
export interface LidarFieldReviewWindow {
reviewId: string;
displayName: string;
sessionId: string;
sourcePackId: string;
windowIndex: number;
windowCount: number;
window: LidarFieldReviewWindowSummary;
pointCount: number;
coordinateFrame: "map";
distanceUnit: "m";
pointsXyzM: Array<[number, number, number]>;
intensity0To255: null;
intensity: {
available: false;
reason: string;
};
masks: LidarGroundFrame["masks"];
counts: LidarGroundFrame["counts"];
previewUrl: string;
groundTruth: false;
authority: LidarFieldReview["authority"];
}
export class LidarReplayContractError extends Error {}
export class LidarReplayApiError extends Error {
@@ -162,7 +262,10 @@ type LidarFetch = (
const SAFE_PACK_ID = /^lidar-replay-pack-[a-f0-9]{64}$/;
const SAFE_GROUND_BENCHMARK_ID = /^ground-benchmark-[a-f0-9]{64}$/;
const SAFE_FIELD_REVIEW_ID = /^lidar-field-review-[a-f0-9]{64}$/;
const SAFE_E10_PACK_ID = /^e10-lidar-pack-[a-f0-9]{64}$/;
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
const SAFE_FIELD_KEY = /^[a-z0-9][a-z0-9-]{0,63}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const GIT_SHA1 = /^[a-f0-9]{40}$/;
@@ -641,6 +744,358 @@ export function parseLidarGroundFrame(value: unknown): LidarGroundFrame {
};
}
function fieldReviewWindowSummary(
value: unknown,
expectedIndex: number,
): LidarFieldReviewWindowSummary {
const source = record(value, `field-review window ${expectedIndex}`);
const index = integer(source.index, "window.index");
const startSeconds = number(source.start_seconds, "window.start_seconds") ?? 0;
const endSeconds = number(source.end_seconds, "window.end_seconds") ?? 0;
if (index !== expectedIndex || startSeconds >= endSeconds) {
throw new LidarReplayContractError("LiDAR field-review window несовместим");
}
return {
index,
key: string(source.key, "window.key", SAFE_FIELD_KEY),
label: string(source.label, "window.label"),
startSeconds,
endSeconds,
midpointSeconds:
number(source.midpoint_seconds, "window.midpoint_seconds") ?? 0,
sourceLidarSamples: integer(
source.source_lidar_samples,
"window.source_lidar_samples",
),
sourcePointCount: integer(
source.source_point_count,
"window.source_point_count",
),
displayPointCount: integer(
source.display_point_count,
"window.display_point_count",
),
sourceFrameStart: integer(
source.source_frame_start,
"window.source_frame_start",
),
sourceFrameEnd: integer(
source.source_frame_end,
"window.source_frame_end",
),
previewSourceFrameIndex: integer(
source.preview_source_frame_index,
"window.preview_source_frame_index",
),
previewSessionSeconds:
number(
source.preview_session_seconds,
"window.preview_session_seconds",
) ?? 0,
};
}
function fieldReviewBranch(
value: unknown,
label: string,
): LidarFieldReview["metrics"]["current"] {
const source = record(value, label);
return {
provider: groundProvider(source.provider, `${label}.provider`),
groundFraction: distribution(
source.ground_fraction,
`${label}.ground_fraction`,
),
latencyMs: distribution(source.latency_ms, `${label}.latency_ms`),
};
}
function fieldReview(value: unknown): LidarFieldReview {
const source = record(value, "LiDAR field review");
const sourceEvidence = record(source.source, "field-review source");
const selection = record(source.selection, "field-review selection");
const sampling = record(selection.sampling, "field-review sampling");
const metrics = record(source.metrics, "field-review metrics");
const comparison = record(metrics.comparison, "field-review comparison");
const decision = record(source.decision, "field-review decision");
const authority = record(source.authority, "field-review authority");
if (
source.status !== "diagnostic-only"
|| source.ground_truth !== false
|| sourceEvidence.representation !== "legacy-e10-vendor-map-with-pose"
|| sourceEvidence.intensity_available !== false
|| sourceEvidence.raw_scan_accepted !== false
|| selection.purpose !== "operator-readable-central-urban-field-review"
|| selection.accumulation !== "per-source-frame masks accumulated in map frame"
|| sampling.method !== "uniform-point-index-per-window"
|| comparison.is_accuracy_metric !== false
|| decision.status !== "visual-review-only"
|| decision.production_promotion !== false
|| authority.commands_enabled !== false
|| authority.navigation_or_safety_accepted !== false
) {
throw new LidarReplayContractError(
"LiDAR field review завышает readiness или меняет evidence",
);
}
const windows = array(source.windows, "field-review windows").map(
fieldReviewWindowSummary,
);
const defaultWindowIndex = integer(
selection.default_window_index,
"selection.default_window_index",
);
const maximumPointsPerWindow = integer(
sampling.maximum_points_per_window,
"sampling.maximum_points_per_window",
);
if (
windows.length < 1
|| defaultWindowIndex >= windows.length
|| maximumPointsPerWindow < 1
|| maximumPointsPerWindow > 80_000
|| windows.some(
(window) =>
window.sourceLidarSamples < 1
|| window.sourcePointCount < window.displayPointCount
|| window.displayPointCount < 1
|| window.displayPointCount > maximumPointsPerWindow,
)
) {
throw new LidarReplayContractError("LiDAR field-review selection несовместим");
}
return {
reviewId: string(source.review_id, "review_id", SAFE_FIELD_REVIEW_ID),
displayName: string(source.display_name, "display_name"),
sessionId: string(source.session_id, "session_id", SAFE_ID),
sourcePackId: string(
source.source_pack_id,
"source_pack_id",
SAFE_E10_PACK_ID,
),
status: "diagnostic-only",
source: {
timelineStartSeconds:
number(
sourceEvidence.timeline_start_seconds,
"source.timeline_start_seconds",
) ?? 0,
timelineEndSeconds:
number(
sourceEvidence.timeline_end_seconds,
"source.timeline_end_seconds",
) ?? 0,
availableLidarFrames: integer(
sourceEvidence.available_lidar_frames,
"source.available_lidar_frames",
),
pointCount: integer(sourceEvidence.point_count, "source.point_count"),
representation: "legacy-e10-vendor-map-with-pose",
intensityAvailable: false,
rawScanAccepted: false,
},
selection: {
purpose: "operator-readable-central-urban-field-review",
defaultWindowIndex,
accumulation: "per-source-frame masks accumulated in map frame",
maximumPointsPerWindow,
},
windows,
metrics: {
sourceSamples: integer(metrics.source_samples, "metrics.source_samples"),
current: fieldReviewBranch(metrics.current, "metrics.current"),
candidate: fieldReviewBranch(metrics.candidate, "metrics.candidate"),
comparison: {
algorithmGroundIou: distribution(
comparison.algorithm_to_algorithm_ground_iou,
"metrics.comparison.algorithm_ground_iou",
),
groundDisagreementFraction: distribution(
comparison.ground_disagreement_fraction,
"metrics.comparison.ground_disagreement_fraction",
),
isAccuracyMetric: false,
},
},
decision: {
status: "visual-review-only",
productionPromotion: false,
reasons: array(decision.reasons, "decision.reasons").map((reason) =>
string(reason, "decision.reason")
),
},
createdAtUtc:
source.created_at_utc === null || source.created_at_utc === undefined
? null
: string(source.created_at_utc, "created_at_utc"),
groundTruth: false,
authority: {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
},
};
}
export function parseLidarFieldReviewCatalog(
value: unknown,
): LidarFieldReviewCatalog {
const source = record(value, "LiDAR field-review catalog");
if (
source.schema_version !== "missioncore.lidar-field-review-catalog/v1"
|| source.access !== "read-only"
) {
throw new LidarReplayContractError("LiDAR field-review catalog несовместим");
}
return {
configured: boolean(source.configured, "configured"),
validTotal: integer(source.valid_total, "valid_total"),
invalidTotal: integer(source.invalid_total, "invalid_total"),
items: array(source.items, "items").map(fieldReview),
};
}
export function parseLidarFieldReviewWindow(
value: unknown,
): LidarFieldReviewWindow {
const source = record(value, "LiDAR field-review window");
const reviewId = string(
source.review_id,
"review_id",
SAFE_FIELD_REVIEW_ID,
);
if (
source.schema_version !== "missioncore.lidar-field-review-window/v1"
|| source.access !== "read-only"
|| source.ground_truth !== false
|| source.coordinate_frame !== "map"
|| source.distance_unit !== "m"
) {
throw new LidarReplayContractError("LiDAR field-review window несовместим");
}
const authority = record(source.authority, "authority");
const intensity = record(source.intensity, "intensity");
if (
authority.commands_enabled !== false
|| authority.navigation_or_safety_accepted !== false
|| intensity.available !== false
) {
throw new LidarReplayContractError("LiDAR field-review authority несовместим");
}
const pointCount = integer(source.point_count, "point_count");
if (pointCount < 1 || pointCount > 80_000) {
throw new LidarReplayContractError("LiDAR field-review window слишком большой");
}
const points = array(source.points_xyz_m, "points_xyz_m");
if (points.length !== pointCount) {
throw new LidarReplayContractError("Количество field-review points не совпадает");
}
const pointsXyzM = points.map((value, index): [number, number, number] => {
const tuple = array(value, `points_xyz_m[${index}]`);
if (tuple.length !== 3) {
throw new LidarReplayContractError("LiDAR point должен содержать XYZ");
}
return [
number(tuple[0], `points_xyz_m[${index}].x`) ?? 0,
number(tuple[1], `points_xyz_m[${index}].y`) ?? 0,
number(tuple[2], `points_xyz_m[${index}].z`) ?? 0,
];
});
const masks = record(source.masks, "masks");
const currentGround = groundMask(
masks.current_ground,
"masks.current_ground",
pointCount,
);
const currentAssigned = groundMask(
masks.current_assigned,
"masks.current_assigned",
pointCount,
);
const candidateGround = groundMask(
masks.candidate_ground,
"masks.candidate_ground",
pointCount,
);
const candidateAssigned = groundMask(
masks.candidate_assigned,
"masks.candidate_assigned",
pointCount,
);
const disagreement = groundMask(
masks.disagreement,
"masks.disagreement",
pointCount,
);
const counts = record(source.counts, "counts");
const parsedCounts = {
currentGround: integer(counts.current_ground, "counts.current_ground"),
candidateGround: integer(
counts.candidate_ground,
"counts.candidate_ground",
),
disagreement: integer(counts.disagreement, "counts.disagreement"),
};
const windowIndex = integer(source.window_index, "window_index");
const windowCount = integer(source.window_count, "window_count");
const window = fieldReviewWindowSummary(source.window, windowIndex);
const expectedPreviewUrl =
`/api/v1/lidar/field-reviews/${reviewId}/windows/${windowIndex}/preview`;
if (
windowCount < 1
|| windowIndex >= windowCount
|| window.displayPointCount !== pointCount
|| parsedCounts.currentGround
!== currentGround.reduce((sum, item) => sum + item, 0)
|| parsedCounts.candidateGround
!== candidateGround.reduce((sum, item) => sum + item, 0)
|| parsedCounts.disagreement
!== disagreement.reduce((sum, item) => sum + item, 0)
|| disagreement.some(
(item, index) =>
item !== Number(currentGround[index] !== candidateGround[index]),
)
|| source.preview_url !== expectedPreviewUrl
) {
throw new LidarReplayContractError("LiDAR field-review content несовместим");
}
return {
reviewId,
displayName: string(source.display_name, "display_name"),
sessionId: string(source.session_id, "session_id", SAFE_ID),
sourcePackId: string(
source.source_pack_id,
"source_pack_id",
SAFE_E10_PACK_ID,
),
windowIndex,
windowCount,
window,
pointCount,
coordinateFrame: "map",
distanceUnit: "m",
pointsXyzM,
intensity0To255: null,
intensity: {
available: false,
reason: string(intensity.reason, "intensity.reason"),
},
masks: {
currentGround,
currentAssigned,
candidateGround,
candidateAssigned,
disagreement,
},
counts: parsedCounts,
previewUrl: expectedPreviewUrl,
groundTruth: false,
authority: {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
},
};
}
async function responseJson(
response: Response,
fallback: string,
@@ -739,3 +1194,48 @@ export async function fetchLidarGroundFrame(
await responseJson(response, "Не удалось получить LiDAR ground frame."),
);
}
export async function fetchLidarFieldReviews(
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
): Promise<LidarFieldReviewCatalog> {
const fetcher = options.fetcher ?? fetch;
const response = await fetcher("/api/v1/lidar/field-reviews?limit=10", {
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
});
return parseLidarFieldReviewCatalog(
await responseJson(response, "Не удалось получить полевой LiDAR review."),
);
}
export async function fetchLidarFieldReviewWindow(
reviewId: string,
windowIndex: number,
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
): Promise<LidarFieldReviewWindow> {
if (
!SAFE_FIELD_REVIEW_ID.test(reviewId)
|| !Number.isInteger(windowIndex)
|| windowIndex < 0
) {
throw new LidarReplayContractError(
"Некорректное окно полевого LiDAR review",
);
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/lidar/field-reviews/${reviewId}/windows/${windowIndex}`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseLidarFieldReviewWindow(
await responseJson(
response,
"Не удалось получить окно полевого LiDAR review.",
),
);
}
@@ -18,6 +18,23 @@
grid-template-columns: 1fr;
}
.lidar-field-review__source {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lidar-field-window-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lidar-field-stage {
grid-template-columns: 1fr;
}
.lidar-field-camera img {
min-height: 0;
aspect-ratio: 4 / 3;
}
.polygon-run-providers {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -202,6 +219,21 @@
display: none;
}
.lidar-field-review__source,
.lidar-field-window-list {
grid-template-columns: 1fr;
}
.lidar-field-cloud > header,
.lidar-field-review__explanation {
align-items: stretch;
grid-template-columns: 1fr;
}
.lidar-field-cloud > header {
flex-direction: column;
}
.polygon-run-identity dl {
grid-template-columns: 1fr;
}
@@ -954,6 +954,218 @@
margin-top: 0.8rem;
}
.lidar-field-review {
min-width: 0;
border-color: rgb(74 215 255 / 0.22);
background:
radial-gradient(circle at 8% 0%, rgb(56 124 255 / 0.13), transparent 30rem),
var(--station-panel);
}
.lidar-field-review__heading p,
.lidar-field-review__explanation p,
.lidar-field-review__empty p {
margin: 0.28rem 0 0;
max-width: 49rem;
color: var(--nodedc-text-muted);
font-size: 0.64rem;
line-height: 1.5;
}
.lidar-field-review__source {
display: grid;
grid-template-columns: minmax(13rem, 1.4fr) repeat(3, minmax(0, 0.72fr));
gap: 0.55rem;
margin-top: 1rem;
}
.lidar-field-review__source > div {
display: grid;
min-width: 0;
gap: 0.28rem;
border: 1px solid var(--station-hairline);
border-radius: 0.72rem;
background: rgb(255 255 255 / 0.025);
padding: 0.64rem 0.7rem;
}
.lidar-field-review__source span,
.lidar-field-window-list small,
.lidar-field-stage__label span,
.lidar-field-camera footer,
.lidar-field-stage__pending {
color: var(--nodedc-text-muted);
font-size: 0.59rem;
line-height: 1.4;
}
.lidar-field-review__source strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.68rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.lidar-field-window-list {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0.48rem;
margin-top: 0.65rem;
}
.lidar-field-window-list > button {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 0.5rem;
border: 1px solid var(--station-hairline);
border-radius: 0.78rem;
background: rgb(255 255 255 / 0.02);
padding: 0.62rem;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.lidar-field-window-list > button:hover,
.lidar-field-window-list > button[data-selected="true"] {
border-color: rgb(74 215 255 / 0.48);
background: rgb(74 215 255 / 0.08);
}
.lidar-field-window-list > button > span {
display: grid;
flex: 0 0 auto;
width: 1.35rem;
height: 1.35rem;
place-items: center;
border-radius: 50%;
background: rgb(74 215 255 / 0.12);
color: #78e3ff;
font-size: 0.59rem;
font-weight: 700;
}
.lidar-field-window-list > button > div {
display: grid;
min-width: 0;
gap: 0.18rem;
}
.lidar-field-window-list strong {
color: var(--nodedc-text-primary);
font-size: 0.61rem;
line-height: 1.35;
}
.lidar-field-stage {
display: grid;
grid-template-columns: minmax(18rem, 0.68fr) minmax(0, 1.32fr);
gap: 0.65rem;
margin-top: 0.7rem;
}
.lidar-field-camera,
.lidar-field-cloud {
overflow: hidden;
min-width: 0;
border: 1px solid rgb(255 255 255 / 0.09);
border-radius: 0.9rem;
background: #071018;
}
.lidar-field-camera {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
}
.lidar-field-camera > .lidar-field-stage__label,
.lidar-field-cloud > header {
min-height: 3.45rem;
border-bottom: 1px solid rgb(255 255 255 / 0.08);
padding: 0.66rem 0.72rem;
}
.lidar-field-stage__label {
display: grid;
gap: 0.18rem;
}
.lidar-field-stage__label strong {
color: var(--nodedc-text-primary);
font-size: 0.68rem;
}
.lidar-field-camera img {
display: block;
width: 100%;
height: 100%;
min-height: 25rem;
object-fit: contain;
}
.lidar-field-camera footer {
display: flex;
justify-content: space-between;
gap: 0.5rem;
border-top: 1px solid rgb(255 255 255 / 0.08);
padding: 0.52rem 0.7rem;
}
.lidar-field-stage__pending {
display: grid;
min-height: 25rem;
place-items: center;
padding: 1rem;
text-align: center;
}
.lidar-field-cloud > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.65rem;
}
.lidar-field-cloud .lidar-ground-scene {
min-height: 28rem;
border: 0;
border-radius: 0;
}
.lidar-field-cloud .lidar-ground-scene-placeholder {
min-height: 28rem;
border: 0;
border-radius: 0;
}
.lidar-field-review__explanation {
display: grid;
grid-template-columns: auto minmax(15rem, 1fr) auto;
align-items: center;
gap: 0.65rem;
margin-top: 0.68rem;
border-top: 1px solid var(--station-hairline);
padding-top: 0.68rem;
}
.lidar-field-review__explanation p {
margin: 0;
}
.lidar-field-review__empty {
display: flex;
align-items: center;
gap: 0.65rem;
margin-top: 0.8rem;
}
.lidar-field-review__empty p {
margin: 0;
}
.lidar-ground-benchmark {
min-width: 0;
}
@@ -2,16 +2,27 @@ import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import type { LidarGroundFrame } from "../core/lidar/replayQuality";
export type LidarGroundViewMode =
| "intensity"
| "current"
| "candidate"
| "disagreement";
export interface LidarGroundPointCloudFrame {
pointCount: number;
pointsXyzM: Array<[number, number, number]>;
intensity0To255: number[] | null;
masks: {
currentGround: number[];
currentAssigned: number[];
candidateGround: number[];
candidateAssigned: number[];
disagreement: number[];
};
}
interface LidarGroundPointCloudProps {
frame: LidarGroundFrame;
frame: LidarGroundPointCloudFrame;
mode: LidarGroundViewMode;
}
@@ -28,7 +39,7 @@ function setRgb(
}
function frameColors(
frame: LidarGroundFrame,
frame: LidarGroundPointCloudFrame,
mode: LidarGroundViewMode,
): Float32Array {
const colors = new Float32Array(frame.pointCount * 3);
@@ -38,7 +49,7 @@ function frameColors(
const candidate = frame.masks.candidateGround[index] === 1;
const candidateAssigned = frame.masks.candidateAssigned[index] === 1;
if (mode === "intensity") {
const intensity = frame.intensity0To255[index] / 255;
const intensity = (frame.intensity0To255?.[index] ?? 96) / 255;
setRgb(
colors,
offset,
@@ -88,6 +99,8 @@ export function LidarGroundPointCloud({
const materialRef = useRef<THREE.PointsMaterial | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null);
const fogRef = useRef<THREE.FogExp2 | null>(null);
const gridRef = useRef<THREE.GridHelper | null>(null);
const [renderError, setRenderError] = useState<string | null>(null);
useEffect(() => {
@@ -115,7 +128,9 @@ export function LidarGroundPointCloud({
host.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x071018, 0.035);
const fog = new THREE.FogExp2(0x071018, 0.035);
scene.fog = fog;
fogRef.current = fog;
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 1_000);
camera.position.set(6, 4.5, 6);
cameraRef.current = camera;
@@ -147,6 +162,7 @@ export function LidarGroundPointCloud({
scene.add(new THREE.Points(geometry, material));
const grid = new THREE.GridHelper(24, 48, 0x3c7cff, 0x233747);
gridRef.current = grid;
const gridMaterials = Array.isArray(grid.material)
? grid.material
: [grid.material];
@@ -198,6 +214,8 @@ export function LidarGroundPointCloud({
materialRef.current = null;
cameraRef.current = null;
controlsRef.current = null;
fogRef.current = null;
gridRef.current = null;
};
}, []);
@@ -206,7 +224,9 @@ export function LidarGroundPointCloud({
const material = materialRef.current;
const camera = cameraRef.current;
const controls = controlsRef.current;
if (!geometry || !material || !camera || !controls) return;
const fog = fogRef.current;
const grid = gridRef.current;
if (!geometry || !material || !camera || !controls || !fog || !grid) return;
const positions = new Float32Array(frame.pointCount * 3);
let minimumX = Number.POSITIVE_INFINITY;
@@ -235,6 +255,8 @@ export function LidarGroundPointCloud({
geometry.computeBoundingSphere();
const radius = Math.max(geometry.boundingSphere?.radius ?? 1, 0.2);
material.size = THREE.MathUtils.clamp(radius / 155, 0.014, 0.075);
fog.density = THREE.MathUtils.clamp(0.18 / radius, 0.0008, 0.035);
grid.scale.setScalar(Math.max(radius / 12, 1));
const targetHeight = Math.max((maximumZ - minimumZ) * 0.35, 0.15);
const distance = Math.max(radius * 1.8, 1.2);
@@ -6,10 +6,14 @@ import {
} from "@nodedc/ui-react";
import {
fetchLidarFieldReviews,
fetchLidarFieldReviewWindow,
fetchLidarGroundFrame,
fetchLidarGroundBenchmarks,
fetchLidarReplayCatalog,
fetchLidarReplayDetail,
type LidarFieldReview,
type LidarFieldReviewWindow,
type LidarGroundBenchmark,
type LidarGroundFrame,
type LidarReplayCatalog,
@@ -69,6 +73,14 @@ export function LidarQualityWorkspace({
const [groundFrameError, setGroundFrameError] = useState<string | null>(null);
const [groundViewMode, setGroundViewMode] =
useState<LidarGroundViewMode>("disagreement");
const [fieldReview, setFieldReview] = useState<LidarFieldReview | null>(null);
const [fieldWindow, setFieldWindow] =
useState<LidarFieldReviewWindow | null>(null);
const [fieldWindowIndex, setFieldWindowIndex] = useState(0);
const [fieldLoading, setFieldLoading] = useState(true);
const [fieldError, setFieldError] = useState<string | null>(null);
const [fieldViewMode, setFieldViewMode] =
useState<LidarGroundViewMode>("disagreement");
const [selectedPackId, setSelectedPackId] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -146,7 +158,56 @@ export function LidarQualityWorkspace({
return () => controller.abort();
}, [groundBenchmark, groundFrameIndex]);
useEffect(() => {
const controller = new AbortController();
setFieldLoading(true);
setFieldError(null);
void fetchLidarFieldReviews({ signal: controller.signal })
.then((nextCatalog) => {
if (controller.signal.aborted) return;
const nextReview = nextCatalog.items[0] ?? null;
setFieldReview(nextReview);
setFieldWindow(null);
setFieldWindowIndex(nextReview?.selection.defaultWindowIndex ?? 0);
})
.catch((loadError) => {
if (controller.signal.aborted) return;
setFieldReview(null);
setFieldWindow(null);
setFieldError(errorMessage(loadError));
})
.finally(() => {
if (!controller.signal.aborted) setFieldLoading(false);
});
return () => controller.abort();
}, [reloadGeneration]);
useEffect(() => {
if (!fieldReview) return;
const controller = new AbortController();
setFieldLoading(true);
setFieldError(null);
void fetchLidarFieldReviewWindow(
fieldReview.reviewId,
fieldWindowIndex,
{ signal: controller.signal },
)
.then((window) => {
if (!controller.signal.aborted) setFieldWindow(window);
})
.catch((loadError) => {
if (controller.signal.aborted) return;
setFieldWindow(null);
setFieldError(errorMessage(loadError));
})
.finally(() => {
if (!controller.signal.aborted) setFieldLoading(false);
});
return () => controller.abort();
}, [fieldReview, fieldWindowIndex]);
const groundNormalization = groundBenchmark?.inputDomain.normalization ?? null;
const selectedFieldWindow = fieldReview?.windows[fieldWindowIndex] ?? null;
return (
<div className="standard-workspace lidar-quality-workspace">
@@ -191,12 +252,213 @@ export function LidarQualityWorkspace({
</GlassSurface>
) : (
<>
<GlassSurface className="lidar-field-review" padding="lg">
<header className="panel-heading lidar-field-review__heading">
<div>
<span className="section-eyebrow">ПОЛЕВОЙ REVIEW · RAVNOVES00</span>
<h2>
{fieldReview?.displayName
?? "Центральный городской интервал"}
</h2>
<p>
Дорога, дома, автомобили и растительность. Облако накоплено
по исходным LiDAR-сэмплам выбранного окна, а кадр камеры
фиксирует контекст сцены.
</p>
</div>
<StatusBadge tone={fieldError ? "danger" : "warning"}>
{fieldError ? "Review недоступен" : "Visual review only"}
</StatusBadge>
</header>
{fieldReview ? (
<>
<div
className="lidar-field-review__source"
aria-label="Источник полевого LiDAR review"
>
<div>
<span>Запись</span>
<strong>{fieldReview.sessionId}</strong>
</div>
<div>
<span>Интервал записи</span>
<strong>
{formatNumber(fieldReview.source.timelineStartSeconds, 2)}
{""}
{formatNumber(fieldReview.source.timelineEndSeconds, 2)} с
</strong>
</div>
<div>
<span>LiDAR-сэмплов</span>
<strong>
{fieldReview.source.availableLidarFrames.toLocaleString("ru-RU")}
</strong>
</div>
<div>
<span>Исходных точек</span>
<strong>
{fieldReview.source.pointCount.toLocaleString("ru-RU")}
</strong>
</div>
</div>
<div
className="lidar-field-window-list"
role="group"
aria-label="Полевые сцены RAVNOVES00"
>
{fieldReview.windows.map((window) => (
<button
type="button"
key={window.key}
data-selected={
window.index === fieldWindowIndex ? "true" : undefined
}
onClick={() => {
setFieldWindow(null);
setFieldWindowIndex(window.index);
}}
>
<span>{window.index + 1}</span>
<div>
<strong>{window.label}</strong>
<small>
{formatNumber(window.startSeconds, 0)}
{""}
{formatNumber(window.endSeconds, 0)} с ·{" "}
{window.sourceLidarSamples} сканов
</small>
</div>
</button>
))}
</div>
<section className="lidar-field-stage">
<div className="lidar-field-camera">
<div className="lidar-field-stage__label">
<span>КОНТЕКСТ КАМЕРЫ</span>
<strong>
{selectedFieldWindow?.label ?? "Выбранная сцена"}
</strong>
</div>
{fieldWindow ? (
<img
src={fieldWindow.previewUrl}
alt={`Кадр камеры: ${fieldWindow.window.label}`}
/>
) : (
<div className="lidar-field-stage__pending">
{fieldError ?? "Загружаем кадр выбранной сцены…"}
</div>
)}
<footer>
<span>
Кадр {selectedFieldWindow?.previewSourceFrameIndex ?? "—"}
</span>
<span>
t = {formatNumber(
selectedFieldWindow?.previewSessionSeconds ?? null,
2,
)}{" "}
с
</span>
</footer>
</div>
<div className="lidar-field-cloud">
<header>
<div className="lidar-field-stage__label">
<span>НАКОПЛЕННОЕ MAP-ОБЛАКО</span>
<strong>
{selectedFieldWindow
? `${selectedFieldWindow.sourcePointCount.toLocaleString(
"ru-RU",
)} исходных · ${selectedFieldWindow.displayPointCount.toLocaleString(
"ru-RU",
)} показано`
: "Ожидание данных"}
</strong>
</div>
<div
className="lidar-ground-modes"
role="group"
aria-label="Режим окраски полевого LiDAR"
>
{([
["current", "Current"],
["candidate", "Patchwork++"],
["disagreement", "Расхождения"],
] as const).map(([mode, label]) => (
<button
type="button"
key={mode}
data-active={
fieldViewMode === mode ? "true" : undefined
}
onClick={() => setFieldViewMode(mode)}
>
{label}
</button>
))}
</div>
</header>
{fieldWindow ? (
<LidarGroundPointCloud
frame={fieldWindow}
mode={fieldViewMode}
/>
) : (
<div className="lidar-ground-scene-placeholder">
<StatusBadge tone={fieldError ? "danger" : "accent"}>
{fieldError ? "Ошибка" : "Загрузка"}
</StatusBadge>
<span>
{fieldError ?? "Готовим накопленное облако…"}
</span>
</div>
)}
</div>
</section>
<div className="lidar-field-review__explanation">
<StatusBadge tone="warning">Не accuracy</StatusBadge>
<p>
Маски вычислены отдельно на каждом исходном скане и только
затем сведены в map frame. Это legacy E10 vendor-map
derivative без intensity и независимой ground-разметки,
поэтому результат предназначен для визуального разбора, а
не для production-gate.
</p>
<div className="lidar-ground-legend">
<span><i data-color="shared" />Ground у обоих</span>
<span><i data-color="current" />Только current</span>
<span><i data-color="candidate" />Только Patchwork++</span>
<span><i data-color="non-ground" />Оба non-ground</span>
</div>
</div>
</>
) : (
<div className="lidar-field-review__empty">
<StatusBadge tone={fieldError ? "danger" : "accent"}>
{fieldError ? "Ошибка загрузки" : "Проверка evidence"}
</StatusBadge>
<p>
{fieldError
?? (fieldLoading
? "Читаем RAVNOVES00 field-review артефакт…"
: "Field-review артефакт ещё не опубликован.")}
</p>
</div>
)}
</GlassSurface>
<section className="metrics-grid" aria-label="Качество LiDAR replay">
<MetricCard
featured
eyebrow="ТОЧЕЧНЫХ КАДРОВ"
eyebrow="ТЕХНИЧЕСКИХ КАДРОВ"
value={detail.pack.pointFrames.toLocaleString("ru-RU")}
detail={`${detail.pack.points.toLocaleString("ru-RU")} точек сохранено`}
detail={`${detail.pack.points.toLocaleString("ru-RU")} точек · indoor contract slice`}
/>
<MetricCard
eyebrow="СРЕДНЕЕ ТОЧЕК"
@@ -358,12 +620,13 @@ export function LidarQualityWorkspace({
<header>
<div>
<span className="section-eyebrow">
POINT-ALIGNED REVIEW
ТЕХНИЧЕСКИЙ CONTRACT SLICE · VIEWER_LIVE
</span>
<h3>Покадровое облако и маски</h3>
<h3>Indoor-проверка point-aligned контракта</h3>
<p>
Один и тот же map-frame XYZ, разные диагностические
раскраски. Маски не изменяют replay.
Этот короткий indoor-срез подтверждает выравнивание XYZ
и масок, но не является полевой оценкой качества. Для
улицы используйте RAVNOVES00 выше.
</p>
</div>
<div className="lidar-ground-frame-status">
@@ -8,10 +8,14 @@ let parseLidarReplayCatalog;
let parseLidarReplayDetail;
let parseLidarGroundBenchmarkCatalog;
let parseLidarGroundFrame;
let parseLidarFieldReviewCatalog;
let parseLidarFieldReviewWindow;
let fetchLidarReplayCatalog;
let fetchLidarReplayDetail;
let fetchLidarGroundBenchmarks;
let fetchLidarGroundFrame;
let fetchLidarFieldReviews;
let fetchLidarFieldReviewWindow;
let LidarReplayContractError;
let workspaceById;
@@ -26,10 +30,14 @@ before(async () => {
parseLidarReplayDetail,
parseLidarGroundBenchmarkCatalog,
parseLidarGroundFrame,
parseLidarFieldReviewCatalog,
parseLidarFieldReviewWindow,
fetchLidarReplayCatalog,
fetchLidarReplayDetail,
fetchLidarGroundBenchmarks,
fetchLidarGroundFrame,
fetchLidarFieldReviews,
fetchLidarFieldReviewWindow,
LidarReplayContractError,
} = await server.ssrLoadModule("/src/core/lidar/replayQuality.ts"));
({ workspaceById } = await server.ssrLoadModule("/src/productModel.ts"));
@@ -40,6 +48,8 @@ after(async () => {
});
const packId = `lidar-replay-pack-${"a".repeat(64)}`;
const fieldReviewId = `lidar-field-review-${"d".repeat(64)}`;
const e10PackId = `e10-lidar-pack-${"e".repeat(64)}`;
function summary(overrides = {}) {
return {
@@ -267,6 +277,145 @@ function groundFrame(overrides = {}) {
};
}
function fieldReviewWindowSummary(overrides = {}) {
return {
index: 0,
key: "intersection-facades",
label: "Перекрёсток, дорога и фасады",
start_seconds: 145,
end_seconds: 151,
midpoint_seconds: 148,
source_lidar_samples: 56,
source_point_count: 98538,
display_point_count: 3,
source_frame_start: 1097,
source_frame_end: 1156,
preview_source_frame_index: 1120,
preview_session_seconds: 147.451857292,
...overrides,
};
}
function fieldReviewCatalog(overrides = {}) {
const provider = (providerId, sourceCommit = undefined) => ({
provider_id: providerId,
source_commit: sourceCommit,
});
const branch = (providerValue) => ({
provider: providerValue,
ground_fraction: distribution(),
latency_ms: distribution(),
});
return {
schema_version: "missioncore.lidar-field-review-catalog/v1",
configured: true,
valid_total: 1,
invalid_total: 0,
access: "read-only",
items: [{
review_id: fieldReviewId,
display_name: "RAVNOVES00 · центральный городской интервал",
session_id: "20260720T065719Z_viewer_live",
source_pack_id: e10PackId,
status: "diagnostic-only",
source: {
timeline_start_seconds: 135.365857292,
timeline_end_seconds: 195.334857292,
available_lidar_frames: 526,
point_count: 1182292,
representation: "legacy-e10-vendor-map-with-pose",
intensity_available: false,
raw_scan_accepted: false,
},
selection: {
purpose: "operator-readable-central-urban-field-review",
default_window_index: 0,
accumulation: "per-source-frame masks accumulated in map frame",
sampling: {
method: "uniform-point-index-per-window",
maximum_points_per_window: 80000,
},
},
windows: [fieldReviewWindowSummary()],
metrics: {
source_samples: 56,
current: branch(
provider("missioncore-local-percentile-ground/v1"),
),
candidate: branch(
provider(
"patchworkpp/v1.4.1",
"3e6903a1d5537a4cc2ace897b0bbb98a92d6014c",
),
),
comparison: {
algorithm_to_algorithm_ground_iou: distribution(),
ground_disagreement_fraction: distribution(),
is_accuracy_metric: false,
},
},
decision: {
status: "visual-review-only",
production_promotion: false,
reasons: ["legacy derivative does not retain intensity"],
},
created_at_utc: "2026-07-25T02:00:00Z",
ground_truth: false,
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
},
}],
...overrides,
};
}
function fieldReviewWindow(overrides = {}) {
return {
schema_version: "missioncore.lidar-field-review-window/v1",
review_id: fieldReviewId,
display_name: "RAVNOVES00 · центральный городской интервал",
session_id: "20260720T065719Z_viewer_live",
source_pack_id: e10PackId,
window_index: 0,
window_count: 1,
window: fieldReviewWindowSummary(),
point_count: 3,
coordinate_frame: "map",
distance_unit: "m",
intensity: {
available: false,
reason: "E10 derivative did not retain rgbi/intensity",
},
points_xyz_m: [
[0, 0, 0],
[1, 0, 0.1],
[0, 1, 0.5],
],
masks: {
current_ground: [1, 1, 0],
current_assigned: [1, 1, 1],
candidate_ground: [1, 0, 0],
candidate_assigned: [1, 1, 1],
disagreement: [0, 1, 0],
},
counts: {
current_ground: 2,
candidate_ground: 1,
disagreement: 1,
},
preview_url:
`/api/v1/lidar/field-reviews/${fieldReviewId}/windows/0/preview`,
access: "read-only",
ground_truth: false,
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
},
...overrides,
};
}
function jsonResponse(payload, status = 200) {
return new Response(JSON.stringify(payload), {
status,
@@ -359,6 +508,49 @@ test("ground frame stays point-aligned, bounded and path-free", () => {
);
});
test("field review identifies RAVNOVES00 source and stays visual-only", () => {
const parsedCatalog = parseLidarFieldReviewCatalog(fieldReviewCatalog());
const parsedWindow = parseLidarFieldReviewWindow(fieldReviewWindow());
assert.equal(parsedCatalog.items[0].sourcePackId, e10PackId);
assert.equal(parsedCatalog.items[0].source.availableLidarFrames, 526);
assert.equal(parsedCatalog.items[0].selection.defaultWindowIndex, 0);
assert.equal(parsedWindow.window.label, "Перекрёсток, дорога и фасады");
assert.equal(parsedWindow.intensity0To255, null);
assert.equal(parsedWindow.counts.disagreement, 1);
assert.equal("path" in parsedWindow, false);
const promoted = fieldReviewCatalog();
promoted.items[0].decision.production_promotion = true;
assert.throws(
() => parseLidarFieldReviewCatalog(promoted),
LidarReplayContractError,
);
});
test("field-review window refuses forged preview and point masks", () => {
assert.throws(
() => parseLidarFieldReviewWindow(fieldReviewWindow({
preview_url: "https://example.invalid/frame.jpg",
})),
LidarReplayContractError,
);
assert.throws(
() => parseLidarFieldReviewWindow(fieldReviewWindow({
masks: {
...fieldReviewWindow().masks,
disagreement: [0, 0, 0],
},
counts: {
current_ground: 2,
candidate_ground: 1,
disagreement: 0,
},
})),
LidarReplayContractError,
);
});
test("LiDAR fetchers use read-only endpoints and workspace is registered", async () => {
const calls = [];
const fetcher = async (input, init) => {
@@ -368,6 +560,11 @@ test("LiDAR fetchers use read-only endpoints and workspace is registered", async
? jsonResponse(groundFrame())
: jsonResponse(groundCatalog());
}
if (String(input).includes("field-reviews")) {
return String(input).includes("/windows/")
? jsonResponse(fieldReviewWindow())
: jsonResponse(fieldReviewCatalog());
}
return String(input).includes(packId)
? jsonResponse(detail())
: jsonResponse(catalog());
@@ -380,11 +577,19 @@ test("LiDAR fetchers use read-only endpoints and workspace is registered", async
0,
{ fetcher },
);
const fieldCatalog = await fetchLidarFieldReviews({ fetcher });
const fieldWindow = await fetchLidarFieldReviewWindow(
fieldReviewId,
0,
{ fetcher },
);
assert.equal(parsedCatalog.validTotal, 1);
assert.equal(parsedDetail.pack.packId, packId);
assert.equal(ground.validTotal, 1);
assert.equal(frame.pointCount, 3);
assert.equal(fieldCatalog.items[0].windows[0].sourceLidarSamples, 56);
assert.equal(fieldWindow.previewUrl.endsWith("/preview"), true);
assert.deepEqual(calls, [
{ input: "/api/v1/lidar/replay-packs?limit=50", method: "GET" },
{ input: `/api/v1/lidar/replay-packs/${packId}`, method: "GET" },
@@ -396,6 +601,14 @@ test("LiDAR fetchers use read-only endpoints and workspace is registered", async
input: `/api/v1/lidar/ground-benchmarks/ground-benchmark-${"c".repeat(64)}/frames/0`,
method: "GET",
},
{
input: "/api/v1/lidar/field-reviews?limit=10",
method: "GET",
},
{
input: `/api/v1/lidar/field-reviews/${fieldReviewId}/windows/0`,
method: "GET",
},
]);
assert.equal(workspaceById("lidar-quality").root, "data");
assert.equal(workspaceById("lidar-quality").kind, "lidar-quality");