@ifc-lite/viewer 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.turbo/turbo-build.log +59 -43
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +496 -0
  4. package/dist/assets/basketViewActivator-Bzw51jhm.js +71 -0
  5. package/dist/assets/{bcf-DOG9_WPX.js → bcf-4K724hw0.js} +18 -18
  6. package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
  7. package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
  8. package/dist/assets/exporters-u0sz2Upj.js +259119 -0
  9. package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
  10. package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
  11. package/dist/assets/ids-B7AXEv7h.js +4067 -0
  12. package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
  13. package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
  14. package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
  15. package/dist/assets/index-CSWgTe1s.css +1 -0
  16. package/dist/assets/{index-BOi3BuUI.js → index-DVNSvEMh.js} +49877 -28410
  17. package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
  18. package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
  19. package/dist/assets/{native-bridge-CpBeOPQa.js → native-bridge-BiD01jI9.js} +2 -2
  20. package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
  21. package/dist/assets/{sandbox-Baez7n-t.js → sandbox-DPD1ROr0.js} +548 -530
  22. package/dist/assets/{server-client-BB6cMAXE.js → server-client-DP8fMPY9.js} +1 -1
  23. package/dist/assets/three-CDRZThFA.js +4057 -0
  24. package/dist/assets/{wasm-bridge-CAYCUHbE.js → wasm-bridge-CErti6zX.js} +1 -1
  25. package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
  26. package/dist/index.html +10 -9
  27. package/dist/samples/building-architecture.ifc +453 -0
  28. package/dist/samples/hello-wall.ifc +1054 -0
  29. package/dist/samples/infra-bridge.ifc +962 -0
  30. package/index.html +1 -1
  31. package/package.json +15 -10
  32. package/public/samples/building-architecture.ifc +453 -0
  33. package/public/samples/hello-wall.ifc +1054 -0
  34. package/public/samples/infra-bridge.ifc +962 -0
  35. package/src/App.tsx +37 -3
  36. package/src/components/mcp/HeroScene.tsx +876 -0
  37. package/src/components/mcp/McpLanding.tsx +1318 -0
  38. package/src/components/mcp/McpPlayground.tsx +524 -0
  39. package/src/components/mcp/PlaygroundChat.tsx +1097 -0
  40. package/src/components/mcp/PlaygroundViewer.tsx +815 -0
  41. package/src/components/mcp/README.md +171 -0
  42. package/src/components/mcp/data.ts +659 -0
  43. package/src/components/mcp/playground-dispatcher.ts +1649 -0
  44. package/src/components/mcp/playground-files.ts +107 -0
  45. package/src/components/mcp/playground-uploads.ts +122 -0
  46. package/src/components/mcp/types.ts +65 -0
  47. package/src/components/mcp/use-mcp-page.ts +109 -0
  48. package/src/components/viewer/BasketPresentationDock.tsx +3 -0
  49. package/src/components/viewer/CesiumOverlay.tsx +165 -120
  50. package/src/components/viewer/DeviationPanel.tsx +172 -0
  51. package/src/components/viewer/HierarchyPanel.tsx +29 -3
  52. package/src/components/viewer/HoverTooltip.tsx +5 -0
  53. package/src/components/viewer/IDSAuditSummary.tsx +389 -0
  54. package/src/components/viewer/IDSPanel.tsx +80 -26
  55. package/src/components/viewer/MainToolbar.tsx +79 -7
  56. package/src/components/viewer/MergeLayersBanner.tsx +108 -0
  57. package/src/components/viewer/MobileToolbar.tsx +326 -0
  58. package/src/components/viewer/PointCloudClasses.tsx +111 -0
  59. package/src/components/viewer/PointCloudLegend.tsx +119 -0
  60. package/src/components/viewer/PointCloudPanel.tsx +52 -1
  61. package/src/components/viewer/PropertiesPanel.tsx +37 -6
  62. package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
  63. package/src/components/viewer/StatusBar.tsx +14 -0
  64. package/src/components/viewer/ViewerLayout.tsx +288 -95
  65. package/src/components/viewer/Viewport.tsx +86 -18
  66. package/src/components/viewer/ViewportContainer.tsx +60 -15
  67. package/src/components/viewer/ViewportOverlays.tsx +41 -26
  68. package/src/components/viewer/mouseHandlerTypes.ts +22 -0
  69. package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
  70. package/src/components/viewer/properties/MaterialCard.tsx +2 -2
  71. package/src/components/viewer/selectionHandlers.ts +41 -0
  72. package/src/components/viewer/tools/SectionPanel.tsx +181 -24
  73. package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
  74. package/src/components/viewer/useAnimationLoop.ts +22 -0
  75. package/src/components/viewer/useMouseControls.ts +296 -3
  76. package/src/components/viewer/usePointCloudSync.ts +8 -1
  77. package/src/components/viewer/useRenderUpdates.ts +21 -1
  78. package/src/components/viewer/useTouchControls.ts +100 -41
  79. package/src/generated/mcp-catalog.json +82 -0
  80. package/src/hooks/federationLoadGate.test.ts +90 -0
  81. package/src/hooks/federationLoadGate.ts +127 -0
  82. package/src/hooks/ids/idsDataAccessor.ts +11 -259
  83. package/src/hooks/ingest/pointCloudIngest.ts +127 -16
  84. package/src/hooks/useDrawingGeneration.ts +81 -8
  85. package/src/hooks/useIDS.ts +90 -10
  86. package/src/hooks/useIfcFederation.ts +94 -16
  87. package/src/hooks/useIfcLoader.ts +289 -64
  88. package/src/hooks/useViewerSelectors.ts +10 -0
  89. package/src/lib/geo/cesium-bridge.ts +84 -67
  90. package/src/lib/geo/clamp-anchor.test.ts +80 -0
  91. package/src/lib/geo/clamp-anchor.ts +57 -0
  92. package/src/lib/geo/effective-georef.test.ts +79 -1
  93. package/src/lib/geo/effective-georef.ts +83 -0
  94. package/src/lib/geo/reproject.ts +26 -13
  95. package/src/lib/geo/terrain-elevation.ts +166 -0
  96. package/src/lib/lens/adapter.ts +1 -1
  97. package/src/lib/llm/context-builder.ts +1 -1
  98. package/src/lib/perf/memoryAccounting.test.ts +92 -0
  99. package/src/lib/perf/memoryAccounting.ts +235 -0
  100. package/src/sdk/adapters/mutation-view.ts +1 -1
  101. package/src/store/constants.ts +39 -2
  102. package/src/store/index.ts +6 -1
  103. package/src/store/slices/cesiumSlice.ts +1 -1
  104. package/src/store/slices/idsSlice.ts +24 -0
  105. package/src/store/slices/loadingSlice.ts +12 -0
  106. package/src/store/slices/pointCloudSlice.ts +72 -1
  107. package/src/store/slices/sectionSlice.test.ts +590 -1
  108. package/src/store/slices/sectionSlice.ts +344 -17
  109. package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
  110. package/src/store/slices/uiSlice.ts +60 -2
  111. package/src/store/types.ts +42 -0
  112. package/src/store.ts +13 -0
  113. package/src/utils/acquireFileBuffer.test.ts +231 -0
  114. package/src/utils/acquireFileBuffer.ts +128 -0
  115. package/src/utils/ifcConfig.ts +24 -0
  116. package/src/utils/nativeSpatialDataStore.ts +20 -2
  117. package/src/utils/spatialHierarchy.test.ts +116 -0
  118. package/src/utils/spatialHierarchy.ts +23 -0
  119. package/tailwind.config.js +5 -0
  120. package/tsconfig.json +1 -0
  121. package/vite.config.ts +12 -0
  122. package/dist/assets/basketViewActivator-RZy5c3Td.js +0 -1
  123. package/dist/assets/decode-worker-Collf_X_.js +0 -1320
  124. package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
  125. package/dist/assets/exporters-BraHBeoi.js +0 -81583
  126. package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
  127. package/dist/assets/ids-DQ5jY0E8.js +0 -1
  128. package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
  129. package/dist/assets/index-0XpVr_S5.css +0 -1
