@ifc-lite/viewer 1.18.0 → 1.19.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 +16 -14
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +436 -0
- package/dist/assets/{basketViewActivator-Cm1QEk_R.js → basketViewActivator-RZy5c3Td.js} +1 -1
- package/dist/assets/decode-worker-Collf_X_.js +1320 -0
- package/dist/assets/{exporters-B_OBqIyD.js → exporters-BraHBeoi.js} +2540 -1958
- package/dist/assets/{geometry.worker-xHHy-9DV.js → geometry.worker-DQEZB2rB.js} +1 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +1 -0
- package/dist/assets/{index-BKq-M3Mk.js → index-BOi3BuUI.js} +25546 -23508
- package/dist/assets/index-XwKzDuw6.js +22 -0
- package/dist/assets/{native-bridge-SHXiQwFW.js → native-bridge-CpBeOPQa.js} +1 -1
- package/dist/assets/{sandbox-jez21HtV.js → sandbox-Baez7n-t.js} +1366 -1311
- package/dist/assets/{server-client-ncOQVNso.js → server-client-BB6cMAXE.js} +1 -1
- package/dist/assets/{wasm-bridge-DyfBSB8z.js → wasm-bridge-CAYCUHbE.js} +1 -1
- package/dist/index.html +5 -5
- package/package.json +7 -6
- package/src/components/viewer/MainToolbar.tsx +4 -2
- package/src/components/viewer/PointCloudPanel.tsx +174 -0
- package/src/components/viewer/Viewport.tsx +18 -1
- package/src/components/viewer/ViewportContainer.tsx +43 -5
- package/src/components/viewer/ViewportOverlays.tsx +13 -2
- package/src/components/viewer/tools/AddElementOverlay.tsx +43 -2
- package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
- package/src/components/viewer/usePointCloudSync.ts +98 -0
- package/src/hooks/ingest/pointCloudIngest.ts +391 -0
- package/src/hooks/ingest/viewerModelIngest.ts +32 -3
- package/src/hooks/useIfcFederation.ts +72 -3
- package/src/hooks/useIfcLoader.ts +67 -3
- package/src/services/file-dialog.ts +4 -2
- package/src/store/index.ts +10 -1
- package/src/store/slices/pointCloudSlice.ts +102 -0
- package/src/store/types.ts +7 -0
- package/vite.config.ts +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/index-COnQRuqY.css +0 -1
|
@@ -46,6 +46,8 @@ import { useIfcCache, getCached } from './useIfcCache.js';
|
|
|
46
46
|
import { useIfcServer } from './useIfcServer.js';
|
|
47
47
|
|
|
48
48
|
import { getMaxExpressId, parseGlbViewerModel, parseIfcxViewerModel } from './ingest/viewerModelIngest.js';
|
|
49
|
+
import { detectPointCloudFormat, ingestPointCloud } from './ingest/pointCloudIngest.js';
|
|
50
|
+
import { getGlobalRenderer } from './useBCF.js';
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* Compute a fast content fingerprint from the first and last 4KB of a buffer.
|
|
@@ -253,7 +255,7 @@ export function useIfcLoader() {
|
|
|
253
255
|
dataStore: IfcDataStore | null,
|
|
254
256
|
geometryResult: { meshes: MeshData[]; totalVertices: number; totalTriangles: number; coordinateInfo: CoordinateInfo } | null,
|
|
255
257
|
schemaVersion: 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5',
|
|
256
|
-
patch?: { loadState?: 'pending' | 'streaming-geometry' | 'hydrating-metadata' | 'complete' | 'error'; cacheState?: 'none' | 'hit' | 'miss' | 'writing'; loadError?: string | null },
|
|
258
|
+
patch?: { loadState?: 'pending' | 'streaming-geometry' | 'hydrating-metadata' | 'complete' | 'error'; cacheState?: 'none' | 'hit' | 'miss' | 'writing'; loadError?: string | null; pointCloudHandleId?: number },
|
|
257
259
|
) => {
|
|
258
260
|
let idOffset = 0;
|
|
259
261
|
let maxExpressId = 0;
|
|
@@ -271,6 +273,7 @@ export function useIfcLoader() {
|
|
|
271
273
|
loadState: patch?.loadState ?? 'complete',
|
|
272
274
|
cacheState: patch?.cacheState ?? 'none',
|
|
273
275
|
loadError: patch?.loadError ?? null,
|
|
276
|
+
pointCloudHandleId: patch?.pointCloudHandleId,
|
|
274
277
|
});
|
|
275
278
|
};
|
|
276
279
|
const getSchemaVersion = (dataStore: IfcDataStore | null): 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5' => {
|
|
@@ -1560,8 +1563,69 @@ export function useIfcLoader() {
|
|
|
1560
1563
|
const fileReadMs = performance.now() - fileReadStart;
|
|
1561
1564
|
console.log(`[useIfc] File: ${file.name}, size: ${fileSizeMB.toFixed(2)}MB, read in ${fileReadMs.toFixed(0)}ms`);
|
|
1562
1565
|
|
|
1563
|
-
// Detect file format (IFCX/IFC5 vs IFC4 STEP vs GLB)
|
|
1564
|
-
const
|
|
1566
|
+
// Detect file format (IFCX/IFC5 vs IFC4 STEP vs GLB vs LAS/LAZ)
|
|
1567
|
+
const pointCloudFormat = detectPointCloudFormat(file.name, buffer);
|
|
1568
|
+
const format = pointCloudFormat ?? detectFormat(buffer);
|
|
1569
|
+
|
|
1570
|
+
// LAS / LAZ point clouds: stream chunks straight to the renderer.
|
|
1571
|
+
// No on-disk cache, no server upload — the data goes worker → GPU.
|
|
1572
|
+
if (format === 'las' || format === 'laz' || format === 'ply' || format === 'pcd' || format === 'e57') {
|
|
1573
|
+
const renderer = getGlobalRenderer();
|
|
1574
|
+
if (!renderer) {
|
|
1575
|
+
setError('Renderer not initialised — try again after the viewer mounts.');
|
|
1576
|
+
updateModel(primaryModelId, { loadState: 'error', loadError: 'renderer-missing' });
|
|
1577
|
+
setLoading(false);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
setProgress({ phase: `Streaming ${format.toUpperCase()}`, percent: 5 });
|
|
1581
|
+
setGeometryStreamingActive(false);
|
|
1582
|
+
const blob = isNativeFileHandle(file) ? new Blob([buffer]) : (file as File);
|
|
1583
|
+
const incCount = useViewerStore.getState().incrementPointCloudAssetCount;
|
|
1584
|
+
const ingest = ingestPointCloud({
|
|
1585
|
+
format,
|
|
1586
|
+
blob,
|
|
1587
|
+
fileName: file.name,
|
|
1588
|
+
buffer,
|
|
1589
|
+
renderer,
|
|
1590
|
+
onProgress: setProgress,
|
|
1591
|
+
onAssetCountDelta: incCount,
|
|
1592
|
+
});
|
|
1593
|
+
// ingestPointCloud's onError callback already runs renderer cleanup
|
|
1594
|
+
// + incCount(-1); the outer catch must NOT repeat them or the
|
|
1595
|
+
// pointCloudAssetCount will go negative.
|
|
1596
|
+
try {
|
|
1597
|
+
await ingest.done;
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
// Bail without touching store/UI state if a newer load
|
|
1600
|
+
// session has already started — the more recent flow owns
|
|
1601
|
+
// the spinner / model record now. Free the renderer handle
|
|
1602
|
+
// so we don't leak the half-streamed asset.
|
|
1603
|
+
if (loadSessionRef.current !== currentSession) {
|
|
1604
|
+
renderer.removePointCloudAsset(ingest.rendererHandle);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1608
|
+
updateModel(primaryModelId, { loadState: 'error', loadError: message });
|
|
1609
|
+
setError(`${format.toUpperCase()} parsing failed: ${message}`);
|
|
1610
|
+
setLoading(false);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (loadSessionRef.current !== currentSession) {
|
|
1614
|
+
// A newer load already began. Drop our streamed asset and
|
|
1615
|
+
// skip every store/UI mutation so we don't overwrite the
|
|
1616
|
+
// newer model's state.
|
|
1617
|
+
renderer.removePointCloudAsset(ingest.rendererHandle);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
setGeometryResult(ingest.geometryResult);
|
|
1621
|
+
setIfcDataStore(ingest.dataStore);
|
|
1622
|
+
finalizePrimaryModel(ingest.dataStore, ingest.geometryResult, ingest.schemaVersion, {
|
|
1623
|
+
pointCloudHandleId: ingest.rendererHandle.id,
|
|
1624
|
+
});
|
|
1625
|
+
setProgress({ phase: 'Complete', percent: 100 });
|
|
1626
|
+
setLoading(false);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1565
1629
|
|
|
1566
1630
|
// IFCX files must be parsed client-side (server only supports IFC4 STEP)
|
|
1567
1631
|
if (format === 'ifcx') {
|
|
@@ -80,9 +80,11 @@ export async function openIfcFileDialog(): Promise<NativeFileHandle | null> {
|
|
|
80
80
|
const selected = await dialog.open({
|
|
81
81
|
multiple: false,
|
|
82
82
|
directory: false,
|
|
83
|
-
title: 'Open IFC File',
|
|
83
|
+
title: 'Open IFC, Mesh or Point Cloud File',
|
|
84
84
|
filters: [
|
|
85
|
-
{ name: 'IFC Files', extensions: ['ifc', 'ifczip', 'ifcxml'] },
|
|
85
|
+
{ name: 'IFC Files', extensions: ['ifc', 'ifczip', 'ifcxml', 'ifcx'] },
|
|
86
|
+
{ name: 'Mesh Files', extensions: ['glb'] },
|
|
87
|
+
{ name: 'Point Clouds', extensions: ['las', 'laz', 'ply', 'pcd', 'e57'] },
|
|
86
88
|
{ name: 'All Files', extensions: ['*'] },
|
|
87
89
|
],
|
|
88
90
|
});
|
package/src/store/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { createOverlaySlice, type OverlaySlice } from './slices/overlaySlice.js'
|
|
|
40
40
|
import { createSearchSlice, type SearchSlice } from './slices/searchSlice.js';
|
|
41
41
|
import { createAnnotationsSlice, type AnnotationsSlice } from './slices/annotationsSlice.js';
|
|
42
42
|
import { createAddElementSlice, type AddElementSlice } from './slices/addElementSlice.js';
|
|
43
|
+
import { createPointCloudSlice, type PointCloudSlice, POINT_CLOUD_DEFAULTS } from './slices/pointCloudSlice.js';
|
|
43
44
|
import { invalidateVisibleBasketCache } from './basketVisibleSet.js';
|
|
44
45
|
|
|
45
46
|
// Import constants for reset function
|
|
@@ -132,7 +133,8 @@ export type ViewerState = LoadingSlice &
|
|
|
132
133
|
OverlaySlice &
|
|
133
134
|
SearchSlice &
|
|
134
135
|
AnnotationsSlice &
|
|
135
|
-
AddElementSlice &
|
|
136
|
+
AddElementSlice &
|
|
137
|
+
PointCloudSlice & {
|
|
136
138
|
resetViewerState: () => void;
|
|
137
139
|
};
|
|
138
140
|
|
|
@@ -169,6 +171,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
169
171
|
...createSearchSlice(...args),
|
|
170
172
|
...createAnnotationsSlice(...args),
|
|
171
173
|
...createAddElementSlice(...args),
|
|
174
|
+
...createPointCloudSlice(...args),
|
|
172
175
|
|
|
173
176
|
// Reset all viewer state when loading new file
|
|
174
177
|
// Note: Does NOT clear models - use clearAllModels() for that
|
|
@@ -415,6 +418,12 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
415
418
|
// pins themselves stay in localStorage (cross-file workspace).
|
|
416
419
|
draft: null,
|
|
417
420
|
selectedAnnotationId: null,
|
|
421
|
+
|
|
422
|
+
// Point cloud — clear runtime fields so a new file doesn't
|
|
423
|
+
// inherit the previous file's color mode / size / EDL state.
|
|
424
|
+
// Single-source-of-truth defaults shared with createPointCloudSlice.
|
|
425
|
+
...POINT_CLOUD_DEFAULTS,
|
|
426
|
+
pointCloudFixedColor: [...POINT_CLOUD_DEFAULTS.pointCloudFixedColor] as [number, number, number, number],
|
|
418
427
|
});
|
|
419
428
|
},
|
|
420
429
|
}));
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
* Point cloud rendering preferences.
|
|
7
|
+
*
|
|
8
|
+
* The renderer reads these via `usePointCloudSync`; UI components write
|
|
9
|
+
* them via the actions below. EDL is opt-in (default on) — costs ~5
|
|
10
|
+
* extra texture taps per pixel.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { StateCreator } from 'zustand';
|
|
14
|
+
|
|
15
|
+
export type PointColorModeUi = 'rgb' | 'classification' | 'intensity' | 'height' | 'fixed';
|
|
16
|
+
export type PointSizeModeUi = 'fixed-px' | 'adaptive-world' | 'attenuated';
|
|
17
|
+
|
|
18
|
+
export interface PointCloudSlice {
|
|
19
|
+
pointCloudColorMode: PointColorModeUi;
|
|
20
|
+
pointCloudFixedColor: [number, number, number, number];
|
|
21
|
+
/** Splat sizing strategy. Default: 'fixed-px' (sized by the px slider). */
|
|
22
|
+
pointCloudSizeMode: PointSizeModeUi;
|
|
23
|
+
/** Splat size in pixels (fixed/attenuated) or upper cap (attenuated). 1..20. */
|
|
24
|
+
pointCloudPointSize: number;
|
|
25
|
+
/** World-space splat radius in metres for adaptive/attenuated modes.
|
|
26
|
+
* Typical scans: 0.005–0.05. Default 0.02. */
|
|
27
|
+
pointCloudWorldRadius: number;
|
|
28
|
+
/** Render splats as discs vs squares. Default true. */
|
|
29
|
+
pointCloudRoundShape: boolean;
|
|
30
|
+
/** Enable Eye-Dome Lighting post-pass. Default true. */
|
|
31
|
+
pointCloudEdlEnabled: boolean;
|
|
32
|
+
/** EDL strength multiplier. 0..3, default 1. */
|
|
33
|
+
pointCloudEdlStrength: number;
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort count of point cloud assets currently uploaded to the
|
|
36
|
+
* renderer. Updated by ingest paths; UI uses it to show/hide the
|
|
37
|
+
* controls panel and the EDL post-pass.
|
|
38
|
+
*/
|
|
39
|
+
pointCloudAssetCount: number;
|
|
40
|
+
setPointCloudColorMode: (mode: PointColorModeUi) => void;
|
|
41
|
+
setPointCloudFixedColor: (rgba: [number, number, number, number]) => void;
|
|
42
|
+
setPointCloudSizeMode: (mode: PointSizeModeUi) => void;
|
|
43
|
+
setPointCloudPointSize: (px: number) => void;
|
|
44
|
+
setPointCloudWorldRadius: (m: number) => void;
|
|
45
|
+
setPointCloudRoundShape: (enabled: boolean) => void;
|
|
46
|
+
setPointCloudEdlEnabled: (enabled: boolean) => void;
|
|
47
|
+
setPointCloudEdlStrength: (strength: number) => void;
|
|
48
|
+
setPointCloudAssetCount: (count: number) => void;
|
|
49
|
+
incrementPointCloudAssetCount: (n?: number) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Single source of truth for the slice's runtime field defaults.
|
|
54
|
+
* Both the slice initializer and `resetViewerState` consume this so
|
|
55
|
+
* the two paths can't drift.
|
|
56
|
+
*/
|
|
57
|
+
export const POINT_CLOUD_DEFAULTS = {
|
|
58
|
+
// Fixed-px is the default so the size slider feels responsive on first
|
|
59
|
+
// contact. `attenuated` is nicer at extreme zooms but its "slider =
|
|
60
|
+
// upper cap" semantic confuses users at typical wide views because the
|
|
61
|
+
// projected world radius sits well below the cap.
|
|
62
|
+
pointCloudColorMode: 'rgb' as PointColorModeUi,
|
|
63
|
+
pointCloudFixedColor: [1, 1, 1, 1] as [number, number, number, number],
|
|
64
|
+
pointCloudSizeMode: 'fixed-px' as PointSizeModeUi,
|
|
65
|
+
pointCloudPointSize: 4,
|
|
66
|
+
pointCloudWorldRadius: 0.02,
|
|
67
|
+
pointCloudRoundShape: true,
|
|
68
|
+
pointCloudEdlEnabled: true,
|
|
69
|
+
pointCloudEdlStrength: 1,
|
|
70
|
+
pointCloudAssetCount: 0,
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
export const createPointCloudSlice: StateCreator<PointCloudSlice, [], [], PointCloudSlice> = (set) => ({
|
|
74
|
+
...POINT_CLOUD_DEFAULTS,
|
|
75
|
+
// Re-spread typed-array fields so consumers get fresh references
|
|
76
|
+
// instead of the readonly literal in POINT_CLOUD_DEFAULTS.
|
|
77
|
+
pointCloudFixedColor: [...POINT_CLOUD_DEFAULTS.pointCloudFixedColor] as [number, number, number, number],
|
|
78
|
+
setPointCloudColorMode: (mode) => set({ pointCloudColorMode: mode }),
|
|
79
|
+
setPointCloudFixedColor: (rgba) => set({ pointCloudFixedColor: rgba }),
|
|
80
|
+
setPointCloudSizeMode: (mode) => set({ pointCloudSizeMode: mode }),
|
|
81
|
+
// NaN/Infinity slip past Math.max+min unchanged ((NaN < x) === false),
|
|
82
|
+
// so guard with isFinite to keep invalid values out of GPU uniforms.
|
|
83
|
+
setPointCloudPointSize: (px) => set({
|
|
84
|
+
pointCloudPointSize: Number.isFinite(px) ? Math.max(1, Math.min(20, px)) : 4,
|
|
85
|
+
}),
|
|
86
|
+
setPointCloudWorldRadius: (m) => set({
|
|
87
|
+
pointCloudWorldRadius: Number.isFinite(m) ? Math.max(1e-4, m) : 0.02,
|
|
88
|
+
}),
|
|
89
|
+
setPointCloudRoundShape: (enabled) => set({ pointCloudRoundShape: enabled }),
|
|
90
|
+
setPointCloudEdlEnabled: (enabled) => set({ pointCloudEdlEnabled: enabled }),
|
|
91
|
+
setPointCloudEdlStrength: (strength) => set({
|
|
92
|
+
pointCloudEdlStrength: Number.isFinite(strength) ? Math.max(0, Math.min(3, strength)) : 1,
|
|
93
|
+
}),
|
|
94
|
+
setPointCloudAssetCount: (count) => set({
|
|
95
|
+
pointCloudAssetCount: Number.isFinite(count) ? Math.max(0, count) : 0,
|
|
96
|
+
}),
|
|
97
|
+
incrementPointCloudAssetCount: (n = 1) => set((s) => ({
|
|
98
|
+
pointCloudAssetCount: Number.isFinite(n)
|
|
99
|
+
? Math.max(0, s.pointCloudAssetCount + n)
|
|
100
|
+
: s.pointCloudAssetCount,
|
|
101
|
+
})),
|
|
102
|
+
});
|
package/src/store/types.ts
CHANGED
|
@@ -340,6 +340,13 @@ export interface FederatedModel {
|
|
|
340
340
|
cacheState?: 'none' | 'hit' | 'miss' | 'writing';
|
|
341
341
|
/** Optional load error for this model. */
|
|
342
342
|
loadError?: string | null;
|
|
343
|
+
/**
|
|
344
|
+
* Renderer handle for a streamed point cloud (LAS/LAZ) attached to
|
|
345
|
+
* this model. Stored as a plain number so the field stays JSON-safe.
|
|
346
|
+
* The viewport's removal effect calls `renderer.removePointCloudAsset`
|
|
347
|
+
* when the model is dropped from the store.
|
|
348
|
+
*/
|
|
349
|
+
pointCloudHandleId?: number;
|
|
343
350
|
}
|
|
344
351
|
|
|
345
352
|
/** Convert EntityRef to string for use as Map/Set key */
|
package/vite.config.ts
CHANGED
|
@@ -253,6 +253,7 @@ export default defineConfig({
|
|
|
253
253
|
'@ifc-lite/export': path.resolve(__dirname, '../../packages/export/src'),
|
|
254
254
|
'@ifc-lite/cache': path.resolve(__dirname, '../../packages/cache/src'),
|
|
255
255
|
'@ifc-lite/ifcx': path.resolve(__dirname, '../../packages/ifcx/src'),
|
|
256
|
+
'@ifc-lite/pointcloud': path.resolve(__dirname, '../../packages/pointcloud/src'),
|
|
256
257
|
'@ifc-lite/wasm': path.resolve(__dirname, '../../packages/wasm/pkg/ifc-lite.js'),
|
|
257
258
|
'@ifc-lite/sdk': path.resolve(__dirname, '../../packages/sdk/src'),
|
|
258
259
|
'@ifc-lite/create': path.resolve(__dirname, '../../packages/create/src'),
|
|
Binary file
|