@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
|
@@ -15,7 +15,16 @@ import { flushSync } from 'react-dom';
|
|
|
15
15
|
import { useShallow } from 'zustand/react/shallow';
|
|
16
16
|
import { getViewerStoreApi, useViewerStore } from '@/store';
|
|
17
17
|
import { IfcParser, detectFormat, type IfcDataStore } from '@ifc-lite/parser';
|
|
18
|
-
import {
|
|
18
|
+
import { WorkerParser } from '@ifc-lite/parser/browser';
|
|
19
|
+
import { memoryAccounting } from '../lib/perf/memoryAccounting.js';
|
|
20
|
+
import {
|
|
21
|
+
GeometryProcessor,
|
|
22
|
+
GeometryQuality,
|
|
23
|
+
getGeometryStreamWatchdogMs as getGeometryStreamWatchdogMsImpl,
|
|
24
|
+
type MeshData,
|
|
25
|
+
type CoordinateInfo,
|
|
26
|
+
} from '@ifc-lite/geometry';
|
|
27
|
+
import { acquireFileBuffer, type AcquiredBuffer } from '../utils/acquireFileBuffer.js';
|
|
19
28
|
import initIfcLiteWasm, { IfcAPI } from '@ifc-lite/wasm';
|
|
20
29
|
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
21
30
|
import { type GeometryData } from '@ifc-lite/cache';
|
|
@@ -95,14 +104,23 @@ function yieldToUiThread(): Promise<void> {
|
|
|
95
104
|
});
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Size-aware first-batch watchdog. Delegates to the package-level helper so
|
|
109
|
+
* the formula stays unit-tested in `@ifc-lite/geometry`. Subsequent-batch
|
|
110
|
+
* deadlines are unchanged from the previous fixed values; only the
|
|
111
|
+
* first-batch deadline grows with file size to give the WASM pre-pass time
|
|
112
|
+
* to finish on multi-GB files (issue #600).
|
|
113
|
+
*/
|
|
98
114
|
function getGeometryStreamWatchdogMs(
|
|
99
115
|
desktopStableWasm: boolean,
|
|
100
116
|
batchCount: number,
|
|
117
|
+
fileSizeMB: number = 0,
|
|
101
118
|
): number {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
return getGeometryStreamWatchdogMsImpl({
|
|
120
|
+
desktopStableWasm,
|
|
121
|
+
batchCount,
|
|
122
|
+
fileSizeMB,
|
|
123
|
+
});
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
function countNativeSpatialNodes(
|
|
@@ -211,6 +229,10 @@ export function useIfcLoader() {
|
|
|
211
229
|
resetViewerState();
|
|
212
230
|
clearAllModels();
|
|
213
231
|
|
|
232
|
+
// Reset memory accounting so per-load summaries don't accumulate across files.
|
|
233
|
+
memoryAccounting.reset();
|
|
234
|
+
memoryAccounting.recordPhase({ phase: 'load-start' });
|
|
235
|
+
|
|
214
236
|
setLoading(true);
|
|
215
237
|
setGeometryStreamingActive(false);
|
|
216
238
|
setError(null);
|
|
@@ -663,9 +685,14 @@ export function useIfcLoader() {
|
|
|
663
685
|
setIfcDataStore(null);
|
|
664
686
|
setProgress({ phase: 'Starting native geometry streaming', percent: 10 });
|
|
665
687
|
|
|
688
|
+
// Snapshot the user's "Merge Multilayer Walls" preference once
|
|
689
|
+
// at load time — flipping the toggle mid-stream cannot affect
|
|
690
|
+
// an in-flight WASM pipeline, the reload banner handles that.
|
|
691
|
+
const mergeLayersAtLoad = useViewerStore.getState().mergeLayers;
|
|
666
692
|
const geometryProcessor = new GeometryProcessor({
|
|
667
693
|
quality: GeometryQuality.Balanced,
|
|
668
694
|
preferNative: true,
|
|
695
|
+
mergeLayers: mergeLayersAtLoad,
|
|
669
696
|
});
|
|
670
697
|
|
|
671
698
|
let estimatedTotal = 0;
|
|
@@ -1555,13 +1582,33 @@ export function useIfcLoader() {
|
|
|
1555
1582
|
return;
|
|
1556
1583
|
}
|
|
1557
1584
|
|
|
1558
|
-
// Read file from disk
|
|
1585
|
+
// Read file from disk. The browser path streams files ≥
|
|
1586
|
+
// STREAM_SAB_THRESHOLD directly into a SharedArrayBuffer, which avoids
|
|
1587
|
+
// a doubled-peak ArrayBuffer + SAB allocation when the geometry
|
|
1588
|
+
// pipeline copies into its own SAB. The native path still reads via
|
|
1589
|
+
// Tauri's Rust IPC because it bounds memory differently. (#600)
|
|
1559
1590
|
const fileReadStart = performance.now();
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1591
|
+
let acquired: AcquiredBuffer;
|
|
1592
|
+
if (isNativeFileHandle(file)) {
|
|
1593
|
+
const nativeBytes = await readNativeFile(file.path);
|
|
1594
|
+
const nativeBuffer = toExactArrayBuffer(nativeBytes);
|
|
1595
|
+
acquired = {
|
|
1596
|
+
buffer: nativeBuffer,
|
|
1597
|
+
view: new Uint8Array(nativeBuffer),
|
|
1598
|
+
isShared: false,
|
|
1599
|
+
};
|
|
1600
|
+
} else {
|
|
1601
|
+
acquired = await acquireFileBuffer(file as File);
|
|
1602
|
+
}
|
|
1603
|
+
// `buffer` retains its previous semantics (ArrayBuffer-shaped) for
|
|
1604
|
+
// every downstream consumer. When `acquired.isShared` is true the
|
|
1605
|
+
// backing store is a SharedArrayBuffer; downstream code only ever
|
|
1606
|
+
// reads bytes via `new Uint8Array(buffer)` / `new DataView(buffer)`,
|
|
1607
|
+
// both of which work on either backing store. The TS cast is purely
|
|
1608
|
+
// type-system: the runtime is identical.
|
|
1609
|
+
const buffer = acquired.buffer as ArrayBuffer;
|
|
1563
1610
|
const fileReadMs = performance.now() - fileReadStart;
|
|
1564
|
-
console.log(`[useIfc] File: ${file.name}, size: ${fileSizeMB.toFixed(2)}MB, read in ${fileReadMs.toFixed(0)}ms`);
|
|
1611
|
+
console.log(`[useIfc] File: ${file.name}, size: ${fileSizeMB.toFixed(2)}MB, read in ${fileReadMs.toFixed(0)}ms${acquired.isShared ? ' (streamed→SAB)' : ''}`);
|
|
1565
1612
|
|
|
1566
1613
|
// Detect file format (IFCX/IFC5 vs IFC4 STEP vs GLB vs LAS/LAZ)
|
|
1567
1614
|
const pointCloudFormat = detectPointCloudFormat(file.name, buffer);
|
|
@@ -1569,7 +1616,7 @@ export function useIfcLoader() {
|
|
|
1569
1616
|
|
|
1570
1617
|
// LAS / LAZ point clouds: stream chunks straight to the renderer.
|
|
1571
1618
|
// No on-disk cache, no server upload — the data goes worker → GPU.
|
|
1572
|
-
if (format === 'las' || format === 'laz' || format === 'ply' || format === 'pcd' || format === 'e57') {
|
|
1619
|
+
if (format === 'las' || format === 'laz' || format === 'ply' || format === 'pcd' || format === 'e57' || format === 'pts' || format === 'xyz') {
|
|
1573
1620
|
const renderer = getGlobalRenderer();
|
|
1574
1621
|
if (!renderer) {
|
|
1575
1622
|
setError('Renderer not initialised — try again after the viewer mounts.');
|
|
@@ -1590,6 +1637,20 @@ export function useIfcLoader() {
|
|
|
1590
1637
|
onProgress: setProgress,
|
|
1591
1638
|
onAssetCountDelta: incCount,
|
|
1592
1639
|
});
|
|
1640
|
+
// Expose cancellation to the UI (StatusBar shows a Cancel
|
|
1641
|
+
// button while this is non-null). Cleared via the
|
|
1642
|
+
// `clearOwnedCanceller` helper below so a later load that
|
|
1643
|
+
// installed its own canceller never gets clobbered by our
|
|
1644
|
+
// cleanup paths — the helper only nulls the store when the
|
|
1645
|
+
// stored function is still ours.
|
|
1646
|
+
const { setActiveStreamCanceller } = useViewerStore.getState();
|
|
1647
|
+
const cancelStream = () => ingest.streamHandle.cancel();
|
|
1648
|
+
setActiveStreamCanceller(cancelStream);
|
|
1649
|
+
const clearOwnedCanceller = () => {
|
|
1650
|
+
if (useViewerStore.getState().activeStreamCanceller === cancelStream) {
|
|
1651
|
+
setActiveStreamCanceller(null);
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1593
1654
|
// ingestPointCloud's onError callback already runs renderer cleanup
|
|
1594
1655
|
// + incCount(-1); the outer catch must NOT repeat them or the
|
|
1595
1656
|
// pointCloudAssetCount will go negative.
|
|
@@ -1601,15 +1662,38 @@ export function useIfcLoader() {
|
|
|
1601
1662
|
// the spinner / model record now. Free the renderer handle
|
|
1602
1663
|
// so we don't leak the half-streamed asset.
|
|
1603
1664
|
if (loadSessionRef.current !== currentSession) {
|
|
1665
|
+
console.warn(
|
|
1666
|
+
`[useIfc] pointcloud ingest rejected on stale session (handle=${ingest.rendererHandle.id}):`,
|
|
1667
|
+
err,
|
|
1668
|
+
);
|
|
1604
1669
|
renderer.removePointCloudAsset(ingest.rendererHandle);
|
|
1670
|
+
clearOwnedCanceller();
|
|
1605
1671
|
return;
|
|
1606
1672
|
}
|
|
1607
1673
|
const message = err instanceof Error ? err.message : String(err);
|
|
1608
|
-
|
|
1609
|
-
|
|
1674
|
+
// Distinguish a user-initiated abort from a real failure so
|
|
1675
|
+
// the status bar shows "Cancelled" instead of a scary error.
|
|
1676
|
+
const isAbort = err instanceof DOMException && err.name === 'AbortError';
|
|
1677
|
+
if (isAbort) {
|
|
1678
|
+
console.log(
|
|
1679
|
+
`[useIfc] pointcloud ingest cancelled (model=${primaryModelId}, handle=${ingest.rendererHandle.id})`,
|
|
1680
|
+
);
|
|
1681
|
+
updateModel(primaryModelId, { loadState: 'error', loadError: 'cancelled' });
|
|
1682
|
+
setError(null);
|
|
1683
|
+
setProgress({ phase: 'Cancelled', percent: 0 });
|
|
1684
|
+
} else {
|
|
1685
|
+
console.error(
|
|
1686
|
+
`[useIfc] pointcloud ingest failed (format=${format}, model=${primaryModelId}):`,
|
|
1687
|
+
err,
|
|
1688
|
+
);
|
|
1689
|
+
updateModel(primaryModelId, { loadState: 'error', loadError: message });
|
|
1690
|
+
setError(`${format.toUpperCase()} parsing failed: ${message}`);
|
|
1691
|
+
}
|
|
1692
|
+
clearOwnedCanceller();
|
|
1610
1693
|
setLoading(false);
|
|
1611
1694
|
return;
|
|
1612
1695
|
}
|
|
1696
|
+
clearOwnedCanceller();
|
|
1613
1697
|
if (loadSessionRef.current !== currentSession) {
|
|
1614
1698
|
// A newer load already began. Drop our streamed asset and
|
|
1615
1699
|
// skip every store/UI mutation so we don't overwrite the
|
|
@@ -1737,16 +1821,46 @@ export function useIfcLoader() {
|
|
|
1737
1821
|
&& file.size < HUGE_NATIVE_FILE_THRESHOLD;
|
|
1738
1822
|
|
|
1739
1823
|
// Initialize geometry processor first (WASM init is fast if already loaded)
|
|
1824
|
+
const mergeLayersAtLoad = useViewerStore.getState().mergeLayers;
|
|
1740
1825
|
const geometryProcessor = new GeometryProcessor({
|
|
1741
1826
|
quality: GeometryQuality.Balanced,
|
|
1742
1827
|
preferNative: false,
|
|
1828
|
+
// Issue #540: snapshot at load time so the WASM bridge applies
|
|
1829
|
+
// the flag before the first parseMeshes* call.
|
|
1830
|
+
mergeLayers: mergeLayersAtLoad,
|
|
1743
1831
|
});
|
|
1744
1832
|
await geometryProcessor.init();
|
|
1745
1833
|
|
|
1834
|
+
// Allocate (or reuse) a SharedArrayBuffer so the parser worker and
|
|
1835
|
+
// the geometry workers read the same memory zero-copy. When
|
|
1836
|
+
// `acquireFileBuffer` already streamed the file directly into a SAB
|
|
1837
|
+
// (large-file entry path, issue #600), reuse it — no second copy.
|
|
1838
|
+
// `WorkerParser.isSupported()` rolls together: COI enabled, SAB
|
|
1839
|
+
// available, AND TextDecoder accepts SAB-backed views (Firefox fails
|
|
1840
|
+
// the third check; we skip the worker path entirely there so the
|
|
1841
|
+
// SAB allocation isn't wasted).
|
|
1842
|
+
const useParserWorker = WorkerParser.isSupported() && !isNativeFileHandle(file);
|
|
1843
|
+
let sharedSource: SharedArrayBuffer | null = null;
|
|
1844
|
+
if (useParserWorker) {
|
|
1845
|
+
if (acquired.isShared && acquired.buffer instanceof SharedArrayBuffer) {
|
|
1846
|
+
// acquireFileBuffer already streamed bytes into a SAB. Reuse it.
|
|
1847
|
+
sharedSource = acquired.buffer;
|
|
1848
|
+
} else {
|
|
1849
|
+
// Smaller files (or non-COI) took the `await file.arrayBuffer()`
|
|
1850
|
+
// branch — make a SAB copy so the parser worker can read it.
|
|
1851
|
+
sharedSource = new SharedArrayBuffer(buffer.byteLength);
|
|
1852
|
+
new Uint8Array(sharedSource).set(new Uint8Array(buffer));
|
|
1853
|
+
}
|
|
1854
|
+
memoryAccounting.setSourceBytes(buffer.byteLength);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1746
1857
|
// Data model parsing runs IN PARALLEL with geometry streaming.
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1858
|
+
// Default path: parser runs in a Web Worker via WorkerParser, both
|
|
1859
|
+
// workers + main share the same SharedArrayBuffer source, and the
|
|
1860
|
+
// main thread never blocks on parse.
|
|
1861
|
+
// Fallback: in-process IfcParser.parseColumnar (the previous default)
|
|
1862
|
+
// — used when cross-origin isolation is missing or the worker spawn
|
|
1863
|
+
// fails (auto-fallback inside the catch).
|
|
1750
1864
|
let resolveDataStore: (dataStore: IfcDataStore) => void;
|
|
1751
1865
|
let rejectDataStore: (err: unknown) => void;
|
|
1752
1866
|
const dataStorePromise = new Promise<IfcDataStore>((resolve, reject) => {
|
|
@@ -1754,59 +1868,119 @@ export function useIfcLoader() {
|
|
|
1754
1868
|
rejectDataStore = reject;
|
|
1755
1869
|
});
|
|
1756
1870
|
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1871
|
+
const onPartialDataStore = (partialStore: IfcDataStore) => {
|
|
1872
|
+
if (loadSessionRef.current !== currentSession) return;
|
|
1873
|
+
if (spatialReadyMs === null) {
|
|
1874
|
+
spatialReadyMs = performance.now() - totalStartTime;
|
|
1875
|
+
console.log(`[useIfc] Spatial tree ready for ${file.name} at ${spatialReadyMs.toFixed(0)}ms`);
|
|
1876
|
+
}
|
|
1877
|
+
if (partialStore.spatialHierarchy && partialStore.spatialHierarchy.storeyHeights.size === 0 && partialStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
1878
|
+
const calculatedHeights = calculateStoreyHeights(partialStore.spatialHierarchy.storeyElevations);
|
|
1879
|
+
for (const [storeyId, height] of calculatedHeights) {
|
|
1880
|
+
partialStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
setIfcDataStore(partialStore);
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
const onFullDataStore = (dataStore: IfcDataStore) => {
|
|
1887
|
+
if (loadSessionRef.current !== currentSession) return;
|
|
1888
|
+
metadataCompleteMs = performance.now() - totalStartTime;
|
|
1889
|
+
if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
1890
|
+
const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
|
|
1891
|
+
for (const [storeyId, height] of calculatedHeights) {
|
|
1892
|
+
dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
setIfcDataStore(dataStore);
|
|
1896
|
+
console.log(`[useIfc] Data model parsing complete for ${file.name}: ${metadataCompleteMs.toFixed(0)}ms`);
|
|
1897
|
+
memoryAccounting.endPhase('parser-worker');
|
|
1898
|
+
memoryAccounting.recordPhase({ phase: 'parser-complete' });
|
|
1899
|
+
resolveDataStore(dataStore);
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
const runMainThreadParser = async (): Promise<IfcDataStore> => {
|
|
1903
|
+
// Same `wasmApi` heuristic as before — desktop loads cannot share
|
|
1904
|
+
// the geometry processor's WASM instance with the parser without
|
|
1905
|
+
// risking corruption.
|
|
1765
1906
|
const parserWasmApi = isNativeFileHandle(file) ? undefined : geometryProcessor.getApi();
|
|
1766
|
-
|
|
1907
|
+
return new IfcParser().parseColumnar(buffer, {
|
|
1767
1908
|
wasmApi: parserWasmApi,
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1909
|
+
onSpatialReady: onPartialDataStore,
|
|
1910
|
+
});
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
// Hoisted so the geometry pre-pass's `onEntityIndex` callback can
|
|
1914
|
+
// hand the SAB triple to the same worker the parser is running in.
|
|
1915
|
+
// Receiving the index lets the parser worker skip its own ~10 s
|
|
1916
|
+
// `scanEntitiesFastBytes` call — the streaming pre-pass already
|
|
1917
|
+
// walked the file and built the same index.
|
|
1918
|
+
let workerParserInstance: WorkerParser | null = null;
|
|
1919
|
+
|
|
1920
|
+
// The geometry pre-pass only emits `entity-index` on the parallel
|
|
1921
|
+
// streaming path inside `processAdaptive`. Files smaller than the
|
|
1922
|
+
// sync threshold (2 MB) and the desktop-stable path don't fire it
|
|
1923
|
+
// — gate `waitForEntityIndex` so the parser doesn't hang.
|
|
1924
|
+
const ADAPTIVE_SYNC_THRESHOLD_MB = 2;
|
|
1925
|
+
const geometryWillEmitEntityIndex =
|
|
1926
|
+
useParserWorker
|
|
1927
|
+
&& !shouldUseDesktopStableWasmGeometry
|
|
1928
|
+
&& fileSizeMB >= ADAPTIVE_SYNC_THRESHOLD_MB;
|
|
1929
|
+
|
|
1930
|
+
const startDataModelParsing = () => {
|
|
1931
|
+
metadataStartMs = performance.now() - totalStartTime;
|
|
1932
|
+
console.log(`[useIfc] Data model parsing start for ${file.name}: ${metadataStartMs.toFixed(0)}ms (${useParserWorker ? 'worker' : 'main-thread'})`);
|
|
1933
|
+
memoryAccounting.beginPhase('parser-worker');
|
|
1934
|
+
memoryAccounting.recordPhase({ phase: 'parser-start' });
|
|
1935
|
+
|
|
1936
|
+
const workerAttempt = (): Promise<IfcDataStore> => {
|
|
1937
|
+
if (!useParserWorker || !sharedSource) {
|
|
1938
|
+
return Promise.reject(new Error('parser worker disabled (no SAB / native file)'));
|
|
1793
1939
|
}
|
|
1940
|
+
// NOTE: `deferPropertyAtomIndex` is not enabled here. The current
|
|
1941
|
+
// implementation in `columnar-parser.ts` calls
|
|
1942
|
+
// `entityRefs.filter(...)` to split property atoms out of the
|
|
1943
|
+
// primary index, which costs more on a 14 M-entity file (~3 s
|
|
1944
|
+
// for the filter pass) than the index-build time it saves.
|
|
1945
|
+
// Re-enable once the categorization loop builds the two
|
|
1946
|
+
// ref arrays inline so there is no second O(N) walk.
|
|
1947
|
+
const worker = new WorkerParser();
|
|
1948
|
+
workerParserInstance = worker;
|
|
1949
|
+
return worker.parseColumnar(sharedSource, {
|
|
1950
|
+
onSpatialReady: onPartialDataStore,
|
|
1951
|
+
// Hold the parser's WASM scan until the pre-pass hands over
|
|
1952
|
+
// the entity index — but only when we know the geometry
|
|
1953
|
+
// path will actually emit one (parallel-streaming branch).
|
|
1954
|
+
waitForEntityIndex: geometryWillEmitEntityIndex,
|
|
1955
|
+
onMemorySnapshot: (snapshot) => {
|
|
1956
|
+
if (snapshot.jsHeapBytes !== undefined) {
|
|
1957
|
+
memoryAccounting.recordWorkerMemory('parser', snapshot.jsHeapBytes);
|
|
1958
|
+
}
|
|
1959
|
+
memoryAccounting.recordPhase({
|
|
1960
|
+
phase: 'parser-transport',
|
|
1961
|
+
transportBytes: snapshot.transportBytes,
|
|
1962
|
+
});
|
|
1963
|
+
},
|
|
1964
|
+
});
|
|
1965
|
+
};
|
|
1794
1966
|
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1967
|
+
workerAttempt()
|
|
1968
|
+
.catch((err) => {
|
|
1969
|
+
console.warn('[useIfc] Parser worker failed, falling back to main-thread parse:', err);
|
|
1970
|
+
memoryAccounting.recordPhase({ phase: 'parser-worker-fallback' });
|
|
1971
|
+
return runMainThreadParser();
|
|
1972
|
+
})
|
|
1973
|
+
.then(onFullDataStore)
|
|
1974
|
+
.catch((err) => {
|
|
1975
|
+
metadataFailedMs = performance.now() - totalStartTime;
|
|
1976
|
+
console.error('[useIfc] Data model parsing failed:', err);
|
|
1977
|
+
console.log(`[useIfc] Data model parsing failed for ${file.name}: ${metadataFailedMs.toFixed(0)}ms`);
|
|
1978
|
+
memoryAccounting.recordPhase({ phase: 'parser-failed' });
|
|
1979
|
+
rejectDataStore(err);
|
|
1980
|
+
});
|
|
1805
1981
|
};
|
|
1806
1982
|
|
|
1807
1983
|
// Start data model parsing IMMEDIATELY — runs in parallel with geometry.
|
|
1808
|
-
// Entity scan uses Web Worker (off main thread), columnar parse yields
|
|
1809
|
-
// every ~4ms to maintain 60fps navigation during geometry streaming.
|
|
1810
1984
|
setTimeout(startDataModelParsing, 0);
|
|
1811
1985
|
|
|
1812
1986
|
// Use adaptive processing: sync for small files, streaming for large files
|
|
@@ -1854,11 +2028,47 @@ export function useIfcLoader() {
|
|
|
1854
2028
|
try {
|
|
1855
2029
|
// Use dynamic batch sizing for optimal throughput
|
|
1856
2030
|
const dynamicBatchConfig = getDynamicBatchConfig(fileSizeMB);
|
|
2031
|
+
memoryAccounting.beginPhase('geometry');
|
|
2032
|
+
// When the parser worker is in use, hand the geometry workers the
|
|
2033
|
+
// same SAB so we don't pay the file-bytes copy twice.
|
|
2034
|
+
const geometryView = sharedSource ? new Uint8Array(sharedSource) : new Uint8Array(buffer);
|
|
2035
|
+
// Phase 2 of single-controller-rayon-design.md — opt-in via
|
|
2036
|
+
// localStorage so we can A/B compare against the N-worker
|
|
2037
|
+
// baseline without rolling out for everyone. Users (and the
|
|
2038
|
+
// benchmark harness) flip this with:
|
|
2039
|
+
// localStorage.setItem('ifc-lite:single-controller', '1')
|
|
2040
|
+
// and reload. Set to anything else (or unset) for the legacy
|
|
2041
|
+
// N-worker path. Safe: if the threaded WASM bundle fails to
|
|
2042
|
+
// load (no COI, Safari, etc.) the controller worker falls back
|
|
2043
|
+
// to per-task serial execution within the controller itself
|
|
2044
|
+
// (par_iter without an initialized pool).
|
|
2045
|
+
const useSingleController = (() => {
|
|
2046
|
+
try {
|
|
2047
|
+
return typeof localStorage !== 'undefined'
|
|
2048
|
+
&& localStorage.getItem('ifc-lite:single-controller') === '1';
|
|
2049
|
+
} catch {
|
|
2050
|
+
return false;
|
|
2051
|
+
}
|
|
2052
|
+
})();
|
|
2053
|
+
if (useSingleController) {
|
|
2054
|
+
console.log('[useIfc] single-controller path enabled (Phase 2)');
|
|
2055
|
+
}
|
|
1857
2056
|
const geometryEvents = shouldUseDesktopStableWasmGeometry
|
|
1858
|
-
? geometryProcessor.processStreaming(
|
|
1859
|
-
: geometryProcessor.processAdaptive(
|
|
2057
|
+
? geometryProcessor.processStreaming(geometryView, undefined, dynamicBatchConfig)
|
|
2058
|
+
: geometryProcessor.processAdaptive(geometryView, {
|
|
1860
2059
|
sizeThreshold: 2 * 1024 * 1024, // 2MB threshold
|
|
1861
2060
|
batchSize: dynamicBatchConfig, // Dynamic batches: small first, then large
|
|
2061
|
+
existingSab: sharedSource ?? undefined,
|
|
2062
|
+
useSingleController,
|
|
2063
|
+
// Hand the streaming pre-pass's entity index to the parser
|
|
2064
|
+
// worker so it skips a duplicate ~10 s WASM scan. Safe even
|
|
2065
|
+
// when the parser falls back to main-thread (instance is
|
|
2066
|
+
// null then; the callback no-ops).
|
|
2067
|
+
onEntityIndex: (ids, starts, lengths) => {
|
|
2068
|
+
if (workerParserInstance) {
|
|
2069
|
+
workerParserInstance.setEntityIndex(ids, starts, lengths);
|
|
2070
|
+
}
|
|
2071
|
+
},
|
|
1862
2072
|
});
|
|
1863
2073
|
const geometryIterator = geometryEvents[Symbol.asyncIterator]();
|
|
1864
2074
|
let geometryIteratorClosed = false;
|
|
@@ -1878,6 +2088,7 @@ export function useIfcLoader() {
|
|
|
1878
2088
|
const watchdogMs = getGeometryStreamWatchdogMs(
|
|
1879
2089
|
shouldUseDesktopStableWasmGeometry,
|
|
1880
2090
|
batchCount,
|
|
2091
|
+
fileSizeMB,
|
|
1881
2092
|
);
|
|
1882
2093
|
let watchdogId: ReturnType<typeof globalThis.setTimeout> | null = null;
|
|
1883
2094
|
const nextResult = await Promise.race([
|
|
@@ -1910,6 +2121,11 @@ export function useIfcLoader() {
|
|
|
1910
2121
|
case 'model-open':
|
|
1911
2122
|
setProgress({ phase: 'Processing geometry', percent: 50 });
|
|
1912
2123
|
break;
|
|
2124
|
+
case 'progress':
|
|
2125
|
+
// Liveness heartbeat from the parallel pipeline. Receiving
|
|
2126
|
+
// any event resets the watchdog implicitly because the next
|
|
2127
|
+
// loop iteration re-creates the timer; nothing to do here.
|
|
2128
|
+
break;
|
|
1913
2129
|
case 'colorUpdate': {
|
|
1914
2130
|
// Accumulate color updates locally during streaming.
|
|
1915
2131
|
// We apply them in a single pass at 'complete' instead of
|
|
@@ -1930,6 +2146,12 @@ export function useIfcLoader() {
|
|
|
1930
2146
|
}
|
|
1931
2147
|
break;
|
|
1932
2148
|
}
|
|
2149
|
+
case 'workerMemory': {
|
|
2150
|
+
// Aggregated by memoryAccounting for per-load summaries.
|
|
2151
|
+
memoryAccounting.recordWorkerMemory(`geom-${event.workerIndex}`, event.wasmHeapBytes);
|
|
2152
|
+
memoryAccounting.addGeometryBytes(event.meshBytes);
|
|
2153
|
+
break;
|
|
2154
|
+
}
|
|
1933
2155
|
case 'batch': {
|
|
1934
2156
|
batchCount++;
|
|
1935
2157
|
|
|
@@ -2004,6 +2226,9 @@ export function useIfcLoader() {
|
|
|
2004
2226
|
updateCoordinateInfo(finalCoordinateInfo);
|
|
2005
2227
|
|
|
2006
2228
|
setProgress({ phase: 'Complete', percent: 100 });
|
|
2229
|
+
memoryAccounting.endPhase('geometry');
|
|
2230
|
+
memoryAccounting.recordPhase({ phase: 'geometry-complete' });
|
|
2231
|
+
console.log(memoryAccounting.formatSummary());
|
|
2007
2232
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
2008
2233
|
if (loadSessionRef.current === currentSession) {
|
|
2009
2234
|
setGeometryStreamingActive(false);
|
|
@@ -50,10 +50,20 @@ export function useVisibilityState() {
|
|
|
50
50
|
export function useToolState() {
|
|
51
51
|
const activeTool = useViewerStore((state) => state.activeTool);
|
|
52
52
|
const sectionPlane = useViewerStore((state) => state.sectionPlane);
|
|
53
|
+
const sectionPickMode = useViewerStore((state) => state.sectionPickMode);
|
|
54
|
+
const setSectionPlaneFromFace = useViewerStore((state) => state.setSectionPlaneFromFace);
|
|
55
|
+
const setSectionPickMode = useViewerStore((state) => state.setSectionPickMode);
|
|
56
|
+
const setSectionPickPreview = useViewerStore((state) => state.setSectionPickPreview);
|
|
57
|
+
const setSectionCustomDistance = useViewerStore((state) => state.setSectionCustomDistance);
|
|
53
58
|
|
|
54
59
|
return {
|
|
55
60
|
activeTool,
|
|
56
61
|
sectionPlane,
|
|
62
|
+
sectionPickMode,
|
|
63
|
+
setSectionPlaneFromFace,
|
|
64
|
+
setSectionPickMode,
|
|
65
|
+
setSectionPickPreview,
|
|
66
|
+
setSectionCustomDistance,
|
|
57
67
|
};
|
|
58
68
|
}
|
|
59
69
|
|