@@ -0,0 +1,111 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Per-ASPRS-class visibility toggles. Renders an inline list of every
7
+ * known LAS 1.4 standard class with a checkbox bound to the
8
+ * `pointCloudClassMask` bitmask. Hidden classes are pushed behind the
9
+ * near plane in the splat shader (`flags.w` cull).
10
+ *
11
+ * The colour swatches mirror `point-shader.wgsl.ts` so the UI stays
12
+ * in sync with what the user actually sees on screen.
13
+ */
14
+
15
+ import { useViewerStore } from '@/store';
16
+
17
+ interface ClassEntry {
18
+ id: number;
19
+ label: string;
20
+ rgb: [number, number, number];
21
+ }
22
+
23
+ const CLASSES: ClassEntry[] = [
24
+ { id: 0, label: 'Never classified', rgb: [0.65, 0.65, 0.65] },
25
+ { id: 1, label: 'Unclassified', rgb: [0.65, 0.65, 0.65] },
26
+ { id: 2, label: 'Ground', rgb: [0.55, 0.40, 0.25] },
27
+ { id: 3, label: 'Low vegetation', rgb: [0.55, 0.85, 0.45] },
28
+ { id: 4, label: 'Medium vegetation', rgb: [0.30, 0.75, 0.30] },
29
+ { id: 5, label: 'High vegetation', rgb: [0.10, 0.45, 0.15] },
30
+ { id: 6, label: 'Building', rgb: [0.95, 0.55, 0.20] },
31
+ { id: 7, label: 'Low point (noise)', rgb: [0.95, 0.20, 0.20] },
32
+ { id: 9, label: 'Water', rgb: [0.20, 0.40, 0.95] },
33
+ { id: 10, label: 'Rail', rgb: [0.55, 0.20, 0.85] },
34
+ { id: 11, label: 'Road surface', rgb: [0.30, 0.30, 0.30] },
35
+ { id: 13, label: 'Wire — guard', rgb: [0.95, 0.85, 0.20] },
36
+ { id: 14, label: 'Wire — conductor', rgb: [0.95, 0.95, 0.50] },
37
+ { id: 15, label: 'Transmission tower', rgb: [0.20, 0.20, 0.55] },
38
+ { id: 16, label: 'Wire-structure', rgb: [0.30, 0.65, 0.65] },
39
+ { id: 17, label: 'Bridge deck', rgb: [0.85, 0.70, 0.50] },
40
+ { id: 18, label: 'High noise', rgb: [0.95, 0.20, 0.20] },
41
+ ];
42
+
43
+ const ALL_VISIBLE = 0xFFFFFFFF;
44
+
45
+ export function PointCloudClasses() {
46
+ const mask = useViewerStore((s) => s.pointCloudClassMask);
47
+ const toggle = useViewerStore((s) => s.togglePointCloudClass);
48
+ const setMask = useViewerStore((s) => s.setPointCloudClassMask);
49
+ const allOn = (mask >>> 0) === ALL_VISIBLE;
50
+ return (
51
+ <details className="flex flex-col gap-0.5">
52
+ <summary className="text-[9px] uppercase text-muted-foreground tracking-wider cursor-pointer select-none">
53
+ Classes {!allOn && (
54
+ <span className="text-[9px] normal-case text-amber-500"> · {countSet(mask)} of 32 visible</span>
55
+ )}
56
+ </summary>
57
+ <div className="flex flex-col gap-0.5 mt-1 max-h-40 overflow-y-auto pr-1">
58
+ <button
59
+ type="button"
60
+ onClick={() => setMask(ALL_VISIBLE)}
61
+ className="text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted px-1 py-0.5 rounded text-left"
62
+ >
63
+ Show all
64
+ </button>
65
+ {CLASSES.map((c) => {
66
+ const visible = ((mask >>> c.id) & 1) !== 0;
67
+ return (
68
+ <label
69
+ key={c.id}
70
+ className="flex items-center gap-1.5 text-[10px] cursor-pointer hover:bg-muted/40 rounded px-1 py-0.5"
71
+ >
72
+ <input
73
+ type="checkbox"
74
+ checked={visible}
75
+ onChange={() => toggle(c.id)}
76
+ className="accent-teal-600"
77
+ aria-label={`Toggle ${c.label}`}
78
+ />
79
+ <span
80
+ className="inline-block h-3 w-3 rounded-sm shrink-0 border border-foreground/10"
81
+ style={{ backgroundColor: rgbCss(c.rgb) }}
82
+ aria-hidden="true"
83
+ />
84
+ <span className="text-muted-foreground tabular-nums w-4 shrink-0">{c.id}</span>
85
+ <span className={visible ? 'text-foreground truncate' : 'text-muted-foreground line-through truncate'}>
86
+ {c.label}
87
+ </span>
88
+ </label>
89
+ );
90
+ })}
91
+ </div>
92
+ </details>
93
+ );
94
+ }
95
+
96
+ function countSet(mask: number): number {
97
+ // Hamming weight via Brian Kernighan's algorithm. JS bitwise ops
98
+ // are 32-bit so we naturally cover the full ASPRS range.
99
+ let n = mask >>> 0;
100
+ let count = 0;
101
+ while (n !== 0) {
102
+ n &= n - 1;
103
+ count++;
104
+ }
105
+ return count;
106
+ }
107
+
108
+ function rgbCss([r, g, b]: [number, number, number]): string {
109
+ const c = (v: number) => Math.max(0, Math.min(255, Math.round(v * 255)));
110
+ return `rgb(${c(r)},${c(g)},${c(b)})`;
111
+ }
@@ -0,0 +1,119 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Per-mode legend for the point-cloud panel.
7
+ *
8
+ * Renders only when the active colour mode benefits from a legend
9
+ * (classification / intensity / height); RGB and Solid don't need one.
10
+ * The palettes here MUST stay in sync with `point-shader.wgsl.ts` —
11
+ * any colour change in the shader has to come back to this file.
12
+ */
13
+
14
+ import type { PointColorModeUi } from '@/store/slices/pointCloudSlice';
15
+
16
+ interface ClassificationEntry {
17
+ id: number;
18
+ label: string;
19
+ rgb: [number, number, number];
20
+ }
21
+
22
+ // ASPRS LAS 1.4 standard classes — ids that don't appear here all
23
+ // fall back to the shader's "default" entry (0.65 grey) and are
24
+ // shown collectively at the bottom of the legend.
25
+ const CLASSIFICATION: ClassificationEntry[] = [
26
+ { id: 0, label: 'Never classified', rgb: [0.65, 0.65, 0.65] },
27
+ { id: 1, label: 'Unclassified', rgb: [0.65, 0.65, 0.65] },
28
+ { id: 2, label: 'Ground', rgb: [0.55, 0.40, 0.25] },
29
+ { id: 3, label: 'Low vegetation', rgb: [0.55, 0.85, 0.45] },
30
+ { id: 4, label: 'Medium vegetation', rgb: [0.30, 0.75, 0.30] },
31
+ { id: 5, label: 'High vegetation', rgb: [0.10, 0.45, 0.15] },
32
+ { id: 6, label: 'Building', rgb: [0.95, 0.55, 0.20] },
33
+ { id: 7, label: 'Low point (noise)', rgb: [0.95, 0.20, 0.20] },
34
+ { id: 9, label: 'Water', rgb: [0.20, 0.40, 0.95] },
35
+ { id: 10, label: 'Rail', rgb: [0.55, 0.20, 0.85] },
36
+ { id: 11, label: 'Road surface', rgb: [0.30, 0.30, 0.30] },
37
+ { id: 13, label: 'Wire — guard', rgb: [0.95, 0.85, 0.20] },
38
+ { id: 14, label: 'Wire — conductor', rgb: [0.95, 0.95, 0.50] },
39
+ { id: 15, label: 'Transmission tower', rgb: [0.20, 0.20, 0.55] },
40
+ { id: 16, label: 'Wire-structure', rgb: [0.30, 0.65, 0.65] },
41
+ { id: 17, label: 'Bridge deck', rgb: [0.85, 0.70, 0.50] },
42
+ { id: 18, label: 'High noise', rgb: [0.95, 0.20, 0.20] },
43
+ ];
44
+
45
+ const HEIGHT_GRADIENT =
46
+ 'linear-gradient(to right, '
47
+ + 'rgb(26,51,217), ' // 0.10, 0.20, 0.85
48
+ + 'rgb(26,217,217), ' // 0.10, 0.85, 0.85
49
+ + 'rgb(51,217,51), ' // 0.20, 0.85, 0.20
50
+ + 'rgb(242,242,51), ' // 0.95, 0.95, 0.20
51
+ + 'rgb(242,51,26))'; // 0.95, 0.20, 0.10
52
+
53
+ export interface PointCloudLegendProps {
54
+ colorMode: PointColorModeUi;
55
+ }
56
+
57
+ export function PointCloudLegend({ colorMode }: PointCloudLegendProps) {
58
+ if (colorMode === 'classification') {
59
+ return (
60
+ <div className="flex flex-col gap-0.5 mt-1 max-h-40 overflow-y-auto">
61
+ <span className="text-[9px] uppercase text-muted-foreground tracking-wider sticky top-0 bg-background/95 py-0.5">
62
+ Classes (ASPRS LAS 1.4)
63
+ </span>
64
+ {CLASSIFICATION.map((c) => (
65
+ <div key={c.id} className="flex items-center gap-1.5 text-[10px]">
66
+ <span
67
+ className="inline-block h-3 w-3 rounded-sm shrink-0 border border-foreground/10"
68
+ style={{ backgroundColor: rgbCss(c.rgb) }}
69
+ aria-hidden="true"
70
+ />
71
+ <span className="text-muted-foreground tabular-nums w-4 shrink-0">{c.id}</span>
72
+ <span className="text-foreground truncate">{c.label}</span>
73
+ </div>
74
+ ))}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ if (colorMode === 'intensity') {
80
+ return (
81
+ <div className="flex flex-col gap-0.5 mt-1">
82
+ <span className="text-[9px] uppercase text-muted-foreground tracking-wider">Intensity</span>
83
+ <div
84
+ className="h-2 rounded-sm border border-foreground/10"
85
+ style={{ background: 'linear-gradient(to right, rgb(0,0,0), rgb(255,255,255))' }}
86
+ aria-label="Intensity ramp from low (black) to high (white)"
87
+ />
88
+ <div className="flex justify-between text-[9px] text-muted-foreground">
89
+ <span>low</span>
90
+ <span>high</span>
91
+ </div>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ if (colorMode === 'height') {
97
+ return (
98
+ <div className="flex flex-col gap-0.5 mt-1">
99
+ <span className="text-[9px] uppercase text-muted-foreground tracking-wider">Height (Y-up)</span>
100
+ <div
101
+ className="h-2 rounded-sm border border-foreground/10"
102
+ style={{ background: HEIGHT_GRADIENT }}
103
+ aria-label="Height ramp from low (blue) to high (red)"
104
+ />
105
+ <div className="flex justify-between text-[9px] text-muted-foreground">
106
+ <span>low</span>
107
+ <span>high</span>
108
+ </div>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ return null;
114
+ }
115
+
116
+ function rgbCss([r, g, b]: [number, number, number]): string {
117
+ const c = (v: number) => Math.max(0, Math.min(255, Math.round(v * 255)));
118
+ return `rgb(${c(r)},${c(g)},${c(b)})`;
119
+ }
@@ -12,6 +12,9 @@
12
12
  import { useViewerStore } from '@/store';
13
13
  import type { PointColorModeUi, PointSizeModeUi } from '@/store/slices/pointCloudSlice';
14
14
  import { cn } from '@/lib/utils';
15
+ import { PointCloudLegend } from './PointCloudLegend';
16
+ import { PointCloudClasses } from './PointCloudClasses';
17
+ import { DeviationPanel } from './DeviationPanel';
15
18
 
16
19
  const COLOR_MODES: Array<{ value: PointColorModeUi; label: string; hint: string }> = [
17
20
  { value: 'rgb', label: 'RGB', hint: 'Per-point colour from the source' },
@@ -19,6 +22,7 @@ const COLOR_MODES: Array<{ value: PointColorModeUi; label: string; hint: string
19
22
  { value: 'intensity', label: 'Intensity', hint: 'Greyscale ramp from per-point intensity' },
20
23
  { value: 'height', label: 'Height', hint: 'Cool-warm ramp by Y-up world height' },
21
24
  { value: 'fixed', label: 'Solid', hint: 'Single colour override' },
25
+ { value: 'deviation', label: 'Deviation', hint: 'Signed distance to nearest BIM surface (compute below)' },
22
26
  ];
23
27
 
24
28
  const SIZE_MODES: Array<{ value: PointSizeModeUi; label: string; hint: string }> = [
@@ -30,9 +34,12 @@ const SIZE_MODES: Array<{ value: PointSizeModeUi; label: string; hint: string }>
30
34
  export interface PointCloudPanelProps {
31
35
  /** Number of currently-loaded point cloud assets — panel hides when 0. */
32
36
  assetCount: number;
37
+ /** Total triangle count across the scene (gates the BIM↔scan deviation
38
+ * compute button — useless without a BIM model loaded). */
39
+ triangleCount: number;
33
40
  }
34
41
 
35
- export function PointCloudPanel({ assetCount }: PointCloudPanelProps) {
42
+ export function PointCloudPanel({ assetCount, triangleCount }: PointCloudPanelProps) {
36
43
  const colorMode = useViewerStore((s) => s.pointCloudColorMode);
37
44
  const setColorMode = useViewerStore((s) => s.setPointCloudColorMode);
38
45
  const sizeMode = useViewerStore((s) => s.pointCloudSizeMode);
@@ -45,6 +52,8 @@ export function PointCloudPanel({ assetCount }: PointCloudPanelProps) {
45
52
  const setEdlEnabled = useViewerStore((s) => s.setPointCloudEdlEnabled);
46
53
  const edlStrength = useViewerStore((s) => s.pointCloudEdlStrength);
47
54
  const setEdlStrength = useViewerStore((s) => s.setPointCloudEdlStrength);
55
+ const fixedColor = useViewerStore((s) => s.pointCloudFixedColor);
56
+ const setFixedColor = useViewerStore((s) => s.setPointCloudFixedColor);
48
57
 
49
58
  if (assetCount <= 0) return null;
50
59
 
@@ -81,8 +90,31 @@ export function PointCloudPanel({ assetCount }: PointCloudPanelProps) {
81
90
  </button>
82
91
  );
83
92
  })}
93
+ <PointCloudLegend colorMode={colorMode} />
94
+ {colorMode === 'fixed' && (
95
+ // Native colour input — keeps the panel dependency-free.
96
+ // Hex round-trips through float[0..1]: parse `#rrggbb` to a
97
+ // [r,g,b,1] tuple on input, format the active rgb back to hex
98
+ // on display. Alpha stays 1 since fixed-mode opacity is
99
+ // controlled by the splat shape, not the colour swatch.
100
+ <label className="flex items-center justify-between gap-2 mt-1 px-2 py-1 rounded bg-muted/40">
101
+ <span className="text-[10px] text-muted-foreground">Solid colour</span>
102
+ <input
103
+ type="color"
104
+ value={rgbToHex(fixedColor)}
105
+ onChange={(e) => setFixedColor(hexToRgba(e.target.value, fixedColor[3]))}
106
+ aria-label="Pick the solid colour applied in fixed mode"
107
+ className="h-6 w-10 rounded border-0 cursor-pointer bg-transparent"
108
+ />
109
+ </label>
110
+ )}
84
111
  </div>
85
112
 
113
+ {/* Per-ASPRS-class visibility — toggles the splat shader's
114
+ class-mask uniform; works in any colour mode but most
115
+ discoverable when colorMode === 'classification'. */}
116
+ <PointCloudClasses />
117
+
86
118
  {/* Size mode */}
87
119
  <div className="flex flex-col gap-0.5">
88
120
  <span className="text-[9px] uppercase text-muted-foreground tracking-wider">Size</span>
@@ -169,6 +201,25 @@ export function PointCloudPanel({ assetCount }: PointCloudPanelProps) {
169
201
  </label>
170
202
  )}
171
203
  </div>
204
+
205
+ {/* BIM↔scan deviation heatmap — only useful when both meshes
206
+ and points are loaded. The panel renders nothing when there
207
+ are no triangles in the scene. */}
208
+ <DeviationPanel triangleCount={triangleCount} />
172
209
  </div>
173
210
  );
174
211
  }
212
+
213
+ function rgbToHex([r, g, b]: [number, number, number, number]): string {
214
+ const c = (v: number) => Math.max(0, Math.min(255, Math.round(v * 255))).toString(16).padStart(2, '0');
215
+ return `#${c(r)}${c(g)}${c(b)}`;
216
+ }
217
+
218
+ function hexToRgba(hex: string, alpha: number): [number, number, number, number] {
219
+ // Browsers always emit "#rrggbb" from <input type="color">, so we
220
+ // can skip the 3-char shorthand path. Parse byte-by-byte and divide.
221
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
222
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
223
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
224
+ return [r, g, b, alpha];
225
+ }
@@ -11,6 +11,7 @@ import {
11
11
  Eye,
12
12
  Building2,
13
13
  Layers,
14
+ Layers2,
14
15
  FileText,
15
16
  Calculator,
16
17
  Tag,
@@ -20,6 +21,7 @@ import {
20
21
  PenLine,
21
22
  Crosshair,
22
23
  } from 'lucide-react';
24
+ import { Badge } from '@/components/ui/badge';
23
25
  import { EditToolbar } from './PropertyEditor';
24
26
  import { Button } from '@/components/ui/button';
25
27
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -134,6 +136,10 @@ export function PropertiesPanel() {
134
136
  const cameraCallbacks = useViewerStore((s) => s.cameraCallbacks);
135
137
  const toggleEntityVisibility = useViewerStore((s) => s.toggleEntityVisibility);
136
138
  const isEntityVisible = useViewerStore((s) => s.isEntityVisible);
139
+ // Issue #540: surface a small "Layers merged" badge on walls when
140
+ // the user has the merge-layers load setting active so they
141
+ // understand the displayed solid is the aggregated representation.
142
+ const mergeLayersActive = useViewerStore((s) => s.mergeLayers);
137
143
  const { query, ifcDataStore, geometryResult, models, getQueryForModel } = useIfc();
138
144
 
139
145
  // Get model-aware query based on selectedEntity
@@ -1108,9 +1114,33 @@ export function PropertiesPanel() {
1108
1114
  <Building2 className="h-5 w-5 text-zinc-700 dark:text-zinc-300" />
1109
1115
  </div>
1110
1116
  <div className="flex-1 min-w-0 pt-0.5">
1111
- <h3 className="font-bold text-sm truncate uppercase tracking-tight text-zinc-900 dark:text-zinc-100">
1112
- {entityName || `${entityType}`}
1113
- </h3>
1117
+ <div className="flex items-start gap-2">
1118
+ <h3 className="font-bold text-sm truncate uppercase tracking-tight text-zinc-900 dark:text-zinc-100 min-w-0">
1119
+ {entityName || `${entityType}`}
1120
+ </h3>
1121
+ {/* Issue #540: indicate that the wall solid the user is
1122
+ looking at represents aggregated multilayer parts. We
1123
+ over-trigger on any IfcWall* class instead of probing
1124
+ the aggregation graph — the chip is cheap and
1125
+ informative, and walls that aren't actually layered
1126
+ simply confirm the user's selection is the parent. */}
1127
+ {mergeLayersActive && entityType?.toLowerCase().startsWith('ifcwall') && (
1128
+ <Tooltip>
1129
+ <TooltipTrigger asChild>
1130
+ <Badge
1131
+ variant="secondary"
1132
+ className="shrink-0 rounded-sm px-1.5 py-0 text-[9px] font-semibold uppercase tracking-wider gap-1 leading-none h-[18px] mt-0.5"
1133
+ >
1134
+ <Layers2 className="h-2.5 w-2.5" />
1135
+ Layers merged
1136
+ </Badge>
1137
+ </TooltipTrigger>
1138
+ <TooltipContent>
1139
+ Multilayer wall parts have been merged into the parent solid.
1140
+ </TooltipContent>
1141
+ </Tooltip>
1142
+ )}
1143
+ </div>
1114
1144
  <p className="text-xs font-mono text-zinc-500 dark:text-zinc-400">{entityType}</p>
1115
1145
  {/* Show associated type entity for occurrences */}
1116
1146
  {!renderedIsTypeEntity && renderedTypeProperties && (
@@ -1309,6 +1339,7 @@ export function PropertiesPanel() {
1309
1339
  coordinateInfo={(model?.geometryResult ?? geometryResult)?.coordinateInfo}
1310
1340
  geometryResult={model?.geometryResult ?? geometryResult}
1311
1341
  lengthUnitScale={lengthUnitScale}
1342
+ storeyElevations={activeDataStore?.spatialHierarchy?.storeyElevations}
1312
1343
  />
1313
1344
  </CollapsibleContent>
1314
1345
  </Collapsible>
@@ -1342,12 +1373,12 @@ export function PropertiesPanel() {
1342
1373
  modelId={selectedEntity.modelId}
1343
1374
  entityId={selectedEntity.expressId}
1344
1375
  attrName={attr.name}
1345
- currentValue={attr.value}
1376
+ currentValue={String(attr.value)}
1346
1377
  />
1347
1378
  ) : (
1348
1379
  <div className="overflow-x-auto scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 min-w-0">
1349
- <span className="font-medium whitespace-nowrap" title={attr.value}>
1350
- {attr.value}
1380
+ <span className="font-medium whitespace-nowrap" title={String(attr.value)}>
1381
+ {String(attr.value)}
1351
1382
  </span>
1352
1383
  </div>
1353
1384
  )}
@@ -0,0 +1,48 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Visual overlay for the GPU rectangle-select drag (Ctrl/⌘ + LMB
7
+ * over the canvas in select mode). Renders an SVG outline whenever
8
+ * `rect` is non-null; the parent supplies / clears the prop in step
9
+ * with the mouse handler.
10
+ */
11
+
12
+ export interface RectSelectionRect {
13
+ x0: number;
14
+ y0: number;
15
+ x1: number;
16
+ y1: number;
17
+ }
18
+
19
+ export interface RectSelectionOverlayProps {
20
+ rect: RectSelectionRect | null;
21
+ }
22
+
23
+ export function RectSelectionOverlay({ rect }: RectSelectionOverlayProps) {
24
+ if (!rect) return null;
25
+ const left = Math.min(rect.x0, rect.x1);
26
+ const top = Math.min(rect.y0, rect.y1);
27
+ const width = Math.abs(rect.x1 - rect.x0);
28
+ const height = Math.abs(rect.y1 - rect.y0);
29
+ if (width < 1 || height < 1) return null;
30
+ return (
31
+ <svg
32
+ className="absolute inset-0 pointer-events-none"
33
+ style={{ width: '100%', height: '100%' }}
34
+ aria-hidden="true"
35
+ >
36
+ <rect
37
+ x={left}
38
+ y={top}
39
+ width={width}
40
+ height={height}
41
+ fill="rgba(20, 184, 166, 0.10)"
42
+ stroke="rgb(20, 184, 166)"
43
+ strokeWidth={1}
44
+ strokeDasharray="4 3"
45
+ />
46
+ </svg>
47
+ );
48
+ }
@@ -15,6 +15,7 @@ export function StatusBar() {
15
15
  const progress = useViewerStore((s) => s.progress);
16
16
  const error = useViewerStore((s) => s.error);
17
17
  const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
18
+ const activeStreamCanceller = useViewerStore((s) => s.activeStreamCanceller);
18
19
  const webgpu = useWebGPU();
19
20
 
20
21
  const [fps, setFps] = useState(60);
@@ -108,6 +109,19 @@ export function StatusBar() {
108
109
  ) : (
109
110
  <span>Ready</span>
110
111
  )}
112
+ {/* Cancel button — only visible while a long-running stream
113
+ (LAS/LAZ/PLY/PCD/E57) is in flight. The loader hooks
114
+ register/clear the canceller around `await ingest.done`. */}
115
+ {activeStreamCanceller && (
116
+ <button
117
+ type="button"
118
+ onClick={() => activeStreamCanceller()}
119
+ className="px-2 py-0.5 rounded border border-destructive/40 text-destructive text-[10px] uppercase tracking-wider hover:bg-destructive hover:text-destructive-foreground transition-colors"
120
+ title="Cancel the active point cloud stream"
121
+ >
122
+ Cancel
123
+ </button>
124
+ )}
111
125
  </div>
112
126
 
113
127
  {/* Center: Model Stats */}