feat(ui): add M4.8S replay with LiDAR overlay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-25 16:44:52 +03:00
parent 3679e43fe3
commit 15be698097
17 changed files with 1448 additions and 31 deletions
@@ -9,6 +9,10 @@ import {
RecordedEvidenceSemanticMaskOverlay,
type RecordedEvidenceSemanticOverlay,
} from "./RecordedEvidenceSemanticMaskOverlay";
import {
RecordedEvidencePointCloudOverlay,
type RecordedEvidencePointCloudOverlayData,
} from "./RecordedEvidencePointCloudOverlay";
export function RecordedEvidenceImageScene({
src,
@@ -16,6 +20,7 @@ export function RecordedEvidenceImageScene({
imageHeight,
boxes,
semanticOverlay,
pointCloudOverlay,
ariaLabel,
}: {
src: string;
@@ -23,6 +28,7 @@ export function RecordedEvidenceImageScene({
imageHeight: number;
boxes: readonly RecordedEvidenceBox[];
semanticOverlay?: RecordedEvidenceSemanticOverlay;
pointCloudOverlay?: RecordedEvidencePointCloudOverlayData;
ariaLabel: string;
}) {
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
@@ -47,6 +53,13 @@ export function RecordedEvidenceImageScene({
imageHeight={imageHeight}
/>
) : null}
{pointCloudOverlay ? (
<RecordedEvidencePointCloudOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
overlay={pointCloudOverlay}
/>
) : null}
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
@@ -0,0 +1,76 @@
import { useEffect, useRef } from "react";
export type RecordedEvidenceProjectedPoint = readonly [number, number, number];
export interface RecordedEvidencePointCloudOverlayData {
pointsXyd: readonly RecordedEvidenceProjectedPoint[];
sourcePointCount: number;
projectedPointCount: number;
projection: "factory-kb4-exact";
ariaLabel: string;
}
const DEPTH_BUCKETS = 64;
function depthColor(depthM: number): string {
const normalized = Math.max(0, Math.min(1, (depthM - 0.5) / 24));
const bucket = Math.round(normalized * (DEPTH_BUCKETS - 1));
const hue = 18 + bucket / (DEPTH_BUCKETS - 1) * 190;
return `hsla(${hue}, 96%, 62%, 0.86)`;
}
export function RecordedEvidencePointCloudOverlay({
imageWidth,
imageHeight,
overlay,
}: {
imageWidth: number;
imageHeight: number;
overlay: RecordedEvidencePointCloudOverlayData;
}) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
const host = canvas?.parentElement;
if (!canvas || !host) return;
const context = canvas.getContext("2d");
if (!context) return;
const render = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
const pixelRatio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * pixelRatio);
canvas.height = Math.round(height * pixelRatio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
context.clearRect(0, 0, width, height);
const scale = Math.min(width / imageWidth, height / imageHeight);
const offsetX = (width - imageWidth * scale) / 2;
const offsetY = (height - imageHeight * scale) / 2;
const radius = Math.max(0.8, Math.min(2.2, scale * 1.45));
for (const [imageX, imageY, depthM] of overlay.pointsXyd) {
context.beginPath();
context.arc(
offsetX + imageX * scale,
offsetY + imageY * scale,
radius,
0,
Math.PI * 2,
);
context.fillStyle = depthColor(depthM);
context.fill();
}
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [imageHeight, imageWidth, overlay]);
return <canvas ref={canvasRef} role="img" aria-label={overlay.ariaLabel} />;
}
@@ -12,6 +12,10 @@ import {
RecordedEvidenceSemanticMaskOverlay,
type RecordedEvidenceSemanticOverlay,
} from "./RecordedEvidenceSemanticMaskOverlay";
import {
RecordedEvidencePointCloudOverlay,
type RecordedEvidencePointCloudOverlayData,
} from "./RecordedEvidencePointCloudOverlay";
export type { RecordedEvidenceBox, RecordedEvidenceBoxTone };
@@ -22,6 +26,7 @@ export function RecordedEvidenceVideoScene({
imageHeight,
boxes,
semanticOverlay,
pointCloudOverlay,
ariaLabel,
interactive = true,
segmentSequence,
@@ -35,6 +40,7 @@ export function RecordedEvidenceVideoScene({
imageHeight: number;
boxes: readonly RecordedEvidenceBox[];
semanticOverlay?: RecordedEvidenceSemanticOverlay;
pointCloudOverlay?: RecordedEvidencePointCloudOverlayData;
ariaLabel: string;
interactive?: boolean;
segmentSequence?: number;
@@ -61,6 +67,13 @@ export function RecordedEvidenceVideoScene({
imageHeight={imageHeight}
/>
) : null}
{pointCloudOverlay ? (
<RecordedEvidencePointCloudOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
overlay={pointCloudOverlay}
/>
) : null}
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}