feat: add passive K1 local surface replay

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 22:47:12 +03:00
parent cfe17e49bb
commit 04a658b218
16 changed files with 2715 additions and 21 deletions
@@ -9,7 +9,8 @@ export type LidarGroundViewMode =
| "disagreement"
| "candidate-disagreement"
| "semantic"
| "ground-truth";
| "ground-truth"
| "local-surface";
export interface LidarGroundPointCloudFrame {
pointCount: number;
@@ -25,6 +26,7 @@ export interface LidarGroundPointCloudFrame {
candidateDisagreement?: number[];
groundTruthGround?: number[];
evaluationMask?: number[];
localSurfaceClass?: number[];
};
}
@@ -67,7 +69,18 @@ function frameColors(
const current = frame.masks.currentGround[index] === 1;
const candidate = frame.masks.candidateGround[index] === 1;
const candidateAssigned = frame.masks.candidateAssigned[index] === 1;
if (mode === "intensity") {
if (mode === "local-surface") {
const localClass = frame.masks.localSurfaceClass?.[index] ?? 0;
if (localClass === 1) {
setRgb(colors, offset, 0.63, 0.72, 0.49);
} else if (localClass === 2) {
setRgb(colors, offset, 0.94, 0.48, 0.24);
} else if (localClass === 3) {
setRgb(colors, offset, 0.66, 0.32, 0.38);
} else {
setRgb(colors, offset, 0.27, 0.29, 0.28);
}
} else if (mode === "intensity") {
const intensity = (frame.intensity0To255?.[index] ?? 96) / 255;
const neutral = 0.16 + intensity * 0.8;
setRgb(
@@ -0,0 +1,219 @@
import { useEffect, useMemo, useState } from "react";
import { StatusBadge } from "@nodedc/ui-react";
import {
fetchLidarLocalSurfaceFrame,
fetchLidarLocalSurfaces,
type LidarLocalSurfaceFrame,
type LidarLocalSurfaceModel,
} from "../core/lidar/localSurface";
import { LidarGroundPointCloud } from "./LidarGroundPointCloud";
function formatNumber(value: number | null, digits = 2): string {
if (value === null) return "—";
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
function errorMessage(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "Не удалось открыть локальную модель LiDAR.";
}
export function LidarLocalSurfacePanel({
selectedWindowKey,
reloadGeneration,
}: {
selectedWindowKey: string | null;
reloadGeneration: number;
}) {
const [model, setModel] = useState<LidarLocalSurfaceModel | null>(null);
const [frame, setFrame] = useState<LidarLocalSurfaceFrame | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const anchor = useMemo(() => {
if (!model) return null;
return (
model.anchors.find((item) => item.key === selectedWindowKey)
?? model.anchors[0]
?? null
);
}, [model, selectedWindowKey]);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchLidarLocalSurfaces({ signal: controller.signal })
.then((catalog) => {
if (controller.signal.aborted) return;
setModel(catalog.items[0] ?? null);
})
.catch((loadError) => {
if (controller.signal.aborted) return;
setModel(null);
setFrame(null);
setError(errorMessage(loadError));
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [reloadGeneration]);
useEffect(() => {
if (!model || !anchor) {
setFrame(null);
return;
}
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchLidarLocalSurfaceFrame(model.modelId, anchor.frameIndex, {
signal: controller.signal,
})
.then((nextFrame) => {
if (!controller.signal.aborted) setFrame(nextFrame);
})
.catch((loadError) => {
if (controller.signal.aborted) return;
setFrame(null);
setError(errorMessage(loadError));
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [anchor, model]);
const cloudFrame = useMemo(() => {
if (!frame) return null;
const emptyMask = new Array<number>(frame.pointCount).fill(0);
return {
pointCount: frame.pointCount,
pointsXyzM: frame.pointsXyzM,
intensity0To255: null,
masks: {
currentGround: emptyMask,
currentAssigned: emptyMask,
candidateGround: emptyMask,
candidateAssigned: emptyMask,
disagreement: emptyMask,
localSurfaceClass: frame.pointClass,
},
};
}, [frame]);
if (!model && !loading && !error) {
return null;
}
return (
<section
className="lidar-local-surface"
aria-label="Динамическая локальная поверхность LiDAR"
>
<header className="lidar-local-surface__heading">
<div>
<span className="section-eyebrow">ЛОКАЛЬНАЯ МОДЕЛЬ LIDAR · L2.6</span>
<h3>Поверхность и наблюдаемые препятствия</h3>
<p>
Производная от неизменяемого RAVNOVES00: высота и уклон
вычисляются из текущей позы и локальной поверхности, без константы
1,27 м и без команд в сканер.
</p>
</div>
<StatusBadge tone={error ? "danger" : frame?.valid ? "success" : "warning"}>
{error
? "Недоступно"
: frame?.valid
? "Кадр рассчитан"
: "Диагностический режим"}
</StatusBadge>
</header>
{model ? (
<div className="lidar-local-surface__summary">
<div>
<span>Покрытие записи</span>
<strong>
{model.metrics.frames.valid.toLocaleString("ru-RU")} /{" "}
{model.metrics.frames.sourceAvailable.toLocaleString("ru-RU")}
</strong>
</div>
<div>
<span>Высота над поверхностью · p50</span>
<strong>{formatNumber(model.metrics.sensorHeightM.p50)} м</strong>
</div>
<div>
<span>Шероховатость · p95</span>
<strong>{formatNumber(model.metrics.roughnessM.p95, 3)} м</strong>
</div>
<div>
<span>Pose binding · p95</span>
<strong>{formatNumber(model.metrics.poseBindingAgeMs.p95)} мс</strong>
</div>
</div>
) : null}
<div className="lidar-local-surface__stage">
{cloudFrame && frame ? (
<LidarGroundPointCloud frame={cloudFrame} mode="local-surface" />
) : (
<div className="lidar-ground-scene-placeholder">
<StatusBadge tone={error ? "danger" : "accent"}>
{error ? "Ошибка" : "Загрузка"}
</StatusBadge>
<span>{error ?? "Читаем производную выбранной сцены…"}</span>
</div>
)}
{frame ? (
<aside className="lidar-local-surface__frame">
<div>
<span>Исходный кадр</span>
<strong>{frame.sourceFrameIndex}</strong>
<small>t = {formatNumber(frame.sessionSeconds)} с</small>
</div>
<dl>
<div>
<dt>Высота</dt>
<dd>{formatNumber(frame.surface.sensorHeightM)} м</dd>
</div>
<div>
<dt>Уклон</dt>
<dd>{formatNumber(frame.surface.slopeDeg)}°</dd>
</div>
<div>
<dt>Шероховатость</dt>
<dd>{formatNumber(frame.surface.roughnessM, 3)} м</dd>
</div>
<div>
<dt>Confidence</dt>
<dd>{formatNumber(frame.surface.confidence * 100, 0)}%</dd>
</div>
<div>
<dt>Поверхность</dt>
<dd>{frame.counts.surface.toLocaleString("ru-RU")} точек</dd>
</div>
<div>
<dt>Препятствия</dt>
<dd>{frame.counts.occupied.toLocaleString("ru-RU")} точек</dd>
</div>
</dl>
<div className="lidar-local-surface__legend">
<span><i data-class="surface" />Наблюдаемая поверхность</span>
<span><i data-class="occupied" />Выше поверхности</span>
<span><i data-class="below" />Нижний выброс</span>
<span><i data-class="unknown" />Не классифицировано</span>
</div>
<p>
Пустота между точками остаётся unknown. Этот слой не разрешает
движение и не меняет постоянную реконструкцию территории.
</p>
</aside>
) : null}
</div>
</section>
);
}
@@ -25,6 +25,7 @@ import {
LidarGroundPointCloud,
type LidarGroundViewMode,
} from "./LidarGroundPointCloud";
import { LidarLocalSurfacePanel } from "./LidarLocalSurfacePanel";
function formatNumber(value: number | null, digits = 1): string {
if (value === null) return "—";
@@ -460,6 +461,11 @@ export function LidarQualityWorkspace({
<span><i data-color="non-ground" />Оба non-ground</span>
</div>
</div>
<LidarLocalSurfacePanel
selectedWindowKey={selectedFieldWindow?.key ?? null}
reloadGeneration={reloadGeneration}
/>
</>
) : groundBenchmark ? (
<section