/* 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/. */ /** * Compact panel that exposes point cloud rendering controls (color mode, * size mode, point size, EDL). Renders only when point cloud assets are * loaded — sits over the canvas without affecting layout for IFC-only * models. */ import { useViewerStore } from '@/store'; import type { PointColorModeUi, PointSizeModeUi } from '@/store/slices/pointCloudSlice'; import { cn } from '@/lib/utils'; import { PointCloudLegend } from './PointCloudLegend'; import { PointCloudClasses } from './PointCloudClasses'; import { DeviationPanel } from './DeviationPanel'; const COLOR_MODES: Array<{ value: PointColorModeUi; label: string; hint: string }> = [ { value: 'rgb', label: 'RGB', hint: 'Per-point colour from the source' }, { value: 'classification', label: 'Classification', hint: 'ASPRS class palette (ground, vegetation, building...)' }, { value: 'intensity', label: 'Intensity', hint: 'Greyscale ramp from per-point intensity' }, { value: 'height', label: 'Height', hint: 'Cool-warm ramp by Y-up world height' }, { value: 'fixed', label: 'Solid', hint: 'Single colour override' }, { value: 'deviation', label: 'Deviation', hint: 'Signed distance to nearest BIM surface (compute below)' }, ]; const SIZE_MODES: Array<{ value: PointSizeModeUi; label: string; hint: string }> = [ { value: 'fixed-px', label: 'Fixed', hint: 'Always render at the slider value (in pixels)' }, { value: 'attenuated', label: 'Auto', hint: 'Adaptive (closer = bigger), clamped to the slider as max' }, { value: 'adaptive-world', label: 'World', hint: 'Pure world-space radius — splat covers N mm in source space' }, ]; export interface PointCloudPanelProps { /** Number of currently-loaded point cloud assets — panel hides when 0. */ assetCount: number; /** Total triangle count across the scene (gates the BIM↔scan deviation * compute button — useless without a BIM model loaded). */ triangleCount: number; } export function PointCloudPanel({ assetCount, triangleCount }: PointCloudPanelProps) { const colorMode = useViewerStore((s) => s.pointCloudColorMode); const setColorMode = useViewerStore((s) => s.setPointCloudColorMode); const sizeMode = useViewerStore((s) => s.pointCloudSizeMode); const setSizeMode = useViewerStore((s) => s.setPointCloudSizeMode); const pointSize = useViewerStore((s) => s.pointCloudPointSize); const setPointSize = useViewerStore((s) => s.setPointCloudPointSize); const worldRadius = useViewerStore((s) => s.pointCloudWorldRadius); const setWorldRadius = useViewerStore((s) => s.setPointCloudWorldRadius); const edlEnabled = useViewerStore((s) => s.pointCloudEdlEnabled); const setEdlEnabled = useViewerStore((s) => s.setPointCloudEdlEnabled); const edlStrength = useViewerStore((s) => s.pointCloudEdlStrength); const setEdlStrength = useViewerStore((s) => s.setPointCloudEdlStrength); const fixedColor = useViewerStore((s) => s.pointCloudFixedColor); const setFixedColor = useViewerStore((s) => s.setPointCloudFixedColor); if (assetCount <= 0) return null; return (
Point Cloud {assetCount} asset{assetCount === 1 ? '' : 's'}
{/* Color mode */}
Colour {COLOR_MODES.map((mode) => { const active = colorMode === mode.value; return ( ); })} {colorMode === 'fixed' && ( // Native colour input — keeps the panel dependency-free. // Hex round-trips through float[0..1]: parse `#rrggbb` to a // [r,g,b,1] tuple on input, format the active rgb back to hex // on display. Alpha stays 1 since fixed-mode opacity is // controlled by the splat shape, not the colour swatch. )}
{/* Per-ASPRS-class visibility — toggles the splat shader's class-mask uniform; works in any colour mode but most discoverable when colorMode === 'classification'. */} {/* Size mode */}
Size
{SIZE_MODES.map((mode) => { const active = sizeMode === mode.value; return ( ); })}
{sizeMode !== 'fixed-px' && ( )}
{/* EDL */}
{edlEnabled && ( )}
{/* BIM↔scan deviation heatmap — only useful when both meshes and points are loaded. The panel renders nothing when there are no triangles in the scene. */}
); } function rgbToHex([r, g, b]: [number, number, number, number]): string { const c = (v: number) => Math.max(0, Math.min(255, Math.round(v * 255))).toString(16).padStart(2, '0'); return `#${c(r)}${c(g)}${c(b)}`; } function hexToRgba(hex: string, alpha: number): [number, number, number, number] { // Browsers always emit "#rrggbb" from , so we // can skip the 3-char shorthand path. Parse byte-by-byte and divide. const r = parseInt(hex.slice(1, 3), 16) / 255; const g = parseInt(hex.slice(3, 5), 16) / 255; const b = parseInt(hex.slice(5, 7), 16) / 255; return [r, g, b, alpha]; }