/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * BIM ↔ scan deviation heatmap controls. * * Renders a "Compute Deviation" button when the scene has at least * one mesh and one point cloud. Once compute completes, exposes a * range slider + diverging-ramp legend; the splat shader's * deviation colour mode then visualises signed distance to the * nearest mesh surface. * * Lives inside the `PointCloudPanel`; rendered conditionally on * `pointCloudAssetCount > 0`. */ import { useCallback, useState } from 'react'; import { useViewerStore } from '@/store'; import { getGlobalRenderer } from '@/hooks/useBCF'; import { cn } from '@/lib/utils'; export interface DeviationPanelProps { /** Total number of triangles currently in the scene — gates the * compute button on the existence of a BIM model. */ triangleCount: number; } export function DeviationPanel({ triangleCount }: DeviationPanelProps) { const halfRange = useViewerStore((s) => s.pointCloudDeviationHalfRange); const setHalfRange = useViewerStore((s) => s.setPointCloudDeviationHalfRange); const computed = useViewerStore((s) => s.pointCloudDeviationComputed); const setComputed = useViewerStore((s) => s.setPointCloudDeviationComputed); const colorMode = useViewerStore((s) => s.pointCloudColorMode); const setColorMode = useViewerStore((s) => s.setPointCloudColorMode); const [running, setRunning] = useState(false); const [stats, setStats] = useState<{ triangles: number; points: number; durationMs: number; } | null>(null); const [error, setError] = useState(null); const handleCompute = useCallback(async () => { const renderer = getGlobalRenderer(); if (!renderer) { setError('Renderer not initialised yet.'); return; } setError(null); setRunning(true); const t0 = performance.now(); try { const result = await renderer.computeDeviations({ maxRange: 1.0 }); const dt = performance.now() - t0; if (result.pointsProcessed === 0) { setError('No points processed — load a point cloud first.'); setRunning(false); return; } if (result.bvhTriangles === 0) { setError('No mesh geometry in the scene — load an IFC first.'); setRunning(false); return; } setStats({ triangles: result.bvhTriangles, points: result.pointsProcessed, durationMs: dt, }); setComputed(true); // Default-pick a sensible half-range from the BVH's bbox if the // user hasn't touched the slider yet (initial 5 cm is fine for // small models but useless for a city-block scan). if (halfRange === 0.05 && result.suggestedHalfRange !== 0.05) { setHalfRange(result.suggestedHalfRange); } // Auto-switch the colour mode to deviation so the user sees // the result immediately. setColorMode('deviation'); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setRunning(false); } }, [halfRange, setHalfRange, setColorMode, setComputed]); // Hide the panel entirely when there's no BIM to compare against. // Point-cloud-only sessions (just a LAS / IFCx scan) have nothing // to deviate from so the button would always fail. if (triangleCount === 0) return null; return (
Deviation (BIM ↔ scan) {error && ( {error} )} {stats && (
{stats.points.toLocaleString()} pts vs.{' '} {stats.triangles.toLocaleString()} tris in{' '} {Math.round(stats.durationMs)} ms
)} {computed && ( <> {/* Range slider: half-width in mm. Range from 1 mm to 1 m (logarithmic feel via the millimetre conversion). */} {/* Legend: blue → white → red gradient with labelled endpoints. */}
−{(halfRange * 1000).toFixed(0)}mm (inside) 0 +{(halfRange * 1000).toFixed(0)}mm (outside)
{colorMode !== 'deviation' && ( )} )}
); }