@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.
- package/.turbo/turbo-build.log +59 -43
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +496 -0
- package/dist/assets/basketViewActivator-Bzw51jhm.js +71 -0
- package/dist/assets/{bcf-DOG9_WPX.js → bcf-4K724hw0.js} +18 -18
- package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
- package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
- package/dist/assets/exporters-u0sz2Upj.js +259119 -0
- package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
- package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
- package/dist/assets/ids-B7AXEv7h.js +4067 -0
- package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
- package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
- package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
- package/dist/assets/index-CSWgTe1s.css +1 -0
- package/dist/assets/{index-BOi3BuUI.js → index-DVNSvEMh.js} +49877 -28410
- package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
- package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
- package/dist/assets/{native-bridge-CpBeOPQa.js → native-bridge-BiD01jI9.js} +2 -2
- package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
- package/dist/assets/{sandbox-Baez7n-t.js → sandbox-DPD1ROr0.js} +548 -530
- package/dist/assets/{server-client-BB6cMAXE.js → server-client-DP8fMPY9.js} +1 -1
- package/dist/assets/three-CDRZThFA.js +4057 -0
- package/dist/assets/{wasm-bridge-CAYCUHbE.js → wasm-bridge-CErti6zX.js} +1 -1
- package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
- package/dist/index.html +10 -9
- package/dist/samples/building-architecture.ifc +453 -0
- package/dist/samples/hello-wall.ifc +1054 -0
- package/dist/samples/infra-bridge.ifc +962 -0
- package/index.html +1 -1
- package/package.json +15 -10
- package/public/samples/building-architecture.ifc +453 -0
- package/public/samples/hello-wall.ifc +1054 -0
- package/public/samples/infra-bridge.ifc +962 -0
- package/src/App.tsx +37 -3
- package/src/components/mcp/HeroScene.tsx +876 -0
- package/src/components/mcp/McpLanding.tsx +1318 -0
- package/src/components/mcp/McpPlayground.tsx +524 -0
- package/src/components/mcp/PlaygroundChat.tsx +1097 -0
- package/src/components/mcp/PlaygroundViewer.tsx +815 -0
- package/src/components/mcp/README.md +171 -0
- package/src/components/mcp/data.ts +659 -0
- package/src/components/mcp/playground-dispatcher.ts +1649 -0
- package/src/components/mcp/playground-files.ts +107 -0
- package/src/components/mcp/playground-uploads.ts +122 -0
- package/src/components/mcp/types.ts +65 -0
- package/src/components/mcp/use-mcp-page.ts +109 -0
- package/src/components/viewer/BasketPresentationDock.tsx +3 -0
- package/src/components/viewer/CesiumOverlay.tsx +165 -120
- package/src/components/viewer/DeviationPanel.tsx +172 -0
- package/src/components/viewer/HierarchyPanel.tsx +29 -3
- package/src/components/viewer/HoverTooltip.tsx +5 -0
- package/src/components/viewer/IDSAuditSummary.tsx +389 -0
- package/src/components/viewer/IDSPanel.tsx +80 -26
- package/src/components/viewer/MainToolbar.tsx +79 -7
- package/src/components/viewer/MergeLayersBanner.tsx +108 -0
- package/src/components/viewer/MobileToolbar.tsx +326 -0
- package/src/components/viewer/PointCloudClasses.tsx +111 -0
- package/src/components/viewer/PointCloudLegend.tsx +119 -0
- package/src/components/viewer/PointCloudPanel.tsx +52 -1
- package/src/components/viewer/PropertiesPanel.tsx +37 -6
- package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
- package/src/components/viewer/StatusBar.tsx +14 -0
- package/src/components/viewer/ViewerLayout.tsx +288 -95
- package/src/components/viewer/Viewport.tsx +86 -18
- package/src/components/viewer/ViewportContainer.tsx +60 -15
- package/src/components/viewer/ViewportOverlays.tsx +41 -26
- package/src/components/viewer/mouseHandlerTypes.ts +22 -0
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
- package/src/components/viewer/properties/MaterialCard.tsx +2 -2
- package/src/components/viewer/selectionHandlers.ts +41 -0
- package/src/components/viewer/tools/SectionPanel.tsx +181 -24
- package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
- package/src/components/viewer/useAnimationLoop.ts +22 -0
- package/src/components/viewer/useMouseControls.ts +296 -3
- package/src/components/viewer/usePointCloudSync.ts +8 -1
- package/src/components/viewer/useRenderUpdates.ts +21 -1
- package/src/components/viewer/useTouchControls.ts +100 -41
- package/src/generated/mcp-catalog.json +82 -0
- package/src/hooks/federationLoadGate.test.ts +90 -0
- package/src/hooks/federationLoadGate.ts +127 -0
- package/src/hooks/ids/idsDataAccessor.ts +11 -259
- package/src/hooks/ingest/pointCloudIngest.ts +127 -16
- package/src/hooks/useDrawingGeneration.ts +81 -8
- package/src/hooks/useIDS.ts +90 -10
- package/src/hooks/useIfcFederation.ts +94 -16
- package/src/hooks/useIfcLoader.ts +289 -64
- package/src/hooks/useViewerSelectors.ts +10 -0
- package/src/lib/geo/cesium-bridge.ts +84 -67
- package/src/lib/geo/clamp-anchor.test.ts +80 -0
- package/src/lib/geo/clamp-anchor.ts +57 -0
- package/src/lib/geo/effective-georef.test.ts +79 -1
- package/src/lib/geo/effective-georef.ts +83 -0
- package/src/lib/geo/reproject.ts +26 -13
- package/src/lib/geo/terrain-elevation.ts +166 -0
- package/src/lib/lens/adapter.ts +1 -1
- package/src/lib/llm/context-builder.ts +1 -1
- package/src/lib/perf/memoryAccounting.test.ts +92 -0
- package/src/lib/perf/memoryAccounting.ts +235 -0
- package/src/sdk/adapters/mutation-view.ts +1 -1
- package/src/store/constants.ts +39 -2
- package/src/store/index.ts +6 -1
- package/src/store/slices/cesiumSlice.ts +1 -1
- package/src/store/slices/idsSlice.ts +24 -0
- package/src/store/slices/loadingSlice.ts +12 -0
- package/src/store/slices/pointCloudSlice.ts +72 -1
- package/src/store/slices/sectionSlice.test.ts +590 -1
- package/src/store/slices/sectionSlice.ts +344 -17
- package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
- package/src/store/slices/uiSlice.ts +60 -2
- package/src/store/types.ts +42 -0
- package/src/store.ts +13 -0
- package/src/utils/acquireFileBuffer.test.ts +231 -0
- package/src/utils/acquireFileBuffer.ts +128 -0
- package/src/utils/ifcConfig.ts +24 -0
- package/src/utils/nativeSpatialDataStore.ts +20 -2
- package/src/utils/spatialHierarchy.test.ts +116 -0
- package/src/utils/spatialHierarchy.ts +23 -0
- package/tailwind.config.js +5 -0
- package/tsconfig.json +1 -0
- package/vite.config.ts +12 -0
- package/dist/assets/basketViewActivator-RZy5c3Td.js +0 -1
- package/dist/assets/decode-worker-Collf_X_.js +0 -1320
- package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
- package/dist/assets/exporters-BraHBeoi.js +0 -81583
- package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
- package/dist/assets/ids-DQ5jY0E8.js +0 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- 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
|
-
<
|
|
1112
|
-
|
|
1113
|
-
|
|
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 */}
|