feat(ui): add M4.8S replay with LiDAR overlay
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user