@ifc-lite/viewer 1.27.0 → 1.28.1
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 +35 -42
- package/CHANGELOG.md +74 -0
- package/dist/assets/{basketViewActivator-B3CdrLsb.js → basketViewActivator-Ce38DhXd.js} +8 -8
- package/dist/assets/{bcf-QeHK_Aud.js → bcf-Cv_O3JfD.js} +56 -56
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/{deflate-B-d0SYQM.js → deflate-HbyMq59o.js} +1 -1
- package/dist/assets/drawing-2d-DW98umlt.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/{exporters-B4LbZFeT.js → exporters-BuD3XRzB.js} +1309 -1153
- package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
- package/dist/assets/{geotiff-CrVtDRFq.js → geotiff-B2HA8Bwm.js} +10 -10
- package/dist/assets/{ids-DjsGFN10.js → ids-DYUFMd5f.js} +952 -945
- package/dist/assets/{ifc-lite_bg-DsYUIHm3.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
- package/dist/assets/index-E9wB0zWt.css +1 -0
- package/dist/assets/{index-COYokSKc.js → index-n5O1QJMM.js} +37877 -38126
- package/dist/assets/{index.es-CY202jA3.js → index.es-BKVIpZgL.js} +9 -9
- package/dist/assets/{jpeg-D4wOkf5h.js → jpeg-C7hjKjPX.js} +1 -1
- package/dist/assets/{jspdf.es.min-DIGb9BHN.js → jspdf.es.min-oWlFc42Y.js} +4 -4
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-DmW0_tgf.js → lerc-BfIOGhQz.js} +1 -1
- package/dist/assets/{lzw-oWetY-d6.js → lzw-B0jRuuW5.js} +1 -1
- package/dist/assets/{native-bridge-BX8_tHXE.js → native-bridge-DpB-dtEn.js} +6 -3
- package/dist/assets/{packbits-F8Nkp4NY.js → packbits-DVvBTC39.js} +1 -1
- package/dist/assets/parser.worker-BDsWQ6rc.js +182 -0
- package/dist/assets/{pdf-Dsh3HPZB.js → pdf-dVIqI5ac.js} +10 -10
- package/dist/assets/raw-C0ZJYGmN.js +1 -0
- package/dist/assets/{sandbox-BAC3a-eN.js → sandbox-qpJlrNN0.js} +2962 -2554
- package/dist/assets/server-client-DVZ2huNS.js +719 -0
- package/dist/assets/{webimage-BLV1dgmd.js → webimage-B394g0Tw.js} +1 -1
- package/dist/assets/{xlsx-Bc2HTrjC.js → xlsx-D-oHO76J.js} +8 -8
- package/dist/assets/{zstd-C_1HxVrA.js → zstd-Bf38MwV2.js} +1 -1
- package/dist/index.html +9 -9
- package/package.json +24 -23
- package/src/App.tsx +1 -3
- package/src/components/mcp/playground-dispatcher.ts +3 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/viewer/BCFPanel.tsx +1 -16
- package/src/components/viewer/ChatPanel.tsx +11 -46
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +48 -183
- package/src/components/viewer/IDSPanel.tsx +1 -26
- package/src/components/viewer/MainToolbar.tsx +94 -187
- package/src/components/viewer/MobileToolbar.tsx +1 -9
- package/src/components/viewer/PropertiesPanel.tsx +98 -127
- package/src/components/viewer/ScriptPanel.tsx +8 -34
- package/src/components/viewer/Section2DPanel.tsx +32 -1
- package/src/components/viewer/ViewerLayout.tsx +5 -2
- package/src/components/viewer/Viewport.tsx +3 -0
- package/src/components/viewer/ViewportContainer.tsx +24 -42
- package/src/components/viewer/ViewportOverlays.tsx +1 -4
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/components/viewer/useGeometryStreaming.ts +0 -2
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +488 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +234 -14
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +100 -24
- package/src/hooks/useIfcFederation.ts +42 -811
- package/src/hooks/useIfcLoader.ts +349 -1517
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/result-export.ts +7 -1
- package/src/sdk/adapters/export-adapter.ts +6 -1
- package/src/services/cacheService.ts +9 -25
- package/src/services/desktop-export.ts +2 -59
- package/src/services/file-dialog.ts +8 -142
- package/src/store/constants.ts +23 -0
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +19 -6
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/visibilitySlice.ts +22 -1
- package/src/store/types.ts +1 -71
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/ifcConfig.ts +0 -12
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/vite.config.ts +6 -3
- package/DESKTOP_CONTRACT_VERSION +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/event-B0kAzHa-.js +0 -1
- package/dist/assets/geometry.worker-BdH-E6NB.js +0 -1
- package/dist/assets/index-ajK6D32J.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-D591Zu_-.js +0 -182
- package/dist/assets/raw-D9iw0tmc.js +0 -1
- package/dist/assets/server-client-Cjwnm7il.js +0 -706
- package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
- package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
- package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
- package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
- package/src/components/viewer/SettingsPage.tsx +0 -581
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
- package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
- package/src/lib/desktop-entitlement.ts +0 -43
- package/src/lib/desktop-product.ts +0 -130
- package/src/lib/platform.ts +0 -23
- package/src/services/desktop-cache.ts +0 -186
- package/src/services/desktop-harness.ts +0 -196
- package/src/services/desktop-logger.ts +0 -20
- package/src/services/desktop-native-metadata.ts +0 -230
- package/src/services/desktop-panel-actions.ts +0 -43
- package/src/services/desktop-preferences.ts +0 -44
- package/src/services/fs-cache.ts +0 -212
- package/src/services/tauri-core-stub.ts +0 -7
- package/src/services/tauri-dialog-stub.ts +0 -7
- package/src/services/tauri-fs-stub.ts +0 -7
- package/src/services/tauri-modules.d.ts +0 -50
- package/src/store/slices/desktopEntitlementSlice.ts +0 -86
- package/src/utils/desktopModelSnapshot.ts +0 -358
- package/src/utils/nativeSpatialDataStore.ts +0 -277
- package/src-tauri/Cargo.toml +0 -29
- package/src-tauri/build.rs +0 -7
- package/src-tauri/capabilities/default.json +0 -18
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +0 -21
- package/src-tauri/src/main.rs +0 -10
- package/src-tauri/tauri.conf.json +0 -39
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { useCallback, useRef } from 'react';
|
|
14
14
|
import { flushSync } from 'react-dom';
|
|
15
15
|
import { useShallow } from 'zustand/react/shallow';
|
|
16
|
-
import { getViewerStoreApi, useViewerStore } from '@/store';
|
|
16
|
+
import { getViewerStoreApi, useViewerStore, type FederatedModel } from '@/store';
|
|
17
17
|
import { IfcParser, detectFormat, type IfcDataStore } from '@ifc-lite/parser';
|
|
18
18
|
import { WorkerParser } from '@ifc-lite/parser/browser';
|
|
19
19
|
import { memoryAccounting } from '../lib/perf/memoryAccounting.js';
|
|
@@ -23,30 +23,20 @@ import {
|
|
|
23
23
|
getGeometryStreamWatchdogMs as getGeometryStreamWatchdogMsImpl,
|
|
24
24
|
type MeshData,
|
|
25
25
|
type CoordinateInfo,
|
|
26
|
+
type GeometryResult,
|
|
26
27
|
} from '@ifc-lite/geometry';
|
|
27
28
|
import { acquireFileBuffer, type AcquiredBuffer } from '../utils/acquireFileBuffer.js';
|
|
28
|
-
import
|
|
29
|
-
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
29
|
+
import { buildSpatialIndexGuarded, buildSpatialIndexForModel } from '../utils/loadingUtils.js';
|
|
30
30
|
import { type GeometryData } from '@ifc-lite/cache';
|
|
31
31
|
|
|
32
|
-
import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, CACHE_MAX_SOURCE_SIZE,
|
|
32
|
+
import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, CACHE_MAX_SOURCE_SIZE, getDynamicBatchConfig } from '../utils/ifcConfig.js';
|
|
33
33
|
import {
|
|
34
34
|
calculateMeshBounds,
|
|
35
35
|
createCoordinateInfo,
|
|
36
36
|
getRenderIntervalMs,
|
|
37
37
|
calculateStoreyHeights,
|
|
38
38
|
} from '../utils/localParsingUtils.js';
|
|
39
|
-
import { buildDesktopMetadataSnapshot, restoreDesktopMetadataSnapshot } from '../utils/desktopModelSnapshot.js';
|
|
40
|
-
import { buildIfcDataStoreFromNativeMetadata } from '../utils/nativeSpatialDataStore.js';
|
|
41
39
|
import { applyColorUpdatesToMeshes } from './meshColorUpdates.js';
|
|
42
|
-
import { readNativeFile, type NativeFileHandle } from '../services/file-dialog.js';
|
|
43
|
-
import {
|
|
44
|
-
bootstrapNativeMetadata,
|
|
45
|
-
persistNativeMetadataSnapshot,
|
|
46
|
-
restoreNativeMetadataSnapshot,
|
|
47
|
-
} from '../services/desktop-native-metadata.js';
|
|
48
|
-
import { finalizeActiveHarnessRun, getActiveHarnessRequest } from '../services/desktop-harness.js';
|
|
49
|
-
import { logToDesktopTerminal } from '../services/desktop-logger.js';
|
|
50
40
|
|
|
51
41
|
// Cache hook
|
|
52
42
|
import { useIfcCache, getCached } from './useIfcCache.js';
|
|
@@ -58,6 +48,35 @@ import { getMaxExpressId, parseGlbViewerModel, parseIfcxViewerModel } from './in
|
|
|
58
48
|
import { boundedIteratorReturn } from './ingest/streamCleanup.js';
|
|
59
49
|
import { detectPointCloudFormat, ingestPointCloud } from './ingest/pointCloudIngest.js';
|
|
60
50
|
import { getGlobalRenderer } from './useBCF.js';
|
|
51
|
+
import { extractModelGeoref, alignGeometryToReference, findReferenceGeorefModel } from './ingest/federationAlign.js';
|
|
52
|
+
import { toast } from '../components/ui/toast.js';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Where a {@link useIfcLoader.loadFile} call should land the model.
|
|
56
|
+
*
|
|
57
|
+
* `primary` is the historical single-model load: it resets all viewer state,
|
|
58
|
+
* clears the model map, and streams progressively into the active slot.
|
|
59
|
+
* `federated` is an additional model joining an existing federation — it does
|
|
60
|
+
* NOT reset state, carries the pre-allocated `modelId`, and the shared RTC
|
|
61
|
+
* origin picked by the federation gate. Both flow through the SAME geometry
|
|
62
|
+
* pipeline + the SAME `finalizeModel`, so load-time behaviour can never again
|
|
63
|
+
* diverge between the two (the cause of the model-diff "all geometry changed"
|
|
64
|
+
* bug). The georef anchor + the user's saved georef edits are resolved inside
|
|
65
|
+
* `finalizeModel` from the live store, exactly as the old federated path did.
|
|
66
|
+
* Default is `primary`.
|
|
67
|
+
*/
|
|
68
|
+
export type LoadTarget =
|
|
69
|
+
| { kind: 'primary' }
|
|
70
|
+
| {
|
|
71
|
+
kind: 'federated';
|
|
72
|
+
modelId: string;
|
|
73
|
+
name?: string;
|
|
74
|
+
visible?: boolean;
|
|
75
|
+
collapsed?: boolean;
|
|
76
|
+
loadedAt?: number;
|
|
77
|
+
/** Shared RTC offset from the earliest existing model (IFC Z-up). */
|
|
78
|
+
sharedRtcOffset?: { x: number; y: number; z: number };
|
|
79
|
+
};
|
|
61
80
|
|
|
62
81
|
/**
|
|
63
82
|
* Compute a fast content fingerprint from the first and last 4KB of a buffer.
|
|
@@ -86,25 +105,6 @@ function computeFastFingerprint(buffer: ArrayBuffer): string {
|
|
|
86
105
|
return (hash >>> 0).toString(16);
|
|
87
106
|
}
|
|
88
107
|
|
|
89
|
-
function toExactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
90
|
-
if (
|
|
91
|
-
bytes.buffer instanceof ArrayBuffer &&
|
|
92
|
-
bytes.byteOffset === 0 &&
|
|
93
|
-
bytes.byteLength === bytes.buffer.byteLength
|
|
94
|
-
) {
|
|
95
|
-
return bytes.buffer;
|
|
96
|
-
}
|
|
97
|
-
return bytes.slice().buffer;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function yieldToUiThread(): Promise<void> {
|
|
101
|
-
return new Promise<void>((resolve) => {
|
|
102
|
-
const channel = new MessageChannel();
|
|
103
|
-
channel.port1.onmessage = () => resolve();
|
|
104
|
-
channel.port2.postMessage(null);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
108
|
/**
|
|
109
109
|
* Size-aware first-batch watchdog. Delegates to the package-level helper so
|
|
110
110
|
* the formula stays unit-tested in `@ifc-lite/geometry`. Subsequent-batch
|
|
@@ -124,48 +124,6 @@ function getGeometryStreamWatchdogMs(
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
function countNativeSpatialNodes(
|
|
128
|
-
node: { children?: Array<{ children?: unknown[] }> } | null | undefined,
|
|
129
|
-
): number {
|
|
130
|
-
if (!node) return 0;
|
|
131
|
-
const children = Array.isArray(node.children) ? node.children : [];
|
|
132
|
-
let total = 1;
|
|
133
|
-
for (let i = 0; i < children.length; i += 1) {
|
|
134
|
-
total += countNativeSpatialNodes(children[i] as { children?: Array<{ children?: unknown[] }> });
|
|
135
|
-
}
|
|
136
|
-
return total;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function computeNativeCacheKey(file: NativeFileHandle): string {
|
|
140
|
-
const encodedPath = new TextEncoder().encode(file.path);
|
|
141
|
-
const pathHash = computeFastFingerprint(toExactArrayBuffer(encodedPath));
|
|
142
|
-
return `native-ifc-${file.size}-${file.modifiedMs ?? 0}-${pathHash}-v1`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
|
|
146
|
-
return typeof (file as NativeFileHandle).path === 'string';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let metadataScanApiPromise: Promise<IfcAPI> | null = null;
|
|
150
|
-
|
|
151
|
-
async function getMetadataScanApi(): Promise<IfcAPI> {
|
|
152
|
-
if (!metadataScanApiPromise) {
|
|
153
|
-
metadataScanApiPromise = (async () => {
|
|
154
|
-
await initIfcLiteWasm();
|
|
155
|
-
return new IfcAPI();
|
|
156
|
-
})();
|
|
157
|
-
}
|
|
158
|
-
return metadataScanApiPromise;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const ENABLE_HUGE_TIME_FLUSH = import.meta.env.VITE_IFC_ENABLE_HUGE_TIME_FLUSH === 'true';
|
|
162
|
-
|
|
163
|
-
async function* startDisabledNativeDesktopRendererModel(
|
|
164
|
-
_path: string,
|
|
165
|
-
_cacheKey?: string,
|
|
166
|
-
): AsyncGenerator<any, void, unknown> {
|
|
167
|
-
throw new Error('Native desktop renderer is disabled');
|
|
168
|
-
}
|
|
169
127
|
|
|
170
128
|
/**
|
|
171
129
|
* Hook providing file loading operations for single-model path
|
|
@@ -216,78 +174,180 @@ export function useIfcLoader() {
|
|
|
216
174
|
// Server operations from extracted hook
|
|
217
175
|
const { loadFromServer } = useIfcServer();
|
|
218
176
|
|
|
219
|
-
const loadFile = useCallback(async (
|
|
177
|
+
const loadFile = useCallback(async (
|
|
178
|
+
file: File,
|
|
179
|
+
target: LoadTarget = { kind: 'primary' },
|
|
180
|
+
) => {
|
|
220
181
|
const { resetViewerState, clearAllModels } = useViewerStore.getState();
|
|
221
|
-
|
|
222
|
-
|
|
182
|
+
// Only a primary (destructive, replace-everything) load bumps the session.
|
|
183
|
+
// Federated adds are independent and run concurrently — they capture the
|
|
184
|
+
// current session without invalidating each other; a subsequent primary
|
|
185
|
+
// load still bumps it and aborts any in-flight federated adds.
|
|
186
|
+
const currentSession = target.kind === 'primary'
|
|
187
|
+
? ++loadSessionRef.current
|
|
188
|
+
: loadSessionRef.current;
|
|
189
|
+
// Federated adds carry a pre-allocated id; primary loads mint a fresh one.
|
|
190
|
+
const modelId = target.kind === 'federated' ? target.modelId : crypto.randomUUID();
|
|
223
191
|
|
|
224
192
|
// Track total elapsed time for complete user experience
|
|
225
193
|
const totalStartTime = performance.now();
|
|
226
194
|
|
|
227
195
|
try {
|
|
228
|
-
// Reset all viewer state before loading new file
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
196
|
+
// Reset all viewer state before loading new file — PRIMARY ONLY. A
|
|
197
|
+
// federated add must never wipe model #1; it joins the existing map.
|
|
198
|
+
if (target.kind === 'primary') {
|
|
199
|
+
resetViewerState();
|
|
200
|
+
clearAllModels();
|
|
201
|
+
}
|
|
232
202
|
|
|
233
203
|
// Reset memory accounting so per-load summaries don't accumulate across files.
|
|
234
204
|
memoryAccounting.reset();
|
|
235
205
|
memoryAccounting.recordPhase({ phase: 'load-start' });
|
|
236
206
|
|
|
237
207
|
setLoading(true);
|
|
238
|
-
setGeometryStreamingActive(false);
|
|
239
208
|
setError(null);
|
|
240
|
-
setBoundedGeometryMode(false);
|
|
241
|
-
setGeometryProgress(null);
|
|
242
|
-
setMetadataProgress(null);
|
|
243
209
|
setProgress({ phase: 'Loading file', percent: 0 });
|
|
244
210
|
|
|
245
211
|
const fileName = file.name;
|
|
246
212
|
const fileSize = file.size;
|
|
247
213
|
const fileSizeMB = fileSize / (1024 * 1024);
|
|
248
214
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
215
|
+
// PRIMARY owns the active-model slots + top-level UI/memory flags and
|
|
216
|
+
// creates the model record. A federated add leaves all of that untouched
|
|
217
|
+
// (model #1 must not be disturbed) and registers atomically at finalize
|
|
218
|
+
// via addModel — so it creates NO placeholder entry here (which also
|
|
219
|
+
// keeps the `collapsed` default counting only the other models).
|
|
220
|
+
if (target.kind === 'primary') {
|
|
221
|
+
setGeometryStreamingActive(false);
|
|
222
|
+
setBoundedGeometryMode(false);
|
|
223
|
+
setGeometryProgress(null);
|
|
224
|
+
setMetadataProgress(null);
|
|
225
|
+
|
|
226
|
+
upsertModel({
|
|
227
|
+
id: modelId,
|
|
228
|
+
name: fileName,
|
|
229
|
+
ifcDataStore: null,
|
|
230
|
+
geometryResult: null,
|
|
231
|
+
visible: true,
|
|
232
|
+
collapsed: false,
|
|
233
|
+
schemaVersion: 'IFC4',
|
|
234
|
+
loadedAt: Date.now(),
|
|
235
|
+
fileSize,
|
|
236
|
+
sourceFile: file,
|
|
237
|
+
idOffset: 0,
|
|
238
|
+
maxExpressId: 0,
|
|
239
|
+
loadState: 'pending',
|
|
263
240
|
geometryLoadState: 'pending',
|
|
264
241
|
metadataLoadState: 'idle',
|
|
265
242
|
interactiveReady: false,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
243
|
+
cacheState: 'none',
|
|
244
|
+
loadError: null,
|
|
245
|
+
});
|
|
246
|
+
updateModel(modelId, {
|
|
247
|
+
loadState: 'streaming-geometry',
|
|
248
|
+
geometryLoadState: 'opening',
|
|
249
|
+
metadataLoadState: 'idle',
|
|
250
|
+
interactiveReady: false,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
276
253
|
|
|
277
|
-
|
|
254
|
+
// The ONE finalizer for every format/platform/role. Primary keeps the
|
|
255
|
+
// historical updateModel-only behaviour; federated runs the georef-align
|
|
256
|
+
// → id-offset → relabel → spatial-index → addModel sequence lifted
|
|
257
|
+
// verbatim from the old useIfcFederation.addModel block (same order).
|
|
258
|
+
const finalizeModel = async (
|
|
278
259
|
dataStore: IfcDataStore | null,
|
|
279
|
-
geometryResult:
|
|
260
|
+
geometryResult: GeometryResult | null,
|
|
280
261
|
schemaVersion: 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5',
|
|
281
262
|
patch?: { loadState?: 'pending' | 'streaming-geometry' | 'hydrating-metadata' | 'complete' | 'error'; cacheState?: 'none' | 'hit' | 'miss' | 'writing'; loadError?: string | null; pointCloudHandleId?: number },
|
|
282
|
-
) => {
|
|
263
|
+
): Promise<void> => {
|
|
264
|
+
if (target.kind === 'federated') {
|
|
265
|
+
if (!dataStore || !geometryResult) {
|
|
266
|
+
throw new Error('Federated model is missing its data store or geometry');
|
|
267
|
+
}
|
|
268
|
+
// Georef alignment against the federation anchor (resolved live from
|
|
269
|
+
// the store, exactly as the former addModel finalize did).
|
|
270
|
+
const referenceGeoref = findReferenceGeorefModel()?.georef ?? null;
|
|
271
|
+
const parsedGeorefMutations = useViewerStore.getState().georefMutations.get(modelId);
|
|
272
|
+
const parsedGeoref = extractModelGeoref(dataStore, geometryResult.coordinateInfo, parsedGeorefMutations);
|
|
273
|
+
let preAlignmentPositions: Float32Array[] | undefined;
|
|
274
|
+
let preAlignmentNormals: (Float32Array | undefined)[] | undefined;
|
|
275
|
+
let preAlignmentCoordinateInfo: CoordinateInfo | undefined;
|
|
276
|
+
let federationAlignmentStatus: FederatedModel['federationAlignmentStatus'] = 'none';
|
|
277
|
+
if (referenceGeoref && parsedGeoref) {
|
|
278
|
+
setProgress({ phase: 'Aligning georeferenced model', percent: 90 });
|
|
279
|
+
preAlignmentPositions = geometryResult.meshes.map((mesh) => new Float32Array(mesh.positions));
|
|
280
|
+
preAlignmentNormals = geometryResult.meshes.map((mesh) =>
|
|
281
|
+
mesh.normals && mesh.normals.length > 0 ? new Float32Array(mesh.normals) : undefined,
|
|
282
|
+
);
|
|
283
|
+
preAlignmentCoordinateInfo = geometryResult.coordinateInfo;
|
|
284
|
+
const status = await alignGeometryToReference(geometryResult, parsedGeoref, referenceGeoref);
|
|
285
|
+
federationAlignmentStatus = status;
|
|
286
|
+
if (status === 'reprojected') {
|
|
287
|
+
toast.info(
|
|
288
|
+
`Reprojected "${file.name}" from ${parsedGeoref.projectedCRS.name} `
|
|
289
|
+
+ `to ${referenceGeoref.projectedCRS.name} for federation alignment.`,
|
|
290
|
+
);
|
|
291
|
+
} else if (status === 'failed') {
|
|
292
|
+
toast.error(
|
|
293
|
+
`Could not align "${file.name}" with the federation anchor — `
|
|
294
|
+
+ `${parsedGeoref.projectedCRS.name} → ${referenceGeoref.projectedCRS.name} `
|
|
295
|
+
+ 'reprojection failed. The model is shown in its own local frame and may '
|
|
296
|
+
+ 'appear at the wrong real-world position.',
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
} else if (parsedGeoref) {
|
|
300
|
+
federationAlignmentStatus = 'anchor';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Federation registry: transform expressIds to globally-unique ids.
|
|
304
|
+
const maxExpressId = getMaxExpressId(dataStore, geometryResult.meshes);
|
|
305
|
+
const idOffset = registerModelOffset(modelId, maxExpressId);
|
|
306
|
+
if (idOffset > 0) {
|
|
307
|
+
for (const mesh of geometryResult.meshes) mesh.expressId = mesh.expressId + idOffset;
|
|
308
|
+
for (const asset of geometryResult.pointClouds ?? []) asset.expressId = asset.expressId + idOffset;
|
|
309
|
+
}
|
|
310
|
+
if (idOffset > 0 && patch?.pointCloudHandleId !== undefined) {
|
|
311
|
+
const renderer = getGlobalRenderer();
|
|
312
|
+
if (renderer && geometryResult.pointClouds && geometryResult.pointClouds.length > 0) {
|
|
313
|
+
renderer.relabelPointCloudAsset({ id: patch.pointCloudHandleId }, geometryResult.pointClouds[0].expressId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const federatedModel: FederatedModel = {
|
|
317
|
+
id: modelId,
|
|
318
|
+
name: target.name ?? file.name,
|
|
319
|
+
ifcDataStore: dataStore,
|
|
320
|
+
geometryResult,
|
|
321
|
+
visible: target.visible ?? true,
|
|
322
|
+
collapsed: target.collapsed ?? (useViewerStore.getState().models.size > 0),
|
|
323
|
+
schemaVersion,
|
|
324
|
+
loadedAt: target.loadedAt ?? Date.now(),
|
|
325
|
+
fileSize: buffer.byteLength,
|
|
326
|
+
sourceFile: file,
|
|
327
|
+
idOffset,
|
|
328
|
+
maxExpressId,
|
|
329
|
+
pointCloudHandleId: patch?.pointCloudHandleId,
|
|
330
|
+
preAlignmentPositions,
|
|
331
|
+
preAlignmentNormals,
|
|
332
|
+
preAlignmentCoordinateInfo,
|
|
333
|
+
federationAlignmentStatus,
|
|
334
|
+
};
|
|
335
|
+
useViewerStore.getState().addModel(federatedModel);
|
|
336
|
+
// Spatial index AFTER id offset + alignment (final ids + world positions)
|
|
337
|
+
// and AFTER addModel so it attaches to THIS model, not the active slot.
|
|
338
|
+
buildSpatialIndexForModel(geometryResult.meshes, modelId, dataStore);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// PRIMARY — unchanged from the former finalizePrimaryModel.
|
|
283
343
|
let idOffset = 0;
|
|
284
344
|
let maxExpressId = 0;
|
|
285
345
|
if (dataStore && geometryResult) {
|
|
286
346
|
maxExpressId = getMaxExpressId(dataStore, geometryResult.meshes);
|
|
287
|
-
idOffset = registerModelOffset(
|
|
347
|
+
idOffset = registerModelOffset(modelId, maxExpressId);
|
|
288
348
|
}
|
|
289
349
|
|
|
290
|
-
updateModel(
|
|
350
|
+
updateModel(modelId, {
|
|
291
351
|
ifcDataStore: dataStore,
|
|
292
352
|
geometryResult,
|
|
293
353
|
schemaVersion,
|
|
@@ -307,1300 +367,14 @@ export function useIfcLoader() {
|
|
|
307
367
|
return 'IFC2X3';
|
|
308
368
|
};
|
|
309
369
|
|
|
310
|
-
// Native renderer streaming path is currently disabled — the
|
|
311
|
-
// `huge native file` block further down handles real desktop
|
|
312
|
-
// streaming. This branch is retained as a scaffold for the future
|
|
313
|
-
// always-on native renderer integration.
|
|
314
|
-
const NATIVE_RENDERER_PATH_ENABLED = false as boolean;
|
|
315
|
-
if (
|
|
316
|
-
NATIVE_RENDERER_PATH_ENABLED &&
|
|
317
|
-
isNativeFileHandle(file) &&
|
|
318
|
-
fileName.toLowerCase().endsWith('.ifc')
|
|
319
|
-
) {
|
|
320
|
-
// Re-narrow `file` for the body — TS occasionally drops the
|
|
321
|
-
// type-predicate result inside a dead branch.
|
|
322
|
-
const nativeFile: NativeFileHandle = file;
|
|
323
|
-
const harnessRequest = getActiveHarnessRequest();
|
|
324
|
-
const nativeCacheKey = computeNativeCacheKey(nativeFile);
|
|
325
|
-
const shouldUseNativeCache = nativeFile.size >= CACHE_SIZE_THRESHOLD;
|
|
326
|
-
const hugeNativeMode = nativeFile.size >= HUGE_NATIVE_FILE_THRESHOLD;
|
|
327
|
-
let firstBatchWaitMs: number | null = null;
|
|
328
|
-
let firstVisibleGeometryMs: number | null = null;
|
|
329
|
-
let modelOpenMs: number | null = null;
|
|
330
|
-
let streamCompleteMs: number | null = null;
|
|
331
|
-
let batchCount = 0;
|
|
332
|
-
let totalMeshes = 0;
|
|
333
|
-
let spatialReadyMs: number | null = null;
|
|
334
|
-
let metadataStartMs: number | null = null;
|
|
335
|
-
let metadataReadCompleteMs: number | null = null;
|
|
336
|
-
let metadataParseStartMs: number | null = null;
|
|
337
|
-
let metadataCompleteMs: number | null = null;
|
|
338
|
-
let metadataFailedMs: number | null = null;
|
|
339
|
-
let metadataReadDurationMs: number | null = null;
|
|
340
|
-
let metadataBufferCopyDurationMs: number | null = null;
|
|
341
|
-
let metadataParseDurationMs: number | null = null;
|
|
342
|
-
let metadataSnapshotWritePromise: Promise<void> | null = null;
|
|
343
|
-
let metadataParsingPromise: Promise<void> | null = null;
|
|
344
|
-
let metadataParsingStarted = false;
|
|
345
|
-
let geometryCompleted = false;
|
|
346
|
-
let nativeGeometryCacheHit = false;
|
|
347
|
-
let nativeMetadataSnapshotHit = false;
|
|
348
|
-
let nativeMetadataSource: 'snapshot' | 'ifc-parse' = 'ifc-parse';
|
|
349
|
-
let nativeMetadataStartGate = 'immediate' as 'immediate' | 'afterInteractiveGeometry' | 'afterGeometryComplete';
|
|
350
|
-
let finalCoordinateInfo: CoordinateInfo | null = null;
|
|
351
|
-
|
|
352
|
-
console.log(`[useIfc] Native renderer load: ${fileName}, size: ${fileSizeMB.toFixed(2)}MB`);
|
|
353
|
-
void logToDesktopTerminal(
|
|
354
|
-
'info',
|
|
355
|
-
`[useIfc] Native renderer load start: ${fileName} (${fileSizeMB.toFixed(2)} MB) path=${file.path}`
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
setBoundedGeometryMode(true);
|
|
359
|
-
setGeometryResult(null);
|
|
360
|
-
setIfcDataStore(null);
|
|
361
|
-
setProgress({ phase: 'Starting native renderer', percent: 10 });
|
|
362
|
-
|
|
363
|
-
const queueNativeMetadataSnapshotWrite = (
|
|
364
|
-
dataStore: IfcDataStore,
|
|
365
|
-
sourceBuffer: ArrayBuffer,
|
|
366
|
-
) => {
|
|
367
|
-
metadataSnapshotWritePromise = (async () => {
|
|
368
|
-
await yieldToUiThread();
|
|
369
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
370
|
-
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
371
|
-
}
|
|
372
|
-
if (!shouldUseNativeCache) return;
|
|
373
|
-
try {
|
|
374
|
-
const { setNativeModelSnapshot } = await import('../services/desktop-cache.js');
|
|
375
|
-
const snapshotBuffer = await buildDesktopMetadataSnapshot(dataStore, sourceBuffer);
|
|
376
|
-
await setNativeModelSnapshot(nativeCacheKey, snapshotBuffer);
|
|
377
|
-
} catch (error) {
|
|
378
|
-
void logToDesktopTerminal(
|
|
379
|
-
'warn',
|
|
380
|
-
`[useIfc] Native metadata snapshot write failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
})();
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const finalizeNativeMetadata = (dataStore: IfcDataStore) => {
|
|
387
|
-
if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
388
|
-
const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
|
|
389
|
-
for (const [storeyId, height] of calculatedHeights) {
|
|
390
|
-
dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
setIfcDataStore(dataStore);
|
|
394
|
-
finalizePrimaryModel(
|
|
395
|
-
dataStore,
|
|
396
|
-
null,
|
|
397
|
-
getSchemaVersion(dataStore),
|
|
398
|
-
{
|
|
399
|
-
loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
|
|
400
|
-
cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
|
|
401
|
-
},
|
|
402
|
-
);
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
const startNativeMetadataParsing = (): Promise<void> | null => {
|
|
406
|
-
if (metadataParsingStarted) return metadataParsingPromise;
|
|
407
|
-
metadataParsingStarted = true;
|
|
408
|
-
metadataStartMs = performance.now() - totalStartTime;
|
|
409
|
-
updateModel(primaryModelId, { loadState: 'hydrating-metadata' });
|
|
410
|
-
void logToDesktopTerminal(
|
|
411
|
-
'info',
|
|
412
|
-
`[useIfc] Native metadata parse start for ${fileName} source=${nativeMetadataSource} gate=${nativeMetadataStartGate}`
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
metadataParsingPromise = (async () => {
|
|
416
|
-
const metadataReadStart = performance.now();
|
|
417
|
-
let parseStart = 0;
|
|
418
|
-
|
|
419
|
-
if (nativeMetadataSnapshotHit) {
|
|
420
|
-
try {
|
|
421
|
-
const { getNativeModelSnapshot } = await import('../services/desktop-cache.js');
|
|
422
|
-
const snapshotBuffer = await getNativeModelSnapshot(nativeCacheKey);
|
|
423
|
-
if (snapshotBuffer) {
|
|
424
|
-
metadataReadCompleteMs = performance.now() - totalStartTime;
|
|
425
|
-
metadataReadDurationMs = performance.now() - metadataReadStart;
|
|
426
|
-
metadataParseStartMs = performance.now() - totalStartTime;
|
|
427
|
-
parseStart = performance.now();
|
|
428
|
-
const dataStore = await restoreDesktopMetadataSnapshot(snapshotBuffer);
|
|
429
|
-
if (spatialReadyMs === null) {
|
|
430
|
-
spatialReadyMs = performance.now() - totalStartTime;
|
|
431
|
-
}
|
|
432
|
-
metadataCompleteMs = performance.now() - totalStartTime;
|
|
433
|
-
metadataParseDurationMs = performance.now() - parseStart;
|
|
434
|
-
finalizeNativeMetadata(dataStore);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
} catch (error) {
|
|
438
|
-
nativeMetadataSnapshotHit = false;
|
|
439
|
-
nativeMetadataSource = 'ifc-parse';
|
|
440
|
-
void logToDesktopTerminal(
|
|
441
|
-
'warn',
|
|
442
|
-
`[useIfc] Native metadata snapshot hydration failed for ${fileName}, falling back to IFC parse: ${error instanceof Error ? error.message : String(error)}`
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const bytes = await readNativeFile(file.path);
|
|
448
|
-
if (loadSessionRef.current !== currentSession) return;
|
|
449
|
-
metadataReadCompleteMs = performance.now() - totalStartTime;
|
|
450
|
-
metadataReadDurationMs = performance.now() - metadataReadStart;
|
|
451
|
-
const copyStart = performance.now();
|
|
452
|
-
const metadataBuffer = toExactArrayBuffer(bytes);
|
|
453
|
-
metadataBufferCopyDurationMs = performance.now() - copyStart;
|
|
454
|
-
metadataParseStartMs = performance.now() - totalStartTime;
|
|
455
|
-
parseStart = performance.now();
|
|
456
|
-
const parser = new IfcParser();
|
|
457
|
-
const wasmApi = hugeNativeMode ? await getMetadataScanApi() : undefined;
|
|
458
|
-
const dataStore = await parser.parseColumnar(metadataBuffer, {
|
|
459
|
-
wasmApi,
|
|
460
|
-
yieldIntervalMs: hugeNativeMode ? 32 : undefined,
|
|
461
|
-
deferPropertyAtomIndex: hugeNativeMode,
|
|
462
|
-
disableWorkerScan: false,
|
|
463
|
-
onSpatialReady: (partialStore) => {
|
|
464
|
-
if (loadSessionRef.current !== currentSession) return;
|
|
465
|
-
if (spatialReadyMs === null) {
|
|
466
|
-
spatialReadyMs = performance.now() - totalStartTime;
|
|
467
|
-
}
|
|
468
|
-
setIfcDataStore(partialStore);
|
|
469
|
-
},
|
|
470
|
-
});
|
|
471
|
-
queueNativeMetadataSnapshotWrite(dataStore, metadataBuffer);
|
|
472
|
-
metadataCompleteMs = performance.now() - totalStartTime;
|
|
473
|
-
metadataParseDurationMs = performance.now() - parseStart;
|
|
474
|
-
finalizeNativeMetadata(dataStore);
|
|
475
|
-
})().catch((error) => {
|
|
476
|
-
if (loadSessionRef.current !== currentSession) return;
|
|
477
|
-
metadataFailedMs = performance.now() - totalStartTime;
|
|
478
|
-
updateModel(primaryModelId, {
|
|
479
|
-
loadState: 'error',
|
|
480
|
-
loadError: error instanceof Error ? error.message : String(error),
|
|
481
|
-
});
|
|
482
|
-
void logToDesktopTerminal(
|
|
483
|
-
'warn',
|
|
484
|
-
`[useIfc] Native metadata parse failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
|
|
485
|
-
);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
return metadataParsingPromise;
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
if (shouldUseNativeCache) {
|
|
492
|
-
const { hasNativeGeometryCache, hasNativeModelSnapshot } = await import('../services/desktop-cache.js');
|
|
493
|
-
setProgress({ phase: 'Checking native cache', percent: 5 });
|
|
494
|
-
nativeGeometryCacheHit = await hasNativeGeometryCache(nativeCacheKey);
|
|
495
|
-
nativeMetadataSnapshotHit = nativeGeometryCacheHit ? await hasNativeModelSnapshot(nativeCacheKey) : false;
|
|
496
|
-
nativeMetadataSource = nativeGeometryCacheHit && nativeMetadataSnapshotHit ? 'snapshot' : 'ifc-parse';
|
|
497
|
-
nativeMetadataStartGate = 'immediate';
|
|
498
|
-
updateModel(primaryModelId, { cacheState: nativeGeometryCacheHit ? 'hit' : 'miss' });
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (nativeMetadataStartGate === 'immediate') {
|
|
502
|
-
startNativeMetadataParsing();
|
|
503
|
-
} else {
|
|
504
|
-
void logToDesktopTerminal(
|
|
505
|
-
'info',
|
|
506
|
-
`[useIfc] Deferring native metadata to ${nativeMetadataStartGate} for ${fileName}`
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const nativeStream = await startDisabledNativeDesktopRendererModel(
|
|
511
|
-
file.path,
|
|
512
|
-
shouldUseNativeCache ? nativeCacheKey : undefined,
|
|
513
|
-
);
|
|
514
|
-
|
|
515
|
-
for await (const event of nativeStream) {
|
|
516
|
-
switch (event.type) {
|
|
517
|
-
case 'sessionReady':
|
|
518
|
-
void logToDesktopTerminal(
|
|
519
|
-
'info',
|
|
520
|
-
event.cacheHit
|
|
521
|
-
? `[useIfc] Native renderer cache hit for ${fileName}`
|
|
522
|
-
: `[useIfc] Native renderer cold load for ${fileName}`
|
|
523
|
-
);
|
|
524
|
-
break;
|
|
525
|
-
case 'modelOpen':
|
|
526
|
-
modelOpenMs = performance.now() - totalStartTime;
|
|
527
|
-
setProgress({ phase: 'Streaming geometry into native renderer', percent: 35 });
|
|
528
|
-
break;
|
|
529
|
-
case 'batch':
|
|
530
|
-
batchCount = event.batchCount;
|
|
531
|
-
totalMeshes = event.totalMeshes;
|
|
532
|
-
if (firstBatchWaitMs === null) {
|
|
533
|
-
firstBatchWaitMs = performance.now() - totalStartTime;
|
|
534
|
-
}
|
|
535
|
-
setProgress({
|
|
536
|
-
phase: `Uploading native geometry (${(event.totalMeshes ?? 0).toLocaleString()} meshes)`,
|
|
537
|
-
percent: Math.min(85, 35 + Math.log10(Math.max(10, event.totalMeshes ?? 0)) * 12),
|
|
538
|
-
});
|
|
539
|
-
break;
|
|
540
|
-
case 'firstFrame':
|
|
541
|
-
firstVisibleGeometryMs = performance.now() - totalStartTime;
|
|
542
|
-
if (nativeMetadataStartGate === 'afterInteractiveGeometry' && !metadataParsingStarted) {
|
|
543
|
-
startNativeMetadataParsing();
|
|
544
|
-
}
|
|
545
|
-
break;
|
|
546
|
-
case 'complete':
|
|
547
|
-
geometryCompleted = true;
|
|
548
|
-
streamCompleteMs = performance.now() - totalStartTime;
|
|
549
|
-
totalMeshes = event.totalMeshes;
|
|
550
|
-
finalCoordinateInfo = event.coordinateInfo;
|
|
551
|
-
updateCoordinateInfo(event.coordinateInfo);
|
|
552
|
-
if (nativeMetadataStartGate === 'afterGeometryComplete' && !metadataParsingStarted) {
|
|
553
|
-
startNativeMetadataParsing();
|
|
554
|
-
}
|
|
555
|
-
updateModel(primaryModelId, {
|
|
556
|
-
loadState: metadataParsingStarted ? 'hydrating-metadata' : 'complete',
|
|
557
|
-
cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
|
|
558
|
-
});
|
|
559
|
-
setProgress({
|
|
560
|
-
phase: metadataParsingStarted ? 'Geometry ready, hydrating metadata' : 'Native geometry ready',
|
|
561
|
-
percent: metadataParsingStarted ? 92 : 100,
|
|
562
|
-
});
|
|
563
|
-
break;
|
|
564
|
-
case 'error':
|
|
565
|
-
throw new Error(event.message);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (harnessRequest?.waitForMetadataCompletion) {
|
|
570
|
-
if (!metadataParsingStarted) {
|
|
571
|
-
startNativeMetadataParsing();
|
|
572
|
-
}
|
|
573
|
-
if (metadataParsingPromise) {
|
|
574
|
-
await metadataParsingPromise;
|
|
575
|
-
}
|
|
576
|
-
if (metadataSnapshotWritePromise) {
|
|
577
|
-
await metadataSnapshotWritePromise;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
if (firstVisibleGeometryMs === null && streamCompleteMs !== null) {
|
|
582
|
-
firstVisibleGeometryMs = streamCompleteMs;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (!metadataParsingStarted) {
|
|
586
|
-
setLoading(false);
|
|
587
|
-
} else if (!harnessRequest?.waitForMetadataCompletion) {
|
|
588
|
-
setLoading(false);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
await finalizeActiveHarnessRun({
|
|
592
|
-
schemaVersion: 1,
|
|
593
|
-
source: 'desktop-native',
|
|
594
|
-
mode: harnessRequest ? 'startup-harness' : 'manual',
|
|
595
|
-
success: true,
|
|
596
|
-
runLabel: harnessRequest?.runLabel,
|
|
597
|
-
cache: {
|
|
598
|
-
key: nativeCacheKey,
|
|
599
|
-
hit: nativeGeometryCacheHit,
|
|
600
|
-
manifestMeshCount: null,
|
|
601
|
-
manifestShardCount: null,
|
|
602
|
-
},
|
|
603
|
-
file: {
|
|
604
|
-
path: file.path,
|
|
605
|
-
name: file.name,
|
|
606
|
-
sizeBytes: file.size,
|
|
607
|
-
sizeMB: fileSizeMB,
|
|
608
|
-
},
|
|
609
|
-
timings: {
|
|
610
|
-
modelOpenMs,
|
|
611
|
-
firstBatchWaitMs,
|
|
612
|
-
firstAppendGeometryBatchMs: null,
|
|
613
|
-
firstVisibleGeometryMs,
|
|
614
|
-
streamCompleteMs,
|
|
615
|
-
totalWallClockMs: performance.now() - totalStartTime,
|
|
616
|
-
metadataStartMs,
|
|
617
|
-
metadataReadCompleteMs,
|
|
618
|
-
metadataParseStartMs,
|
|
619
|
-
spatialReadyMs,
|
|
620
|
-
metadataCompleteMs,
|
|
621
|
-
metadataFailedMs,
|
|
622
|
-
metadataReadDurationMs,
|
|
623
|
-
metadataBufferCopyDurationMs,
|
|
624
|
-
metadataParseDurationMs,
|
|
625
|
-
nativeRendererFirstFrameMs: firstVisibleGeometryMs,
|
|
626
|
-
},
|
|
627
|
-
batches: {
|
|
628
|
-
estimatedTotal: shouldUseNativeCache ? totalMeshes : null,
|
|
629
|
-
totalBatches: batchCount,
|
|
630
|
-
totalMeshes,
|
|
631
|
-
firstBatchMeshes: null,
|
|
632
|
-
firstPayloadKind: 'native-renderer',
|
|
633
|
-
},
|
|
634
|
-
nativeStats: finalCoordinateInfo
|
|
635
|
-
? {
|
|
636
|
-
parseTimeMs: null,
|
|
637
|
-
entityScanTimeMs: null,
|
|
638
|
-
lookupTimeMs: null,
|
|
639
|
-
preprocessTimeMs: null,
|
|
640
|
-
geometryTimeMs: streamCompleteMs,
|
|
641
|
-
totalTimeMs: streamCompleteMs,
|
|
642
|
-
firstChunkReadyTimeMs: firstBatchWaitMs,
|
|
643
|
-
firstChunkPackTimeMs: null,
|
|
644
|
-
firstChunkEmittedTimeMs: null,
|
|
645
|
-
firstChunkEmitTimeMs: null,
|
|
646
|
-
}
|
|
647
|
-
: null,
|
|
648
|
-
metadata: {
|
|
649
|
-
started: metadataParsingStarted,
|
|
650
|
-
metadataStartMs,
|
|
651
|
-
metadataReadCompleteMs,
|
|
652
|
-
metadataParseStartMs,
|
|
653
|
-
spatialReadyMs,
|
|
654
|
-
metadataCompleteMs,
|
|
655
|
-
metadataFailedMs,
|
|
656
|
-
metadataReadDurationMs,
|
|
657
|
-
metadataBufferCopyDurationMs,
|
|
658
|
-
metadataParseDurationMs,
|
|
659
|
-
},
|
|
660
|
-
firstBatchTelemetry: null,
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Desktop native streaming path is reserved for truly large IFC files.
|
|
667
|
-
// Mid-size files are more stable on the shared WASM/web loader and still
|
|
668
|
-
// provide full viewer parity without the native streaming complexity.
|
|
669
|
-
if (
|
|
670
|
-
isNativeFileHandle(file)
|
|
671
|
-
&& fileName.toLowerCase().endsWith('.ifc')
|
|
672
|
-
&& file.size >= HUGE_NATIVE_FILE_THRESHOLD
|
|
673
|
-
) {
|
|
674
|
-
const harnessRequest = getActiveHarnessRequest();
|
|
675
|
-
const nativeCacheKey = computeNativeCacheKey(file);
|
|
676
|
-
const shouldUseNativeCache = file.size >= CACHE_SIZE_THRESHOLD;
|
|
677
|
-
const hugeNativeMode = file.size >= HUGE_NATIVE_FILE_THRESHOLD;
|
|
678
|
-
const retainAllMeshes = !hugeNativeMode;
|
|
679
|
-
console.log(`[useIfc] Native path load: ${fileName}, size: ${fileSizeMB.toFixed(2)}MB`);
|
|
680
|
-
void logToDesktopTerminal(
|
|
681
|
-
'info',
|
|
682
|
-
`[useIfc] Native path load start: ${fileName} (${fileSizeMB.toFixed(2)} MB) path=${file.path} hugeMode=${hugeNativeMode ? 'yes' : 'no'}`
|
|
683
|
-
);
|
|
684
|
-
setBoundedGeometryMode(hugeNativeMode);
|
|
685
|
-
setGeometryStreamingActive(true);
|
|
686
|
-
setIfcDataStore(null);
|
|
687
|
-
setProgress({ phase: 'Starting native geometry streaming', percent: 10 });
|
|
688
|
-
|
|
689
|
-
// Snapshot the user's "Merge Multilayer Walls" preference once
|
|
690
|
-
// at load time — flipping the toggle mid-stream cannot affect
|
|
691
|
-
// an in-flight WASM pipeline, the reload banner handles that.
|
|
692
|
-
const mergeLayersAtLoad = useViewerStore.getState().mergeLayers;
|
|
693
|
-
const geometryProcessor = new GeometryProcessor({
|
|
694
|
-
quality: GeometryQuality.Balanced,
|
|
695
|
-
preferNative: true,
|
|
696
|
-
mergeLayers: mergeLayersAtLoad,
|
|
697
|
-
});
|
|
698
370
|
|
|
699
|
-
let estimatedTotal = 0;
|
|
700
|
-
let totalMeshes = 0;
|
|
701
|
-
let totalVertices = 0;
|
|
702
|
-
let totalTriangles = 0;
|
|
703
|
-
const allMeshes: MeshData[] = [];
|
|
704
|
-
let finalCoordinateInfo: CoordinateInfo | null = null;
|
|
705
|
-
let batchCount = 0;
|
|
706
|
-
let modelOpenMs: number | null = null;
|
|
707
|
-
let firstGeometryTime = 0;
|
|
708
|
-
let firstAppendGeometryBatchMs: number | null = null;
|
|
709
|
-
let firstVisibleGeometryMs: number | null = null;
|
|
710
|
-
let jsFirstChunkReceivedMs: number | null = null;
|
|
711
|
-
let lastTotalMeshes = 0;
|
|
712
|
-
let pendingMeshes: MeshData[] = [];
|
|
713
|
-
let loggedFirstAppendStoreState = false;
|
|
714
|
-
let lastRenderTime = 0;
|
|
715
|
-
let streamCompleteMs: number | null = null;
|
|
716
|
-
let metadataStartMs: number | null = null;
|
|
717
|
-
let metadataReadCompleteMs: number | null = null;
|
|
718
|
-
let metadataParseStartMs: number | null = null;
|
|
719
|
-
let spatialReadyMs: number | null = null;
|
|
720
|
-
let metadataCompleteMs: number | null = null;
|
|
721
|
-
let metadataFailedMs: number | null = null;
|
|
722
|
-
let metadataReadDurationMs: number | null = null;
|
|
723
|
-
let metadataBufferCopyDurationMs: number | null = null;
|
|
724
|
-
let metadataParseDurationMs: number | null = null;
|
|
725
|
-
let metadataParsingPromise: Promise<void> | null = null;
|
|
726
|
-
let metadataStallWatchId: ReturnType<typeof globalThis.setInterval> | null = null;
|
|
727
|
-
let lastMetadataActivityTime = 0;
|
|
728
|
-
let currentMetadataActivity = 'idle';
|
|
729
|
-
let firstNativeBatchTelemetry: {
|
|
730
|
-
batchSequence: number;
|
|
731
|
-
payloadKind: string;
|
|
732
|
-
meshCount: number;
|
|
733
|
-
positionsLen: number;
|
|
734
|
-
normalsLen: number;
|
|
735
|
-
indicesLen: number;
|
|
736
|
-
chunkReadyTimeMs: number;
|
|
737
|
-
packTimeMs: number;
|
|
738
|
-
emittedTimeMs: number;
|
|
739
|
-
emitTimeMs: number;
|
|
740
|
-
jsReceivedTimeMs?: number;
|
|
741
|
-
} | null = null;
|
|
742
|
-
let nativeStats: {
|
|
743
|
-
parseTimeMs?: number;
|
|
744
|
-
entityScanTimeMs?: number;
|
|
745
|
-
lookupTimeMs?: number;
|
|
746
|
-
preprocessTimeMs?: number;
|
|
747
|
-
geometryTimeMs?: number;
|
|
748
|
-
totalTimeMs?: number;
|
|
749
|
-
firstChunkReadyTimeMs?: number;
|
|
750
|
-
firstChunkPackTimeMs?: number;
|
|
751
|
-
firstChunkEmittedTimeMs?: number;
|
|
752
|
-
firstChunkEmitTimeMs?: number;
|
|
753
|
-
} | null = null;
|
|
754
|
-
const RENDER_INTERVAL_MS = getRenderIntervalMs(fileSizeMB);
|
|
755
|
-
const NATIVE_PENDING_MESH_THRESHOLD =
|
|
756
|
-
fileSizeMB > 768 ? 8192 :
|
|
757
|
-
fileSizeMB > 512 ? 6144 :
|
|
758
|
-
fileSizeMB > 256 ? 4096 :
|
|
759
|
-
fileSizeMB > 100 ? 2048 :
|
|
760
|
-
512;
|
|
761
|
-
const HUGE_NATIVE_APPEND_CHUNK_SIZE = fileSizeMB > 768 ? 2048 : hugeNativeMode ? 1536 : 0;
|
|
762
|
-
const HUGE_NATIVE_APPEND_YIELD_THRESHOLD = fileSizeMB > 768 ? 8192 : 6144;
|
|
763
|
-
const HUGE_NATIVE_APPEND_YIELD_BUDGET_MS = 10;
|
|
764
|
-
let metadataParsingStarted = false;
|
|
765
|
-
let geometryCompleted = false;
|
|
766
|
-
let fullNativeDataStore: IfcDataStore | null = null;
|
|
767
|
-
let nativeLoadStage: 'open' | 'streamGeometry' | 'finalizeGeometry' | 'hydrateMetadata' | 'complete' = 'open';
|
|
768
|
-
let nativeMetadataSource: 'snapshot' | 'ifc-parse' = 'ifc-parse';
|
|
769
|
-
let nativeMetadataStartGate = 'immediate' as 'immediate' | 'afterInteractiveGeometry' | 'afterGeometryComplete';
|
|
770
|
-
|
|
771
|
-
setGeometryResult(null);
|
|
772
|
-
|
|
773
|
-
const maybeBuildNativeSpatialIndex = () => {
|
|
774
|
-
if (
|
|
775
|
-
!retainAllMeshes ||
|
|
776
|
-
!geometryCompleted ||
|
|
777
|
-
!fullNativeDataStore ||
|
|
778
|
-
allMeshes.length === 0 ||
|
|
779
|
-
hugeNativeMode ||
|
|
780
|
-
loadSessionRef.current !== currentSession
|
|
781
|
-
) {
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
buildSpatialIndexGuarded(allMeshes, fullNativeDataStore, setIfcDataStore);
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
const flushPendingNativeMeshes = async (
|
|
788
|
-
coordinateInfo: CoordinateInfo | null | undefined,
|
|
789
|
-
totalMeshesSoFar: number,
|
|
790
|
-
) => {
|
|
791
|
-
if (pendingMeshes.length === 0) {
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
if (firstAppendGeometryBatchMs === null) {
|
|
796
|
-
firstAppendGeometryBatchMs = performance.now() - totalStartTime;
|
|
797
|
-
void logToDesktopTerminal(
|
|
798
|
-
'info',
|
|
799
|
-
`[useIfc] Native first appendGeometryBatch for ${fileName}: ${firstAppendGeometryBatchMs.toFixed(0)}ms`
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
void totalMeshesSoFar;
|
|
804
|
-
|
|
805
|
-
const appendMeshesToStore = (meshesToAppend: MeshData[]) => {
|
|
806
|
-
const appendGeometryBatchToStore = getViewerStoreApi().getState().appendGeometryBatch;
|
|
807
|
-
if (hugeNativeMode) {
|
|
808
|
-
flushSync(() => {
|
|
809
|
-
appendGeometryBatchToStore(meshesToAppend, coordinateInfo ?? undefined);
|
|
810
|
-
});
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
appendGeometryBatchToStore(meshesToAppend, coordinateInfo ?? undefined);
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
if (!hugeNativeMode || HUGE_NATIVE_APPEND_CHUNK_SIZE <= 0 || pendingMeshes.length <= HUGE_NATIVE_APPEND_CHUNK_SIZE) {
|
|
817
|
-
appendMeshesToStore(pendingMeshes);
|
|
818
|
-
if (!loggedFirstAppendStoreState) {
|
|
819
|
-
const stateAfterAppend = useViewerStore.getState();
|
|
820
|
-
void logToDesktopTerminal(
|
|
821
|
-
'info',
|
|
822
|
-
`[useIfc] Store after append for ${fileName}: activeModelId=${stateAfterAppend.activeModelId ?? 'null'} legacyMeshes=${stateAfterAppend.geometryResult?.meshes.length ?? 0} modelMeshes=${stateAfterAppend.models.get(primaryModelId)?.geometryResult?.meshes.length ?? 0} geometryTick=${stateAfterAppend.geometryUpdateTick}`
|
|
823
|
-
);
|
|
824
|
-
loggedFirstAppendStoreState = true;
|
|
825
|
-
}
|
|
826
|
-
if (hugeNativeMode) {
|
|
827
|
-
await yieldToUiThread();
|
|
828
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
829
|
-
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
pendingMeshes = [];
|
|
833
|
-
markFirstVisibleGeometry();
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
let appendedSinceYield = 0;
|
|
838
|
-
let appendWindowStart = performance.now();
|
|
839
|
-
while (pendingMeshes.length > 0) {
|
|
840
|
-
const chunk = pendingMeshes.splice(0, HUGE_NATIVE_APPEND_CHUNK_SIZE);
|
|
841
|
-
appendMeshesToStore(chunk);
|
|
842
|
-
if (!loggedFirstAppendStoreState) {
|
|
843
|
-
const stateAfterAppend = useViewerStore.getState();
|
|
844
|
-
void logToDesktopTerminal(
|
|
845
|
-
'info',
|
|
846
|
-
`[useIfc] Store after append for ${fileName}: activeModelId=${stateAfterAppend.activeModelId ?? 'null'} legacyMeshes=${stateAfterAppend.geometryResult?.meshes.length ?? 0} modelMeshes=${stateAfterAppend.models.get(primaryModelId)?.geometryResult?.meshes.length ?? 0} geometryTick=${stateAfterAppend.geometryUpdateTick}`
|
|
847
|
-
);
|
|
848
|
-
loggedFirstAppendStoreState = true;
|
|
849
|
-
}
|
|
850
|
-
appendedSinceYield += chunk.length;
|
|
851
|
-
markFirstVisibleGeometry();
|
|
852
|
-
if (pendingMeshes.length === 0) {
|
|
853
|
-
break;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const shouldYield =
|
|
857
|
-
appendedSinceYield >= HUGE_NATIVE_APPEND_YIELD_THRESHOLD ||
|
|
858
|
-
performance.now() - appendWindowStart >= HUGE_NATIVE_APPEND_YIELD_BUDGET_MS;
|
|
859
|
-
if (shouldYield) {
|
|
860
|
-
await yieldToUiThread();
|
|
861
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
862
|
-
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
863
|
-
}
|
|
864
|
-
appendedSinceYield = 0;
|
|
865
|
-
appendWindowStart = performance.now();
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
const markFirstVisibleGeometry = () => {
|
|
871
|
-
if (firstVisibleGeometryMs !== null) return;
|
|
872
|
-
requestAnimationFrame(() => {
|
|
873
|
-
if (firstVisibleGeometryMs !== null || loadSessionRef.current !== currentSession) return;
|
|
874
|
-
firstVisibleGeometryMs = performance.now() - totalStartTime;
|
|
875
|
-
void logToDesktopTerminal(
|
|
876
|
-
'info',
|
|
877
|
-
`[useIfc] Native first visible geometry for ${fileName}: ${firstVisibleGeometryMs.toFixed(0)}ms`
|
|
878
|
-
);
|
|
879
|
-
});
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
const finalizeNativeDataStore = (dataStore: IfcDataStore) => {
|
|
883
|
-
if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
884
|
-
const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
|
|
885
|
-
for (const [storeyId, height] of calculatedHeights) {
|
|
886
|
-
dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
fullNativeDataStore = dataStore;
|
|
890
|
-
setIfcDataStore(dataStore);
|
|
891
|
-
if (geometryCompleted) {
|
|
892
|
-
nativeLoadStage = 'complete';
|
|
893
|
-
}
|
|
894
|
-
finalizePrimaryModel(
|
|
895
|
-
dataStore,
|
|
896
|
-
useViewerStore.getState().geometryResult,
|
|
897
|
-
getSchemaVersion(dataStore),
|
|
898
|
-
{
|
|
899
|
-
loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
|
|
900
|
-
cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
|
|
901
|
-
},
|
|
902
|
-
);
|
|
903
|
-
updateModel(primaryModelId, {
|
|
904
|
-
geometryLoadState: geometryCompleted ? 'complete' : 'interactive',
|
|
905
|
-
metadataLoadState: 'complete',
|
|
906
|
-
interactiveReady: true,
|
|
907
|
-
});
|
|
908
|
-
maybeBuildNativeSpatialIndex();
|
|
909
|
-
};
|
|
910
|
-
|
|
911
|
-
const hydrateNativeSpatialDataStore = (
|
|
912
|
-
nativeMetadata: NonNullable<Awaited<ReturnType<typeof restoreNativeMetadataSnapshot>>>,
|
|
913
|
-
) => {
|
|
914
|
-
const spatialDataStore = buildIfcDataStoreFromNativeMetadata(nativeMetadata);
|
|
915
|
-
if (!spatialDataStore) {
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
if (spatialDataStore.spatialHierarchy && spatialDataStore.spatialHierarchy.storeyHeights.size === 0 && spatialDataStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
919
|
-
const calculatedHeights = calculateStoreyHeights(spatialDataStore.spatialHierarchy.storeyElevations);
|
|
920
|
-
for (const [storeyId, height] of calculatedHeights) {
|
|
921
|
-
spatialDataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
const state = useViewerStore.getState();
|
|
925
|
-
const currentGeometryResult =
|
|
926
|
-
state.models.get(primaryModelId)?.geometryResult ??
|
|
927
|
-
state.geometryResult;
|
|
928
|
-
setIfcDataStore(spatialDataStore);
|
|
929
|
-
finalizePrimaryModel(
|
|
930
|
-
spatialDataStore,
|
|
931
|
-
currentGeometryResult,
|
|
932
|
-
nativeMetadata.schemaVersion,
|
|
933
|
-
{
|
|
934
|
-
loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
|
|
935
|
-
cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
|
|
936
|
-
},
|
|
937
|
-
);
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
let nativeMetadataSnapshotHit = false;
|
|
941
|
-
let metadataSnapshotWritePromise: Promise<void> | null = null;
|
|
942
|
-
|
|
943
|
-
const queueNativeMetadataSnapshotWrite = (
|
|
944
|
-
dataStore: IfcDataStore,
|
|
945
|
-
sourceBuffer: ArrayBuffer,
|
|
946
|
-
) => {
|
|
947
|
-
metadataSnapshotWritePromise = (async () => {
|
|
948
|
-
await new Promise<void>((resolve) => {
|
|
949
|
-
const channel = new MessageChannel();
|
|
950
|
-
channel.port1.onmessage = () => resolve();
|
|
951
|
-
channel.port2.postMessage(null);
|
|
952
|
-
});
|
|
953
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
954
|
-
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
955
|
-
}
|
|
956
|
-
await writeNativeMetadataSnapshot(dataStore, sourceBuffer);
|
|
957
|
-
})();
|
|
958
|
-
};
|
|
959
|
-
|
|
960
|
-
const writeNativeMetadataSnapshot = async (
|
|
961
|
-
dataStore: IfcDataStore,
|
|
962
|
-
sourceBuffer: ArrayBuffer,
|
|
963
|
-
): Promise<void> => {
|
|
964
|
-
if (!shouldUseNativeCache || !nativeCacheKey) return;
|
|
965
|
-
try {
|
|
966
|
-
const { setNativeModelSnapshot } = await import('../services/desktop-cache.js');
|
|
967
|
-
const snapshotBuffer = await buildDesktopMetadataSnapshot(dataStore, sourceBuffer);
|
|
968
|
-
await setNativeModelSnapshot(nativeCacheKey, snapshotBuffer);
|
|
969
|
-
} catch (error) {
|
|
970
|
-
console.warn('[useIfc] Failed to persist native metadata snapshot:', error);
|
|
971
|
-
void logToDesktopTerminal(
|
|
972
|
-
'warn',
|
|
973
|
-
`[useIfc] Native metadata snapshot write failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
|
|
978
|
-
const noteMetadataActivity = (activity: string) => {
|
|
979
|
-
currentMetadataActivity = activity;
|
|
980
|
-
lastMetadataActivityTime = performance.now();
|
|
981
|
-
};
|
|
982
|
-
|
|
983
|
-
const stopMetadataStallWatch = () => {
|
|
984
|
-
if (metadataStallWatchId !== null) {
|
|
985
|
-
globalThis.clearInterval(metadataStallWatchId);
|
|
986
|
-
metadataStallWatchId = null;
|
|
987
|
-
}
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
const startMetadataStallWatch = () => {
|
|
991
|
-
stopMetadataStallWatch();
|
|
992
|
-
noteMetadataActivity('starting');
|
|
993
|
-
metadataStallWatchId = globalThis.setInterval(() => {
|
|
994
|
-
if (loadSessionRef.current !== currentSession) {
|
|
995
|
-
stopMetadataStallWatch();
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
const idleForMs = performance.now() - lastMetadataActivityTime;
|
|
999
|
-
if (idleForMs < 8000) return;
|
|
1000
|
-
lastMetadataActivityTime = performance.now();
|
|
1001
|
-
void logToDesktopTerminal(
|
|
1002
|
-
'warn',
|
|
1003
|
-
`[useIfc] Metadata stall watch for ${fileName}: stage=${nativeLoadStage} idle=${idleForMs.toFixed(0)}ms phase=${currentMetadataActivity} batches=${batchCount} meshes=${lastTotalMeshes} geometryCompleted=${geometryCompleted}`
|
|
1004
|
-
);
|
|
1005
|
-
}, 5000);
|
|
1006
|
-
};
|
|
1007
|
-
|
|
1008
|
-
const startNativeMetadataParsing = (): Promise<void> | null => {
|
|
1009
|
-
if (metadataParsingStarted) return metadataParsingPromise;
|
|
1010
|
-
metadataParsingStarted = true;
|
|
1011
|
-
nativeLoadStage = 'hydrateMetadata';
|
|
1012
|
-
const metadataStartTime = performance.now();
|
|
1013
|
-
metadataStartMs = metadataStartTime - totalStartTime;
|
|
1014
|
-
let lastMetadataProgressPhase = '';
|
|
1015
|
-
let lastMetadataProgressPercent = -1;
|
|
1016
|
-
startMetadataStallWatch();
|
|
1017
|
-
setMetadataProgress({ phase: 'Bootstrapping metadata', percent: 5, indeterminate: hugeNativeMode });
|
|
1018
|
-
updateModel(primaryModelId, {
|
|
1019
|
-
loadState: 'hydrating-metadata',
|
|
1020
|
-
metadataLoadState: 'bootstrapping',
|
|
1021
|
-
});
|
|
1022
|
-
void logToDesktopTerminal(
|
|
1023
|
-
'info',
|
|
1024
|
-
`[useIfc] Native metadata parse start for ${fileName} source=${nativeMetadataSource} gate=${nativeMetadataStartGate}`
|
|
1025
|
-
);
|
|
1026
|
-
|
|
1027
|
-
const metadataReadStartTime = performance.now();
|
|
1028
|
-
let parseStartTime = 0;
|
|
1029
|
-
metadataParsingPromise = (async () => {
|
|
1030
|
-
if (hugeNativeMode) {
|
|
1031
|
-
noteMetadataActivity('native bootstrap');
|
|
1032
|
-
metadataParseStartMs = performance.now() - totalStartTime;
|
|
1033
|
-
parseStartTime = performance.now();
|
|
1034
|
-
if (nativeMetadataSnapshotHit) {
|
|
1035
|
-
const restoredSnapshot = await restoreNativeMetadataSnapshot(nativeCacheKey);
|
|
1036
|
-
if (restoredSnapshot && loadSessionRef.current === currentSession) {
|
|
1037
|
-
try {
|
|
1038
|
-
spatialReadyMs = performance.now() - totalStartTime;
|
|
1039
|
-
hydrateNativeSpatialDataStore(restoredSnapshot);
|
|
1040
|
-
updateModel(primaryModelId, {
|
|
1041
|
-
nativeMetadata: restoredSnapshot,
|
|
1042
|
-
schemaVersion: restoredSnapshot.schemaVersion,
|
|
1043
|
-
metadataLoadState: 'spatial-ready',
|
|
1044
|
-
interactiveReady: true,
|
|
1045
|
-
});
|
|
1046
|
-
setMetadataProgress({ phase: 'Restored metadata sidecar', percent: 70 });
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
nativeMetadataSnapshotHit = false;
|
|
1049
|
-
nativeMetadataSource = 'ifc-parse';
|
|
1050
|
-
void logToDesktopTerminal(
|
|
1051
|
-
'warn',
|
|
1052
|
-
`[useIfc] Native metadata snapshot restore incompatible for ${fileName}, continuing with live bootstrap: ${error instanceof Error ? error.message : String(error)}`
|
|
1053
|
-
);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
void logToDesktopTerminal(
|
|
1058
|
-
'info',
|
|
1059
|
-
`[useIfc] Awaiting native metadata bootstrap for ${fileName}`
|
|
1060
|
-
);
|
|
1061
|
-
const nativeMetadata = await bootstrapNativeMetadata(file.path, nativeCacheKey);
|
|
1062
|
-
if (loadSessionRef.current !== currentSession) {
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
const spatialNodeCount = countNativeSpatialNodes(nativeMetadata.spatialTree);
|
|
1066
|
-
void logToDesktopTerminal(
|
|
1067
|
-
'info',
|
|
1068
|
-
`[useIfc] Native metadata bootstrap resolved for ${fileName}: elapsed=${(performance.now() - parseStartTime).toFixed(0)}ms hasTree=${nativeMetadata.spatialTree ? 'yes' : 'no'} spatialNodes=${spatialNodeCount}`
|
|
1069
|
-
);
|
|
1070
|
-
metadataReadCompleteMs = performance.now() - totalStartTime;
|
|
1071
|
-
metadataReadDurationMs = metadataReadCompleteMs - metadataStartMs;
|
|
1072
|
-
spatialReadyMs = performance.now() - totalStartTime;
|
|
1073
|
-
void logToDesktopTerminal(
|
|
1074
|
-
'info',
|
|
1075
|
-
`[useIfc] Applying native metadata to store for ${fileName}`
|
|
1076
|
-
);
|
|
1077
|
-
hydrateNativeSpatialDataStore(nativeMetadata);
|
|
1078
|
-
updateModel(primaryModelId, {
|
|
1079
|
-
nativeMetadata,
|
|
1080
|
-
schemaVersion: nativeMetadata.schemaVersion,
|
|
1081
|
-
metadataLoadState: 'spatial-ready',
|
|
1082
|
-
interactiveReady: true,
|
|
1083
|
-
});
|
|
1084
|
-
void logToDesktopTerminal(
|
|
1085
|
-
'info',
|
|
1086
|
-
`[useIfc] Native metadata store update complete for ${fileName}`
|
|
1087
|
-
);
|
|
1088
|
-
setMetadataProgress({ phase: 'Spatial tree ready', percent: 70 });
|
|
1089
|
-
if (!nativeMetadataSnapshotHit) {
|
|
1090
|
-
void persistNativeMetadataSnapshot(nativeMetadata);
|
|
1091
|
-
}
|
|
1092
|
-
metadataCompleteMs = performance.now() - totalStartTime;
|
|
1093
|
-
metadataParseDurationMs = performance.now() - parseStartTime;
|
|
1094
|
-
updateModel(primaryModelId, {
|
|
1095
|
-
loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
|
|
1096
|
-
metadataLoadState: 'lazy',
|
|
1097
|
-
});
|
|
1098
|
-
setMetadataProgress({ phase: 'Metadata ready on demand', percent: 100 });
|
|
1099
|
-
return null;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (nativeGeometryCacheHit && nativeMetadataSnapshotHit) {
|
|
1103
|
-
try {
|
|
1104
|
-
const { getNativeModelSnapshot } = await import('../services/desktop-cache.js');
|
|
1105
|
-
const snapshotBuffer = await getNativeModelSnapshot(nativeCacheKey);
|
|
1106
|
-
if (!snapshotBuffer) {
|
|
1107
|
-
throw new Error(`missing-native-metadata-snapshot:${nativeCacheKey}`);
|
|
1108
|
-
}
|
|
1109
|
-
metadataReadCompleteMs = performance.now() - totalStartTime;
|
|
1110
|
-
metadataReadDurationMs = performance.now() - metadataReadStartTime;
|
|
1111
|
-
metadataParseStartMs = performance.now() - totalStartTime;
|
|
1112
|
-
parseStartTime = performance.now();
|
|
1113
|
-
noteMetadataActivity('snapshot hydrate');
|
|
1114
|
-
if (spatialReadyMs === null) {
|
|
1115
|
-
spatialReadyMs = performance.now() - totalStartTime;
|
|
1116
|
-
}
|
|
1117
|
-
setMetadataProgress({ phase: 'Restoring cached metadata', percent: 80 });
|
|
1118
|
-
return restoreDesktopMetadataSnapshot(snapshotBuffer);
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
nativeMetadataSnapshotHit = false;
|
|
1121
|
-
nativeMetadataSource = 'ifc-parse';
|
|
1122
|
-
void logToDesktopTerminal(
|
|
1123
|
-
'warn',
|
|
1124
|
-
`[useIfc] Native metadata snapshot hydration failed for ${fileName}, falling back to IFC parse: ${error instanceof Error ? error.message : String(error)}`
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
const bytes = await readNativeFile(file.path);
|
|
1130
|
-
if (loadSessionRef.current !== currentSession) {
|
|
1131
|
-
return null;
|
|
1132
|
-
}
|
|
1133
|
-
metadataReadCompleteMs = performance.now() - totalStartTime;
|
|
1134
|
-
metadataReadDurationMs = performance.now() - metadataReadStartTime;
|
|
1135
|
-
void logToDesktopTerminal(
|
|
1136
|
-
'info',
|
|
1137
|
-
`[useIfc] Native metadata file read complete for ${fileName}: ${metadataReadDurationMs.toFixed(0)}ms`
|
|
1138
|
-
);
|
|
1139
|
-
const copyStartTime = performance.now();
|
|
1140
|
-
const metadataBuffer = toExactArrayBuffer(bytes);
|
|
1141
|
-
metadataBufferCopyDurationMs = performance.now() - copyStartTime;
|
|
1142
|
-
metadataParseStartMs = performance.now() - totalStartTime;
|
|
1143
|
-
parseStartTime = performance.now();
|
|
1144
|
-
noteMetadataActivity('parse setup');
|
|
1145
|
-
void logToDesktopTerminal(
|
|
1146
|
-
'info',
|
|
1147
|
-
`[useIfc] Native metadata buffer copy complete for ${fileName}: ${metadataBufferCopyDurationMs.toFixed(0)}ms`
|
|
1148
|
-
);
|
|
1149
|
-
|
|
1150
|
-
const parser = new IfcParser();
|
|
1151
|
-
const wasmApi = hugeNativeMode ? await getMetadataScanApi() : undefined;
|
|
1152
|
-
const dataStore = await parser.parseColumnar(metadataBuffer, {
|
|
1153
|
-
wasmApi,
|
|
1154
|
-
yieldIntervalMs: hugeNativeMode ? 32 : undefined,
|
|
1155
|
-
deferPropertyAtomIndex: hugeNativeMode,
|
|
1156
|
-
disableWorkerScan: false,
|
|
1157
|
-
onProgress: (progress) => {
|
|
1158
|
-
if (!hugeNativeMode) return;
|
|
1159
|
-
noteMetadataActivity(`progress:${progress.phase}:${Math.round(progress.percent)}`);
|
|
1160
|
-
const roundedPercent = Math.round(progress.percent);
|
|
1161
|
-
const shouldLog =
|
|
1162
|
-
progress.phase !== lastMetadataProgressPhase ||
|
|
1163
|
-
roundedPercent >= lastMetadataProgressPercent + 5 ||
|
|
1164
|
-
roundedPercent === 100;
|
|
1165
|
-
if (!shouldLog) return;
|
|
1166
|
-
setMetadataProgress({
|
|
1167
|
-
phase: `Metadata ${progress.phase}`,
|
|
1168
|
-
percent: roundedPercent,
|
|
1169
|
-
indeterminate: false,
|
|
1170
|
-
});
|
|
1171
|
-
lastMetadataProgressPhase = progress.phase;
|
|
1172
|
-
lastMetadataProgressPercent = roundedPercent;
|
|
1173
|
-
void logToDesktopTerminal(
|
|
1174
|
-
'info',
|
|
1175
|
-
`[useIfc] Native metadata progress for ${fileName}: ${progress.phase} ${roundedPercent}%`
|
|
1176
|
-
);
|
|
1177
|
-
},
|
|
1178
|
-
onSpatialReady: (partialStore) => {
|
|
1179
|
-
if (loadSessionRef.current !== currentSession) return;
|
|
1180
|
-
noteMetadataActivity('spatial ready');
|
|
1181
|
-
if (spatialReadyMs === null) {
|
|
1182
|
-
spatialReadyMs = performance.now() - totalStartTime;
|
|
1183
|
-
}
|
|
1184
|
-
setMetadataProgress({ phase: 'Spatial tree ready', percent: 70 });
|
|
1185
|
-
if (partialStore.spatialHierarchy && partialStore.spatialHierarchy.storeyHeights.size === 0 && partialStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
1186
|
-
const calculatedHeights = calculateStoreyHeights(partialStore.spatialHierarchy.storeyElevations);
|
|
1187
|
-
for (const [storeyId, height] of calculatedHeights) {
|
|
1188
|
-
partialStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
setIfcDataStore(partialStore);
|
|
1192
|
-
void logToDesktopTerminal(
|
|
1193
|
-
'info',
|
|
1194
|
-
`[useIfc] Native spatial tree ready for ${fileName} at ${(performance.now() - totalStartTime).toFixed(0)}ms`
|
|
1195
|
-
);
|
|
1196
|
-
},
|
|
1197
|
-
onDiagnostic: (message) => {
|
|
1198
|
-
noteMetadataActivity(`diag:${message}`);
|
|
1199
|
-
void logToDesktopTerminal('info', `[useIfc][diag] ${fileName}: ${message}`);
|
|
1200
|
-
},
|
|
1201
|
-
});
|
|
1202
|
-
queueNativeMetadataSnapshotWrite(dataStore, metadataBuffer);
|
|
1203
|
-
return dataStore;
|
|
1204
|
-
})()
|
|
1205
|
-
.then((dataStore) => {
|
|
1206
|
-
stopMetadataStallWatch();
|
|
1207
|
-
if (loadSessionRef.current !== currentSession || !dataStore) return;
|
|
1208
|
-
metadataCompleteMs = performance.now() - totalStartTime;
|
|
1209
|
-
metadataParseDurationMs = parseStartTime > 0 ? performance.now() - parseStartTime : null;
|
|
1210
|
-
setMetadataProgress({ phase: 'Metadata ready', percent: 100 });
|
|
1211
|
-
finalizeNativeDataStore(dataStore);
|
|
1212
|
-
void logToDesktopTerminal(
|
|
1213
|
-
'info',
|
|
1214
|
-
`[useIfc] Native metadata parse complete for ${fileName}: total=${(performance.now() - metadataStartTime).toFixed(0)}ms read=${metadataReadDurationMs?.toFixed(0) ?? 'n/a'}ms copy=${metadataBufferCopyDurationMs?.toFixed(0) ?? 'n/a'}ms parse=${metadataParseDurationMs?.toFixed(0) ?? 'n/a'}ms`
|
|
1215
|
-
);
|
|
1216
|
-
})
|
|
1217
|
-
.catch((error) => {
|
|
1218
|
-
if (loadSessionRef.current !== currentSession) return;
|
|
1219
|
-
stopMetadataStallWatch();
|
|
1220
|
-
metadataFailedMs = performance.now() - totalStartTime;
|
|
1221
|
-
console.warn('[useIfc] Native metadata parsing failed:', error);
|
|
1222
|
-
updateModel(primaryModelId, {
|
|
1223
|
-
loadState: 'error',
|
|
1224
|
-
metadataLoadState: 'error',
|
|
1225
|
-
loadError: error instanceof Error ? error.message : String(error),
|
|
1226
|
-
});
|
|
1227
|
-
setMetadataProgress({ phase: 'Metadata failed', percent: 100 });
|
|
1228
|
-
void logToDesktopTerminal(
|
|
1229
|
-
'warn',
|
|
1230
|
-
`[useIfc] Native metadata parse failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
|
|
1231
|
-
);
|
|
1232
|
-
});
|
|
1233
|
-
return metadataParsingPromise;
|
|
1234
|
-
};
|
|
1235
|
-
|
|
1236
|
-
const HUGE_NATIVE_METADATA_START_BATCH = 20;
|
|
1237
|
-
let metadataStartQueued = false;
|
|
1238
|
-
const queueNativeMetadataStart = (reason: string) => {
|
|
1239
|
-
if (metadataParsingStarted || metadataStartQueued) return;
|
|
1240
|
-
metadataStartQueued = true;
|
|
1241
|
-
void logToDesktopTerminal('info', `[useIfc] Queueing metadata hydration for ${fileName} after ${reason}`);
|
|
1242
|
-
metadataStartQueued = false;
|
|
1243
|
-
if (loadSessionRef.current !== currentSession || metadataParsingStarted) return;
|
|
1244
|
-
void logToDesktopTerminal('info', `[useIfc] Starting metadata hydration after ${reason} for ${fileName}`);
|
|
1245
|
-
startNativeMetadataParsing();
|
|
1246
|
-
};
|
|
1247
|
-
|
|
1248
|
-
let nativeGeometryCacheHit = false;
|
|
1249
|
-
if (shouldUseNativeCache) {
|
|
1250
|
-
const { hasNativeGeometryCache, hasNativeModelSnapshot } = await import('../services/desktop-cache.js');
|
|
1251
|
-
setProgress({ phase: 'Checking cache', percent: 5 });
|
|
1252
|
-
setGeometryProgress({ phase: 'Checking geometry cache', percent: 5 });
|
|
1253
|
-
nativeGeometryCacheHit = await hasNativeGeometryCache(nativeCacheKey);
|
|
1254
|
-
nativeMetadataSnapshotHit = nativeGeometryCacheHit
|
|
1255
|
-
? await hasNativeModelSnapshot(nativeCacheKey)
|
|
1256
|
-
: false;
|
|
1257
|
-
nativeMetadataSource = nativeMetadataSnapshotHit ? 'snapshot' : 'ifc-parse';
|
|
1258
|
-
nativeMetadataStartGate = 'immediate';
|
|
1259
|
-
updateModel(primaryModelId, { cacheState: nativeGeometryCacheHit ? 'hit' : 'miss' });
|
|
1260
|
-
void logToDesktopTerminal(
|
|
1261
|
-
'info',
|
|
1262
|
-
nativeGeometryCacheHit
|
|
1263
|
-
? `[useIfc] Native geometry cache hit for ${fileName}`
|
|
1264
|
-
: `[useIfc] Native geometry cache miss for ${fileName}`
|
|
1265
|
-
);
|
|
1266
|
-
if (nativeMetadataStartGate === 'immediate') {
|
|
1267
|
-
startNativeMetadataParsing();
|
|
1268
|
-
} else {
|
|
1269
|
-
void logToDesktopTerminal(
|
|
1270
|
-
'info',
|
|
1271
|
-
nativeMetadataStartGate === 'afterInteractiveGeometry'
|
|
1272
|
-
? `[useIfc] Deferring metadata hydration until geometry batch ${HUGE_NATIVE_METADATA_START_BATCH} for ${fileName}`
|
|
1273
|
-
: `[useIfc] Deferring metadata hydration until geometry complete for ${fileName}`
|
|
1274
|
-
);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
if (!shouldUseNativeCache) {
|
|
1279
|
-
if (nativeMetadataStartGate === 'immediate') {
|
|
1280
|
-
startNativeMetadataParsing();
|
|
1281
|
-
} else {
|
|
1282
|
-
void logToDesktopTerminal(
|
|
1283
|
-
'info',
|
|
1284
|
-
`[useIfc] Deferring metadata hydration until geometry complete for ${fileName}`
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
await geometryProcessor.init();
|
|
1289
|
-
void logToDesktopTerminal('info', `[useIfc] GeometryProcessor.init complete for ${fileName}`);
|
|
1290
|
-
|
|
1291
|
-
const nativeStream = nativeGeometryCacheHit
|
|
1292
|
-
? geometryProcessor.processStreamingCache(nativeCacheKey)
|
|
1293
|
-
: geometryProcessor.processStreamingPath(
|
|
1294
|
-
file.path,
|
|
1295
|
-
file.size,
|
|
1296
|
-
shouldUseNativeCache ? nativeCacheKey : undefined,
|
|
1297
|
-
);
|
|
1298
|
-
|
|
1299
|
-
for await (const event of nativeStream) {
|
|
1300
|
-
const eventReceived = performance.now();
|
|
1301
|
-
|
|
1302
|
-
switch (event.type) {
|
|
1303
|
-
case 'start':
|
|
1304
|
-
estimatedTotal = event.totalEstimate;
|
|
1305
|
-
void logToDesktopTerminal('info', `[useIfc] Native stream start for ${fileName}: estimate=${Math.round(estimatedTotal)}`);
|
|
1306
|
-
break;
|
|
1307
|
-
case 'model-open':
|
|
1308
|
-
nativeLoadStage = 'streamGeometry';
|
|
1309
|
-
setProgress({ phase: 'Processing geometry (native precompute)', percent: 50, indeterminate: true });
|
|
1310
|
-
setGeometryProgress({ phase: 'Opening native geometry stream', percent: 10, indeterminate: true });
|
|
1311
|
-
modelOpenMs = performance.now() - totalStartTime;
|
|
1312
|
-
console.log(`[useIfc] Native model opened at ${modelOpenMs.toFixed(0)}ms`);
|
|
1313
|
-
void logToDesktopTerminal('info', `[useIfc] Native model opened for ${fileName} at ${modelOpenMs.toFixed(0)}ms`);
|
|
1314
|
-
break;
|
|
1315
|
-
case 'batch': {
|
|
1316
|
-
batchCount++;
|
|
1317
|
-
|
|
1318
|
-
if (batchCount === 1) {
|
|
1319
|
-
firstGeometryTime = performance.now() - totalStartTime;
|
|
1320
|
-
jsFirstChunkReceivedMs = event.nativeTelemetry?.jsReceivedTimeMs ?? firstGeometryTime;
|
|
1321
|
-
firstNativeBatchTelemetry = event.nativeTelemetry ?? null;
|
|
1322
|
-
updateModel(primaryModelId, {
|
|
1323
|
-
geometryLoadState: 'interactive',
|
|
1324
|
-
interactiveReady: true,
|
|
1325
|
-
});
|
|
1326
|
-
console.log(`[useIfc] Native batch #1: ${event.meshes.length} meshes, wait: ${firstGeometryTime.toFixed(0)}ms`);
|
|
1327
|
-
void logToDesktopTerminal('info', `[useIfc] Native first batch for ${fileName}: meshes=${event.meshes.length}, wait=${firstGeometryTime.toFixed(0)}ms`);
|
|
1328
|
-
if (event.nativeTelemetry) {
|
|
1329
|
-
const transferLagMs = (event.nativeTelemetry.jsReceivedTimeMs ?? 0) - event.nativeTelemetry.emittedTimeMs;
|
|
1330
|
-
void logToDesktopTerminal(
|
|
1331
|
-
'info',
|
|
1332
|
-
`[useIfc] Native first batch transport for ${fileName}: rustReady=${event.nativeTelemetry.chunkReadyTimeMs.toFixed(0)}ms pack=${event.nativeTelemetry.packTimeMs.toFixed(0)}ms emit=${event.nativeTelemetry.emitTimeMs.toFixed(0)}ms rustEmitted=${event.nativeTelemetry.emittedTimeMs.toFixed(0)}ms jsReceived=${(event.nativeTelemetry.jsReceivedTimeMs ?? 0).toFixed(0)}ms transfer=${transferLagMs.toFixed(0)}ms`
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
} else if (batchCount % 20 === 0) {
|
|
1336
|
-
void logToDesktopTerminal('info', `[useIfc] Native batch milestone for ${fileName}: batch=${batchCount}, totalMeshes=${event.totalSoFar}`);
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
for (let i = 0; i < event.meshes.length; i++) {
|
|
1340
|
-
const mesh = event.meshes[i];
|
|
1341
|
-
if (retainAllMeshes) {
|
|
1342
|
-
allMeshes.push(mesh);
|
|
1343
|
-
}
|
|
1344
|
-
totalVertices += mesh.positions.length / 3;
|
|
1345
|
-
totalTriangles += mesh.indices.length / 3;
|
|
1346
|
-
}
|
|
1347
|
-
finalCoordinateInfo = event.coordinateInfo ?? null;
|
|
1348
|
-
totalMeshes = event.totalSoFar;
|
|
1349
|
-
lastTotalMeshes = event.totalSoFar;
|
|
1350
|
-
|
|
1351
|
-
for (let i = 0; i < event.meshes.length; i++) pendingMeshes.push(event.meshes[i]);
|
|
1352
|
-
|
|
1353
|
-
if (
|
|
1354
|
-
nativeMetadataStartGate === 'afterInteractiveGeometry' &&
|
|
1355
|
-
!metadataParsingStarted &&
|
|
1356
|
-
batchCount >= HUGE_NATIVE_METADATA_START_BATCH &&
|
|
1357
|
-
firstAppendGeometryBatchMs !== null
|
|
1358
|
-
) {
|
|
1359
|
-
queueNativeMetadataStart(`geometry batch ${batchCount}`);
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
const timeSinceLastRender = eventReceived - lastRenderTime;
|
|
1363
|
-
const allowTimeBasedFlush = !hugeNativeMode || ENABLE_HUGE_TIME_FLUSH;
|
|
1364
|
-
const shouldRender =
|
|
1365
|
-
batchCount === 1 ||
|
|
1366
|
-
pendingMeshes.length >= NATIVE_PENDING_MESH_THRESHOLD ||
|
|
1367
|
-
(allowTimeBasedFlush && timeSinceLastRender >= RENDER_INTERVAL_MS);
|
|
1368
|
-
|
|
1369
|
-
if (shouldRender && pendingMeshes.length > 0) {
|
|
1370
|
-
await flushPendingNativeMeshes(event.coordinateInfo, totalMeshes);
|
|
1371
|
-
lastRenderTime = eventReceived;
|
|
1372
|
-
|
|
1373
|
-
const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal / 10, totalMeshes || 1)) * 45);
|
|
1374
|
-
setProgress({
|
|
1375
|
-
phase: `Rendering geometry (${totalMeshes} meshes)`,
|
|
1376
|
-
percent: progressPercent,
|
|
1377
|
-
indeterminate: false,
|
|
1378
|
-
});
|
|
1379
|
-
setGeometryProgress({
|
|
1380
|
-
phase: `Rendering geometry (${totalMeshes} meshes)`,
|
|
1381
|
-
percent: Math.min(99, progressPercent),
|
|
1382
|
-
indeterminate: false,
|
|
1383
|
-
});
|
|
1384
|
-
}
|
|
1385
|
-
break;
|
|
1386
|
-
}
|
|
1387
|
-
case 'complete':
|
|
1388
|
-
nativeLoadStage = 'finalizeGeometry';
|
|
1389
|
-
geometryCompleted = true;
|
|
1390
|
-
streamCompleteMs = performance.now() - totalStartTime;
|
|
1391
|
-
if (pendingMeshes.length > 0) {
|
|
1392
|
-
await flushPendingNativeMeshes(event.coordinateInfo, lastTotalMeshes);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
finalCoordinateInfo = event.coordinateInfo;
|
|
1396
|
-
updateCoordinateInfo(finalCoordinateInfo);
|
|
1397
|
-
maybeBuildNativeSpatialIndex();
|
|
1398
|
-
if (nativeMetadataStartGate === 'afterGeometryComplete' && !metadataParsingStarted) {
|
|
1399
|
-
queueNativeMetadataStart('geometry complete');
|
|
1400
|
-
}
|
|
1401
|
-
setProgress({
|
|
1402
|
-
phase: hugeNativeMode ? 'Geometry ready, hydrating metadata' : 'Complete',
|
|
1403
|
-
percent: 100,
|
|
1404
|
-
});
|
|
1405
|
-
setGeometryProgress({
|
|
1406
|
-
phase: 'Geometry interactive',
|
|
1407
|
-
percent: 100,
|
|
1408
|
-
});
|
|
1409
|
-
setMetadataProgress(
|
|
1410
|
-
hugeNativeMode
|
|
1411
|
-
? { phase: 'Preparing metadata', percent: nativeMetadataStartGate === 'afterGeometryComplete' ? 5 : 0, indeterminate: false }
|
|
1412
|
-
: { phase: 'Metadata complete', percent: 100 }
|
|
1413
|
-
);
|
|
1414
|
-
updateModel(primaryModelId, {
|
|
1415
|
-
loadState: hugeNativeMode ? 'hydrating-metadata' : 'complete',
|
|
1416
|
-
geometryLoadState: 'complete',
|
|
1417
|
-
metadataLoadState: hugeNativeMode ? 'bootstrapping' : 'complete',
|
|
1418
|
-
interactiveReady: true,
|
|
1419
|
-
cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
|
|
1420
|
-
});
|
|
1421
|
-
console.log(`[useIfc] Native geometry streaming complete: ${batchCount} batches, ${lastTotalMeshes} meshes`);
|
|
1422
|
-
void logToDesktopTerminal(
|
|
1423
|
-
'info',
|
|
1424
|
-
`[useIfc] Native stream complete for ${fileName}: stage=${nativeLoadStage} batches=${batchCount}, meshes=${lastTotalMeshes}`
|
|
1425
|
-
);
|
|
1426
|
-
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
1427
|
-
if (loadSessionRef.current === currentSession) {
|
|
1428
|
-
setGeometryStreamingActive(false);
|
|
1429
|
-
}
|
|
1430
|
-
break;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
nativeStats = geometryProcessor.getLastNativeStats();
|
|
1435
|
-
|
|
1436
|
-
const totalElapsedMs = performance.now() - totalStartTime;
|
|
1437
|
-
console.log(
|
|
1438
|
-
`[useIfc] ✓ ${fileName} (${fileSizeMB.toFixed(1)}MB) → ` +
|
|
1439
|
-
`${lastTotalMeshes} meshes, ${(totalVertices / 1000).toFixed(0)}k vertices | ` +
|
|
1440
|
-
`first: ${firstGeometryTime.toFixed(0)}ms, total: ${totalElapsedMs.toFixed(0)}ms`
|
|
1441
|
-
);
|
|
1442
|
-
if (nativeStats) {
|
|
1443
|
-
void logToDesktopTerminal(
|
|
1444
|
-
'info',
|
|
1445
|
-
`[useIfc] Native timings for ${fileName}: scan=${nativeStats.entityScanTimeMs ?? 0}ms lookup=${nativeStats.lookupTimeMs ?? 0}ms preprocess=${nativeStats.preprocessTimeMs ?? 0}ms parse=${nativeStats.parseTimeMs ?? 0}ms geometry=${nativeStats.geometryTimeMs ?? 0}ms total=${nativeStats.totalTimeMs ?? 0}ms`
|
|
1446
|
-
);
|
|
1447
|
-
}
|
|
1448
|
-
if (!metadataParsingStarted) {
|
|
1449
|
-
console.warn('[useIfc] Native large-file mode completed without metadata parsing');
|
|
1450
|
-
void logToDesktopTerminal('warn', `[useIfc] Native large-file mode completed without metadata parsing for ${fileName}`);
|
|
1451
|
-
}
|
|
1452
|
-
if (harnessRequest?.waitForMetadataCompletion) {
|
|
1453
|
-
if (!metadataParsingStarted) {
|
|
1454
|
-
startNativeMetadataParsing();
|
|
1455
|
-
}
|
|
1456
|
-
if (metadataParsingPromise) {
|
|
1457
|
-
await metadataParsingPromise;
|
|
1458
|
-
}
|
|
1459
|
-
if (metadataSnapshotWritePromise) {
|
|
1460
|
-
await metadataSnapshotWritePromise;
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
if (firstVisibleGeometryMs === null && firstAppendGeometryBatchMs !== null) {
|
|
1464
|
-
await new Promise<void>((resolve) => {
|
|
1465
|
-
const fallbackTimer = globalThis.setTimeout(() => {
|
|
1466
|
-
if (firstVisibleGeometryMs === null && loadSessionRef.current === currentSession) {
|
|
1467
|
-
firstVisibleGeometryMs = firstAppendGeometryBatchMs;
|
|
1468
|
-
}
|
|
1469
|
-
resolve();
|
|
1470
|
-
}, 250);
|
|
1471
|
-
requestAnimationFrame(() => {
|
|
1472
|
-
globalThis.clearTimeout(fallbackTimer);
|
|
1473
|
-
if (firstVisibleGeometryMs === null && loadSessionRef.current === currentSession) {
|
|
1474
|
-
firstVisibleGeometryMs = performance.now() - totalStartTime;
|
|
1475
|
-
}
|
|
1476
|
-
resolve();
|
|
1477
|
-
});
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
if (hugeNativeMode) {
|
|
1481
|
-
setLoading(false);
|
|
1482
|
-
}
|
|
1483
|
-
const telemetryElapsedMs = performance.now() - totalStartTime;
|
|
1484
|
-
await finalizeActiveHarnessRun({
|
|
1485
|
-
schemaVersion: 1,
|
|
1486
|
-
source: 'desktop-native',
|
|
1487
|
-
mode: harnessRequest ? 'startup-harness' : 'manual',
|
|
1488
|
-
success: true,
|
|
1489
|
-
runLabel: harnessRequest?.runLabel,
|
|
1490
|
-
cache: {
|
|
1491
|
-
key: nativeCacheKey,
|
|
1492
|
-
hit: nativeGeometryCacheHit,
|
|
1493
|
-
manifestMeshCount: null,
|
|
1494
|
-
manifestShardCount: null,
|
|
1495
|
-
},
|
|
1496
|
-
file: {
|
|
1497
|
-
path: file.path,
|
|
1498
|
-
name: file.name,
|
|
1499
|
-
sizeBytes: file.size,
|
|
1500
|
-
sizeMB: fileSizeMB,
|
|
1501
|
-
},
|
|
1502
|
-
timings: {
|
|
1503
|
-
modelOpenMs,
|
|
1504
|
-
firstBatchWaitMs: firstGeometryTime || null,
|
|
1505
|
-
firstAppendGeometryBatchMs,
|
|
1506
|
-
firstVisibleGeometryMs,
|
|
1507
|
-
streamCompleteMs,
|
|
1508
|
-
totalWallClockMs: telemetryElapsedMs,
|
|
1509
|
-
metadataStartMs,
|
|
1510
|
-
metadataReadCompleteMs,
|
|
1511
|
-
metadataParseStartMs,
|
|
1512
|
-
spatialReadyMs,
|
|
1513
|
-
metadataCompleteMs,
|
|
1514
|
-
metadataFailedMs,
|
|
1515
|
-
metadataReadDurationMs,
|
|
1516
|
-
metadataBufferCopyDurationMs,
|
|
1517
|
-
metadataParseDurationMs,
|
|
1518
|
-
},
|
|
1519
|
-
batches: {
|
|
1520
|
-
estimatedTotal,
|
|
1521
|
-
totalBatches: batchCount,
|
|
1522
|
-
totalMeshes: lastTotalMeshes,
|
|
1523
|
-
firstBatchMeshes: firstNativeBatchTelemetry?.meshCount ?? null,
|
|
1524
|
-
firstPayloadKind: firstNativeBatchTelemetry?.payloadKind ?? null,
|
|
1525
|
-
},
|
|
1526
|
-
nativeStats: nativeStats
|
|
1527
|
-
? {
|
|
1528
|
-
parseTimeMs: nativeStats.parseTimeMs ?? null,
|
|
1529
|
-
entityScanTimeMs: nativeStats.entityScanTimeMs ?? null,
|
|
1530
|
-
lookupTimeMs: nativeStats.lookupTimeMs ?? null,
|
|
1531
|
-
preprocessTimeMs: nativeStats.preprocessTimeMs ?? null,
|
|
1532
|
-
geometryTimeMs: nativeStats.geometryTimeMs ?? null,
|
|
1533
|
-
totalTimeMs: nativeStats.totalTimeMs ?? null,
|
|
1534
|
-
firstChunkReadyTimeMs: nativeStats.firstChunkReadyTimeMs ?? null,
|
|
1535
|
-
firstChunkPackTimeMs: nativeStats.firstChunkPackTimeMs ?? null,
|
|
1536
|
-
firstChunkEmittedTimeMs: nativeStats.firstChunkEmittedTimeMs ?? null,
|
|
1537
|
-
firstChunkEmitTimeMs: nativeStats.firstChunkEmitTimeMs ?? null,
|
|
1538
|
-
}
|
|
1539
|
-
: null,
|
|
1540
|
-
metadata: {
|
|
1541
|
-
started: metadataParsingStarted,
|
|
1542
|
-
metadataStartMs,
|
|
1543
|
-
metadataReadCompleteMs,
|
|
1544
|
-
metadataParseStartMs,
|
|
1545
|
-
spatialReadyMs,
|
|
1546
|
-
metadataCompleteMs,
|
|
1547
|
-
metadataFailedMs,
|
|
1548
|
-
metadataReadDurationMs,
|
|
1549
|
-
metadataBufferCopyDurationMs,
|
|
1550
|
-
metadataParseDurationMs,
|
|
1551
|
-
},
|
|
1552
|
-
firstBatchTelemetry: firstNativeBatchTelemetry
|
|
1553
|
-
? {
|
|
1554
|
-
batchSequence: firstNativeBatchTelemetry.batchSequence,
|
|
1555
|
-
payloadKind: firstNativeBatchTelemetry.payloadKind,
|
|
1556
|
-
meshCount: firstNativeBatchTelemetry.meshCount,
|
|
1557
|
-
positionsLen: firstNativeBatchTelemetry.positionsLen,
|
|
1558
|
-
normalsLen: firstNativeBatchTelemetry.normalsLen,
|
|
1559
|
-
indicesLen: firstNativeBatchTelemetry.indicesLen,
|
|
1560
|
-
rustChunkReadyMs: firstNativeBatchTelemetry.chunkReadyTimeMs,
|
|
1561
|
-
rustPackMs: firstNativeBatchTelemetry.packTimeMs,
|
|
1562
|
-
rustEmittedMs: firstNativeBatchTelemetry.emittedTimeMs,
|
|
1563
|
-
rustEmitMs: firstNativeBatchTelemetry.emitTimeMs,
|
|
1564
|
-
jsReceivedMs: jsFirstChunkReceivedMs,
|
|
1565
|
-
transportToJsMs:
|
|
1566
|
-
jsFirstChunkReceivedMs !== null
|
|
1567
|
-
? jsFirstChunkReceivedMs - firstNativeBatchTelemetry.emittedTimeMs
|
|
1568
|
-
: null,
|
|
1569
|
-
appendAfterReceiveMs:
|
|
1570
|
-
jsFirstChunkReceivedMs !== null && firstAppendGeometryBatchMs !== null
|
|
1571
|
-
? firstAppendGeometryBatchMs - jsFirstChunkReceivedMs
|
|
1572
|
-
: null,
|
|
1573
|
-
visibleAfterAppendMs:
|
|
1574
|
-
firstVisibleGeometryMs !== null && firstAppendGeometryBatchMs !== null
|
|
1575
|
-
? firstVisibleGeometryMs - firstAppendGeometryBatchMs
|
|
1576
|
-
: null,
|
|
1577
|
-
}
|
|
1578
|
-
: null,
|
|
1579
|
-
});
|
|
1580
|
-
if (!hugeNativeMode) {
|
|
1581
|
-
setLoading(false);
|
|
1582
|
-
}
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
371
|
|
|
1586
372
|
// Read file from disk. The browser path streams files ≥
|
|
1587
373
|
// STREAM_SAB_THRESHOLD directly into a SharedArrayBuffer, which avoids
|
|
1588
374
|
// a doubled-peak ArrayBuffer + SAB allocation when the geometry
|
|
1589
|
-
// pipeline copies into its own SAB.
|
|
1590
|
-
// Tauri's Rust IPC because it bounds memory differently. (#600)
|
|
375
|
+
// pipeline copies into its own SAB. (#600)
|
|
1591
376
|
const fileReadStart = performance.now();
|
|
1592
|
-
|
|
1593
|
-
if (isNativeFileHandle(file)) {
|
|
1594
|
-
const nativeBytes = await readNativeFile(file.path);
|
|
1595
|
-
const nativeBuffer = toExactArrayBuffer(nativeBytes);
|
|
1596
|
-
acquired = {
|
|
1597
|
-
buffer: nativeBuffer,
|
|
1598
|
-
view: new Uint8Array(nativeBuffer),
|
|
1599
|
-
isShared: false,
|
|
1600
|
-
};
|
|
1601
|
-
} else {
|
|
1602
|
-
acquired = await acquireFileBuffer(file as File);
|
|
1603
|
-
}
|
|
377
|
+
const acquired: AcquiredBuffer = await acquireFileBuffer(file);
|
|
1604
378
|
// `buffer` retains its previous semantics (ArrayBuffer-shaped) for
|
|
1605
379
|
// every downstream consumer. When `acquired.isShared` is true the
|
|
1606
380
|
// backing store is a SharedArrayBuffer; downstream code only ever
|
|
@@ -1621,13 +395,13 @@ export function useIfcLoader() {
|
|
|
1621
395
|
const renderer = getGlobalRenderer();
|
|
1622
396
|
if (!renderer) {
|
|
1623
397
|
setError('Renderer not initialised — try again after the viewer mounts.');
|
|
1624
|
-
updateModel(
|
|
398
|
+
updateModel(modelId, { loadState: 'error', loadError: 'renderer-missing' });
|
|
1625
399
|
setLoading(false);
|
|
1626
400
|
return;
|
|
1627
401
|
}
|
|
1628
402
|
setProgress({ phase: `Streaming ${format.toUpperCase()}`, percent: 5 });
|
|
1629
403
|
setGeometryStreamingActive(false);
|
|
1630
|
-
const blob =
|
|
404
|
+
const blob = file;
|
|
1631
405
|
const incCount = useViewerStore.getState().incrementPointCloudAssetCount;
|
|
1632
406
|
const ingest = ingestPointCloud({
|
|
1633
407
|
format,
|
|
@@ -1677,17 +451,17 @@ export function useIfcLoader() {
|
|
|
1677
451
|
const isAbort = err instanceof DOMException && err.name === 'AbortError';
|
|
1678
452
|
if (isAbort) {
|
|
1679
453
|
console.log(
|
|
1680
|
-
`[useIfc] pointcloud ingest cancelled (model=${
|
|
454
|
+
`[useIfc] pointcloud ingest cancelled (model=${modelId}, handle=${ingest.rendererHandle.id})`,
|
|
1681
455
|
);
|
|
1682
|
-
updateModel(
|
|
456
|
+
updateModel(modelId, { loadState: 'error', loadError: 'cancelled' });
|
|
1683
457
|
setError(null);
|
|
1684
458
|
setProgress({ phase: 'Cancelled', percent: 0 });
|
|
1685
459
|
} else {
|
|
1686
460
|
console.error(
|
|
1687
|
-
`[useIfc] pointcloud ingest failed (format=${format}, model=${
|
|
461
|
+
`[useIfc] pointcloud ingest failed (format=${format}, model=${modelId}):`,
|
|
1688
462
|
err,
|
|
1689
463
|
);
|
|
1690
|
-
updateModel(
|
|
464
|
+
updateModel(modelId, { loadState: 'error', loadError: message });
|
|
1691
465
|
setError(`${format.toUpperCase()} parsing failed: ${message}`);
|
|
1692
466
|
}
|
|
1693
467
|
clearOwnedCanceller();
|
|
@@ -1702,9 +476,13 @@ export function useIfcLoader() {
|
|
|
1702
476
|
renderer.removePointCloudAsset(ingest.rendererHandle);
|
|
1703
477
|
return;
|
|
1704
478
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
479
|
+
// Primary owns the active-model slots; a federated add must not touch
|
|
480
|
+
// them (finalizeModel's federated branch wires via addModel instead).
|
|
481
|
+
if (target.kind === 'primary') {
|
|
482
|
+
setGeometryResult(ingest.geometryResult);
|
|
483
|
+
setIfcDataStore(ingest.dataStore);
|
|
484
|
+
}
|
|
485
|
+
await finalizeModel(ingest.dataStore, ingest.geometryResult, ingest.schemaVersion, {
|
|
1708
486
|
pointCloudHandleId: ingest.rendererHandle.id,
|
|
1709
487
|
});
|
|
1710
488
|
setProgress({ phase: 'Complete', percent: 100 });
|
|
@@ -1719,9 +497,11 @@ export function useIfcLoader() {
|
|
|
1719
497
|
|
|
1720
498
|
try {
|
|
1721
499
|
const result = await parseIfcxViewerModel(buffer, setProgress);
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
500
|
+
if (target.kind === 'primary') {
|
|
501
|
+
setGeometryResult(result.geometryResult);
|
|
502
|
+
setIfcDataStore(result.dataStore);
|
|
503
|
+
}
|
|
504
|
+
await finalizeModel(result.dataStore, result.geometryResult, result.schemaVersion);
|
|
1725
505
|
|
|
1726
506
|
setProgress({ phase: 'Complete', percent: 100 });
|
|
1727
507
|
setLoading(false);
|
|
@@ -1731,13 +511,13 @@ export function useIfcLoader() {
|
|
|
1731
511
|
console.warn(`[useIfc] IFCX file "${file.name}" has no geometry - this appears to be an overlay file that adds properties to a base model.`);
|
|
1732
512
|
console.warn('[useIfc] To use this file, load it together with a base IFCX file (select both files at once).');
|
|
1733
513
|
setError(`"${file.name}" is an overlay file with no geometry. Please load it together with a base IFCX file (select all files at once).`);
|
|
1734
|
-
updateModel(
|
|
514
|
+
updateModel(modelId, { loadState: 'error', loadError: 'overlay-only-ifcx' });
|
|
1735
515
|
setLoading(false);
|
|
1736
516
|
return;
|
|
1737
517
|
}
|
|
1738
518
|
console.error('[useIfc] IFCX parsing failed:', err);
|
|
1739
519
|
const message = err instanceof Error ? err.message : String(err);
|
|
1740
|
-
updateModel(
|
|
520
|
+
updateModel(modelId, { loadState: 'error', loadError: message });
|
|
1741
521
|
setError(`IFCX parsing failed: ${message}`);
|
|
1742
522
|
setLoading(false);
|
|
1743
523
|
return;
|
|
@@ -1751,9 +531,18 @@ export function useIfcLoader() {
|
|
|
1751
531
|
|
|
1752
532
|
try {
|
|
1753
533
|
const result = await parseGlbViewerModel(buffer);
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
534
|
+
if (target.kind === 'primary') {
|
|
535
|
+
setGeometryResult(result.geometryResult);
|
|
536
|
+
setIfcDataStore(null);
|
|
537
|
+
}
|
|
538
|
+
// Primary keeps the historical null data store (GLB has no entities);
|
|
539
|
+
// a federated add needs the minimal store so finalizeModel can offset
|
|
540
|
+
// ids + register the model (matches the old addModel GLB path).
|
|
541
|
+
await finalizeModel(
|
|
542
|
+
target.kind === 'federated' ? result.dataStore : null,
|
|
543
|
+
result.geometryResult,
|
|
544
|
+
result.schemaVersion,
|
|
545
|
+
);
|
|
1757
546
|
|
|
1758
547
|
setProgress({ phase: 'Complete', percent: 100 });
|
|
1759
548
|
|
|
@@ -1762,7 +551,7 @@ export function useIfcLoader() {
|
|
|
1762
551
|
} catch (err: unknown) {
|
|
1763
552
|
console.error('[useIfc] GLB parsing failed:', err);
|
|
1764
553
|
const message = err instanceof Error ? err.message : String(err);
|
|
1765
|
-
updateModel(
|
|
554
|
+
updateModel(modelId, { loadState: 'error', loadError: message });
|
|
1766
555
|
setError(`GLB parsing failed: ${message}`);
|
|
1767
556
|
setLoading(false);
|
|
1768
557
|
return;
|
|
@@ -1776,14 +565,19 @@ export function useIfcLoader() {
|
|
|
1776
565
|
// persisted key filename-safe and independent of the original filename.
|
|
1777
566
|
const cacheKey = `ifc-${buffer.byteLength}-${fingerprint}-v4`;
|
|
1778
567
|
|
|
1779
|
-
|
|
568
|
+
// Cache + server are PRIMARY-ONLY: a federated add is WASM-only with no
|
|
569
|
+
// cache/server round-trip (matches the former parseStepBufferViewerModel).
|
|
570
|
+
if (target.kind === 'primary' && buffer.byteLength >= CACHE_SIZE_THRESHOLD) {
|
|
1780
571
|
setProgress({ phase: 'Checking cache', percent: 5 });
|
|
1781
572
|
const cacheResult = await getCached(cacheKey);
|
|
1782
573
|
if (cacheResult) {
|
|
1783
|
-
|
|
574
|
+
// Pass the freshly read file buffer as the source fallback: the
|
|
575
|
+
// desktop cache doesn't persist a sourceBuffer, and without one the
|
|
576
|
+
// restored store can't carry the lazy entity accessors.
|
|
577
|
+
const cacheLoadResult = await loadFromCache(cacheResult, file.name, cacheKey, buffer);
|
|
1784
578
|
if (cacheLoadResult.success) {
|
|
1785
579
|
const state = useViewerStore.getState();
|
|
1786
|
-
|
|
580
|
+
await finalizeModel(state.ifcDataStore, state.geometryResult, getSchemaVersion(state.ifcDataStore), {
|
|
1787
581
|
loadState: 'complete',
|
|
1788
582
|
cacheState: 'hit',
|
|
1789
583
|
});
|
|
@@ -1798,12 +592,12 @@ export function useIfcLoader() {
|
|
|
1798
592
|
// Only for IFC4 STEP files (server doesn't support IFCX). Native
|
|
1799
593
|
// file handles (Tauri) don't have an HTTP-uploadable body, so skip
|
|
1800
594
|
// the server path and fall through to the WASM loader.
|
|
1801
|
-
if (format === 'ifc' && USE_SERVER && SERVER_URL && SERVER_URL !== ''
|
|
595
|
+
if (target.kind === 'primary' && format === 'ifc' && USE_SERVER && SERVER_URL && SERVER_URL !== '') {
|
|
1802
596
|
// Pass buffer directly - server uses File object for parsing, buffer is only for size checks
|
|
1803
597
|
const serverSuccess = await loadFromServer(file, buffer, () => loadSessionRef.current !== currentSession);
|
|
1804
598
|
if (serverSuccess) {
|
|
1805
599
|
const state = useViewerStore.getState();
|
|
1806
|
-
|
|
600
|
+
await finalizeModel(state.ifcDataStore, state.geometryResult, getSchemaVersion(state.ifcDataStore));
|
|
1807
601
|
console.log(`[useIfc] TOTAL LOAD TIME (server): ${(performance.now() - totalStartTime).toFixed(0)}ms`);
|
|
1808
602
|
setLoading(false);
|
|
1809
603
|
return;
|
|
@@ -1814,12 +608,11 @@ export function useIfcLoader() {
|
|
|
1814
608
|
|
|
1815
609
|
// Using local WASM parsing
|
|
1816
610
|
setProgress({ phase: 'Starting geometry streaming', percent: 10 });
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
&& file.size < HUGE_NATIVE_FILE_THRESHOLD;
|
|
611
|
+
// Global streaming flag is a PRIMARY (active-model) concern; a federated
|
|
612
|
+
// add must not toggle it (the former federated path never did).
|
|
613
|
+
if (target.kind === 'primary') {
|
|
614
|
+
setGeometryStreamingActive(true);
|
|
615
|
+
}
|
|
1823
616
|
|
|
1824
617
|
// Initialize geometry processor first (WASM init is fast if already loaded)
|
|
1825
618
|
const mergeLayersAtLoad = useViewerStore.getState().mergeLayers;
|
|
@@ -1831,6 +624,11 @@ export function useIfcLoader() {
|
|
|
1831
624
|
mergeLayers: mergeLayersAtLoad,
|
|
1832
625
|
});
|
|
1833
626
|
await geometryProcessor.init();
|
|
627
|
+
// Issue #924: enable RTC-invariant per-entity geometry fingerprints so
|
|
628
|
+
// the model-compare feature can detect geometry changes. The hash rides
|
|
629
|
+
// on each MeshData.geometryHash (and through the worker pool); cost is
|
|
630
|
+
// the O(verts) quantized hash, negligible next to tessellation.
|
|
631
|
+
geometryProcessor.enableGeometryHashes();
|
|
1834
632
|
|
|
1835
633
|
// Allocate (or reuse) a SharedArrayBuffer so the parser worker and
|
|
1836
634
|
// the geometry workers read the same memory zero-copy. When
|
|
@@ -1840,7 +638,7 @@ export function useIfcLoader() {
|
|
|
1840
638
|
// available, AND TextDecoder accepts SAB-backed views (Firefox fails
|
|
1841
639
|
// the third check; we skip the worker path entirely there so the
|
|
1842
640
|
// SAB allocation isn't wasted).
|
|
1843
|
-
const useParserWorker = WorkerParser.isSupported()
|
|
641
|
+
const useParserWorker = WorkerParser.isSupported();
|
|
1844
642
|
let sharedSource: SharedArrayBuffer | null = null;
|
|
1845
643
|
if (useParserWorker) {
|
|
1846
644
|
if (acquired.isShared && acquired.buffer instanceof SharedArrayBuffer) {
|
|
@@ -1881,7 +679,10 @@ export function useIfcLoader() {
|
|
|
1881
679
|
partialStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
1882
680
|
}
|
|
1883
681
|
}
|
|
1884
|
-
setIfcDataStore
|
|
682
|
+
// PRIMARY only: setIfcDataStore writes the ACTIVE model. A federated
|
|
683
|
+
// add must not touch model #1's store — it wires its own via
|
|
684
|
+
// finalizeModel → addModel once dataStorePromise resolves.
|
|
685
|
+
if (target.kind === 'primary') setIfcDataStore(partialStore);
|
|
1885
686
|
};
|
|
1886
687
|
|
|
1887
688
|
const onFullDataStore = (dataStore: IfcDataStore) => {
|
|
@@ -1893,7 +694,10 @@ export function useIfcLoader() {
|
|
|
1893
694
|
dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
1894
695
|
}
|
|
1895
696
|
}
|
|
1896
|
-
|
|
697
|
+
// PRIMARY only (active-model write); federated wires via finalizeModel.
|
|
698
|
+
// resolveDataStore stays unconditional so the federated finalizePromise
|
|
699
|
+
// still resolves and registers the model.
|
|
700
|
+
if (target.kind === 'primary') setIfcDataStore(dataStore);
|
|
1897
701
|
console.log(`[useIfc] Data model parsing complete for ${file.name}: ${metadataCompleteMs.toFixed(0)}ms`);
|
|
1898
702
|
memoryAccounting.endPhase('parser-worker');
|
|
1899
703
|
memoryAccounting.recordPhase({ phase: 'parser-complete' });
|
|
@@ -1904,7 +708,7 @@ export function useIfcLoader() {
|
|
|
1904
708
|
// Same `wasmApi` heuristic as before — desktop loads cannot share
|
|
1905
709
|
// the geometry processor's WASM instance with the parser without
|
|
1906
710
|
// risking corruption.
|
|
1907
|
-
const parserWasmApi =
|
|
711
|
+
const parserWasmApi = geometryProcessor.getApi();
|
|
1908
712
|
return new IfcParser().parseColumnar(buffer, {
|
|
1909
713
|
wasmApi: parserWasmApi ?? undefined,
|
|
1910
714
|
onSpatialReady: onPartialDataStore,
|
|
@@ -1925,7 +729,6 @@ export function useIfcLoader() {
|
|
|
1925
729
|
const ADAPTIVE_SYNC_THRESHOLD_MB = 2;
|
|
1926
730
|
const geometryWillEmitEntityIndex =
|
|
1927
731
|
useParserWorker
|
|
1928
|
-
&& !shouldUseDesktopStableWasmGeometry
|
|
1929
732
|
&& fileSizeMB >= ADAPTIVE_SYNC_THRESHOLD_MB;
|
|
1930
733
|
|
|
1931
734
|
const startDataModelParsing = () => {
|
|
@@ -2001,8 +804,11 @@ export function useIfcLoader() {
|
|
|
2001
804
|
let metadataCompleteMs: number | null = null;
|
|
2002
805
|
let metadataFailedMs: number | null = null;
|
|
2003
806
|
|
|
2004
|
-
// Clear existing geometry result
|
|
2005
|
-
|
|
807
|
+
// Clear existing geometry result — PRIMARY only (federated must not
|
|
808
|
+
// disturb the active model's geometry).
|
|
809
|
+
if (target.kind === 'primary') {
|
|
810
|
+
setGeometryResult(null);
|
|
811
|
+
}
|
|
2006
812
|
|
|
2007
813
|
// Timing instrumentation
|
|
2008
814
|
let batchCount = 0;
|
|
@@ -2025,6 +831,11 @@ export function useIfcLoader() {
|
|
|
2025
831
|
|
|
2026
832
|
// Declare at function scope so the catch block can always reach it.
|
|
2027
833
|
let closeGeometryIterator: (() => Promise<void>) | null = null;
|
|
834
|
+
// The background finalize (spatial index / cache for primary; align +
|
|
835
|
+
// addModel for federated). Primary leaves it running in the background
|
|
836
|
+
// for a fast first frame; federated MUST await it so the model is
|
|
837
|
+
// registered before loadFile resolves (loadFilesSequentially relies on it).
|
|
838
|
+
let finalizePromise: Promise<void> | null = null;
|
|
2028
839
|
|
|
2029
840
|
try {
|
|
2030
841
|
// Use dynamic batch sizing for optimal throughput
|
|
@@ -2033,12 +844,13 @@ export function useIfcLoader() {
|
|
|
2033
844
|
// When the parser worker is in use, hand the geometry workers the
|
|
2034
845
|
// same SAB so we don't pay the file-bytes copy twice.
|
|
2035
846
|
const geometryView = sharedSource ? new Uint8Array(sharedSource) : new Uint8Array(buffer);
|
|
2036
|
-
const geometryEvents =
|
|
2037
|
-
? geometryProcessor.processStreaming(geometryView, undefined, dynamicBatchConfig)
|
|
2038
|
-
: geometryProcessor.processAdaptive(geometryView, {
|
|
847
|
+
const geometryEvents = geometryProcessor.processAdaptive(geometryView, {
|
|
2039
848
|
sizeThreshold: 2 * 1024 * 1024, // 2MB threshold
|
|
2040
849
|
batchSize: dynamicBatchConfig, // Dynamic batches: small first, then large
|
|
2041
850
|
existingSab: sharedSource ?? undefined,
|
|
851
|
+
// Federated adds share the anchor's RTC origin so all models sit in
|
|
852
|
+
// one coordinate space (pixel-perfect alignment, no post-shift).
|
|
853
|
+
sharedRtcOffset: target.kind === 'federated' ? target.sharedRtcOffset : undefined,
|
|
2042
854
|
// Hand the streaming pre-pass's entity index to the parser
|
|
2043
855
|
// worker so it skips a duplicate ~10 s WASM scan. Safe even
|
|
2044
856
|
// when the parser falls back to main-thread (instance is
|
|
@@ -2062,7 +874,7 @@ export function useIfcLoader() {
|
|
|
2062
874
|
|
|
2063
875
|
while (true) {
|
|
2064
876
|
const watchdogMs = getGeometryStreamWatchdogMs(
|
|
2065
|
-
|
|
877
|
+
false,
|
|
2066
878
|
batchCount,
|
|
2067
879
|
fileSizeMB,
|
|
2068
880
|
);
|
|
@@ -2141,29 +953,39 @@ export function useIfcLoader() {
|
|
|
2141
953
|
totalMeshes = event.totalSoFar;
|
|
2142
954
|
lastTotalMeshes = event.totalSoFar;
|
|
2143
955
|
|
|
2144
|
-
|
|
2145
|
-
|
|
956
|
+
if (target.kind === 'primary') {
|
|
957
|
+
// Accumulate meshes for batched rendering
|
|
958
|
+
for (let i = 0; i < event.meshes.length; i++) pendingMeshes.push(event.meshes[i]);
|
|
2146
959
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
960
|
+
// FIRST BATCH: Render immediately for fast first frame
|
|
961
|
+
// SUBSEQUENT: Throttle to reduce React re-renders
|
|
962
|
+
const timeSinceLastRender = eventReceived - lastRenderTime;
|
|
963
|
+
const shouldRender = batchCount === 1 || timeSinceLastRender >= RENDER_INTERVAL_MS;
|
|
2151
964
|
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
965
|
+
if (shouldRender && pendingMeshes.length > 0) {
|
|
966
|
+
if (firstAppendGeometryBatchMs === null) {
|
|
967
|
+
firstAppendGeometryBatchMs = performance.now() - totalStartTime;
|
|
968
|
+
console.log(`[useIfc] First appendGeometryBatch for ${file.name}: ${firstAppendGeometryBatchMs.toFixed(0)}ms`);
|
|
969
|
+
}
|
|
970
|
+
appendGeometryBatch(pendingMeshes, event.coordinateInfo);
|
|
971
|
+
pendingMeshes = [];
|
|
972
|
+
lastRenderTime = eventReceived;
|
|
973
|
+
markFirstVisibleGeometry();
|
|
974
|
+
|
|
975
|
+
// Update progress
|
|
976
|
+
const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal / 10, totalMeshes)) * 45);
|
|
977
|
+
setProgress({
|
|
978
|
+
phase: `Rendering geometry (${totalMeshes} meshes)`,
|
|
979
|
+
percent: progressPercent
|
|
980
|
+
});
|
|
2156
981
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
// Update progress
|
|
2163
|
-
const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal / 10, totalMeshes)) * 45);
|
|
982
|
+
} else {
|
|
983
|
+
// Federated add: accumulate into allMeshes only (done above) and
|
|
984
|
+
// surface progress — it paints atomically at completion via
|
|
985
|
+
// finalizeModel's addModel, never touching the active slot.
|
|
2164
986
|
setProgress({
|
|
2165
|
-
phase: `
|
|
2166
|
-
percent:
|
|
987
|
+
phase: `Processing geometry (${totalMeshes} meshes)`,
|
|
988
|
+
percent: 10 + Math.min(80, (allMeshes.length / 1000) * 0.8),
|
|
2167
989
|
});
|
|
2168
990
|
}
|
|
2169
991
|
|
|
@@ -2171,8 +993,9 @@ export function useIfcLoader() {
|
|
|
2171
993
|
}
|
|
2172
994
|
case 'complete':
|
|
2173
995
|
streamCompleteMs = performance.now() - totalStartTime;
|
|
2174
|
-
// Flush
|
|
2175
|
-
|
|
996
|
+
// Flush remaining pending meshes — PRIMARY only. A federated add
|
|
997
|
+
// never pushed to pendingMeshes; it paints atomically at finalize.
|
|
998
|
+
if (target.kind === 'primary' && pendingMeshes.length > 0) {
|
|
2176
999
|
if (firstAppendGeometryBatchMs === null) {
|
|
2177
1000
|
firstAppendGeometryBatchMs = performance.now() - totalStartTime;
|
|
2178
1001
|
console.log(`[useIfc] First appendGeometryBatch for ${file.name}: ${firstAppendGeometryBatchMs.toFixed(0)}ms`);
|
|
@@ -2184,40 +1007,58 @@ export function useIfcLoader() {
|
|
|
2184
1007
|
|
|
2185
1008
|
finalCoordinateInfo = event.coordinateInfo ?? null;
|
|
2186
1009
|
|
|
2187
|
-
//
|
|
2188
|
-
// No need to start it here — it runs concurrently with geometry.
|
|
2189
|
-
|
|
2190
|
-
// Apply all accumulated color updates in a single store update
|
|
2191
|
-
// instead of one updateMeshColors() call per colorUpdate event.
|
|
2192
|
-
if (cumulativeColorUpdates.size > 0) {
|
|
2193
|
-
updateMeshColors(cumulativeColorUpdates);
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
// Store captured RTC offset in coordinate info for multi-model alignment
|
|
1010
|
+
// Store captured RTC offset in coordinate info for multi-model alignment.
|
|
2197
1011
|
if (finalCoordinateInfo && capturedRtcOffset) {
|
|
2198
1012
|
finalCoordinateInfo.wasmRtcOffset = capturedRtcOffset;
|
|
2199
1013
|
}
|
|
2200
1014
|
|
|
2201
|
-
|
|
2202
|
-
|
|
1015
|
+
if (target.kind === 'primary') {
|
|
1016
|
+
// Active-model writes — PRIMARY only. Federated meshes already
|
|
1017
|
+
// carry colours (applied during streaming) and their coordinate
|
|
1018
|
+
// info rides the geometryResult handed to addModel at finalize.
|
|
1019
|
+
if (cumulativeColorUpdates.size > 0) {
|
|
1020
|
+
updateMeshColors(cumulativeColorUpdates);
|
|
1021
|
+
}
|
|
1022
|
+
updateCoordinateInfo(finalCoordinateInfo);
|
|
1023
|
+
}
|
|
2203
1024
|
|
|
2204
1025
|
setProgress({ phase: 'Complete', percent: 100 });
|
|
2205
1026
|
memoryAccounting.endPhase('geometry');
|
|
2206
1027
|
memoryAccounting.recordPhase({ phase: 'geometry-complete' });
|
|
2207
1028
|
console.log(memoryAccounting.formatSummary());
|
|
2208
1029
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
2209
|
-
if (loadSessionRef.current === currentSession) {
|
|
1030
|
+
if (loadSessionRef.current === currentSession && target.kind === 'primary') {
|
|
2210
1031
|
setGeometryStreamingActive(false);
|
|
2211
1032
|
}
|
|
2212
1033
|
console.log(`[useIfc] Geometry streaming complete: ${batchCount} batches, ${lastTotalMeshes} meshes`);
|
|
2213
1034
|
console.log(`[useIfc] Stream complete for ${file.name}: ${streamCompleteMs.toFixed(0)}ms`);
|
|
2214
1035
|
|
|
2215
|
-
//
|
|
2216
|
-
|
|
2217
|
-
dataStorePromise.then(async dataStore => {
|
|
1036
|
+
// Finalize once the data model is ready (parses in parallel).
|
|
1037
|
+
finalizePromise = dataStorePromise.then(async dataStore => {
|
|
2218
1038
|
// Guard: skip if user loaded a new file since this load started
|
|
2219
1039
|
if (loadSessionRef.current !== currentSession) return;
|
|
2220
|
-
|
|
1040
|
+
|
|
1041
|
+
if (target.kind === 'federated') {
|
|
1042
|
+
// Build the model's geometryResult from the accumulated meshes —
|
|
1043
|
+
// federated never streamed into the active slot — and hand it to
|
|
1044
|
+
// finalizeModel, which aligns, offsets ids, builds the spatial
|
|
1045
|
+
// index, and registers the model via addModel. NOT cached (the
|
|
1046
|
+
// former federated path never cached); allMeshes stays alive as
|
|
1047
|
+
// the model's geometryResult.meshes, so it is NOT cleared.
|
|
1048
|
+
applyColorUpdatesToMeshes(allMeshes, cumulativeColorUpdates);
|
|
1049
|
+
const federatedGeometry: GeometryResult = {
|
|
1050
|
+
meshes: allMeshes,
|
|
1051
|
+
totalVertices: allMeshes.reduce((sum, m) => sum + m.positions.length / 3, 0),
|
|
1052
|
+
totalTriangles: allMeshes.reduce((sum, m) => sum + m.indices.length / 3, 0),
|
|
1053
|
+
coordinateInfo: finalCoordinateInfo ?? createCoordinateInfo(calculateMeshBounds(allMeshes).bounds),
|
|
1054
|
+
};
|
|
1055
|
+
await finalizeModel(dataStore, federatedGeometry, getSchemaVersion(dataStore), {
|
|
1056
|
+
loadState: 'complete',
|
|
1057
|
+
});
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
await finalizeModel(dataStore, useViewerStore.getState().geometryResult, getSchemaVersion(dataStore), {
|
|
2221
1062
|
loadState: 'complete',
|
|
2222
1063
|
cacheState: buffer.byteLength >= CACHE_SIZE_THRESHOLD ? 'writing' : 'none',
|
|
2223
1064
|
});
|
|
@@ -2261,10 +1102,19 @@ export function useIfcLoader() {
|
|
|
2261
1102
|
}).catch(err => {
|
|
2262
1103
|
// Data model parsing failed - spatial index and caching skipped
|
|
2263
1104
|
console.warn('[useIfc] Skipping spatial index/cache - data model unavailable:', err);
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
1105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1106
|
+
if (target.kind === 'federated') {
|
|
1107
|
+
// No placeholder model exists for a federated add (it is only
|
|
1108
|
+
// registered on success via finalizeModel→addModel), so
|
|
1109
|
+
// updateModel would no-op and the failure would vanish —
|
|
1110
|
+
// addModel just returns null. Surface it to the user instead.
|
|
1111
|
+
toast.error(`Failed to load "${file.name}": ${message}`);
|
|
1112
|
+
} else {
|
|
1113
|
+
updateModel(modelId, {
|
|
1114
|
+
loadState: 'error',
|
|
1115
|
+
loadError: message,
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
2268
1118
|
});
|
|
2269
1119
|
break;
|
|
2270
1120
|
}
|
|
@@ -2275,6 +1125,11 @@ export function useIfcLoader() {
|
|
|
2275
1125
|
if (closeGeometryIterator) {
|
|
2276
1126
|
await closeGeometryIterator();
|
|
2277
1127
|
}
|
|
1128
|
+
// The parser worker may be parked in `waitForEntityIndex` (the aborted
|
|
1129
|
+
// geometry pre-pass would have unblocked it); it self-terminates on its
|
|
1130
|
+
// own watchdog. Swallow the now-orphaned dataStorePromise rejection so
|
|
1131
|
+
// it doesn't surface as an unhandled rejection.
|
|
1132
|
+
void dataStorePromise.catch(() => {});
|
|
2278
1133
|
if (loadSessionRef.current !== currentSession) return;
|
|
2279
1134
|
console.error('[useIfc] Error in processing:', err);
|
|
2280
1135
|
setError(err instanceof Error ? err.message : 'Unknown error during geometry processing');
|
|
@@ -2285,6 +1140,14 @@ export function useIfcLoader() {
|
|
|
2285
1140
|
|
|
2286
1141
|
if (loadSessionRef.current !== currentSession) return;
|
|
2287
1142
|
|
|
1143
|
+
// Federated adds register the model inside finalizePromise (georef align
|
|
1144
|
+
// → id offset → spatial index → addModel). Await it so loadFile resolves
|
|
1145
|
+
// only AFTER the model is in the map — loadFilesSequentially loads the
|
|
1146
|
+
// next file serially and relies on this ordering for id-offset assignment.
|
|
1147
|
+
if (target.kind === 'federated' && finalizePromise) {
|
|
1148
|
+
await finalizePromise;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
2288
1151
|
if (firstVisibleGeometryMs === null && firstAppendGeometryBatchMs !== null) {
|
|
2289
1152
|
await new Promise<void>((resolve) => {
|
|
2290
1153
|
const fallbackTimer = globalThis.setTimeout(() => {
|
|
@@ -2314,41 +1177,10 @@ export function useIfcLoader() {
|
|
|
2314
1177
|
setGeometryStreamingActive(false);
|
|
2315
1178
|
} catch (err) {
|
|
2316
1179
|
if (loadSessionRef.current !== currentSession) return;
|
|
2317
|
-
updateModel(
|
|
1180
|
+
updateModel(modelId, {
|
|
2318
1181
|
loadState: 'error',
|
|
2319
1182
|
loadError: err instanceof Error ? err.message : String(err),
|
|
2320
1183
|
});
|
|
2321
|
-
if (isNativeFileHandle(file)) {
|
|
2322
|
-
const harnessRequest = getActiveHarnessRequest();
|
|
2323
|
-
await finalizeActiveHarnessRun({
|
|
2324
|
-
schemaVersion: 1,
|
|
2325
|
-
source: 'desktop-native',
|
|
2326
|
-
mode: harnessRequest ? 'startup-harness' : 'manual',
|
|
2327
|
-
success: false,
|
|
2328
|
-
runLabel: harnessRequest?.runLabel,
|
|
2329
|
-
cache: {
|
|
2330
|
-
key: computeNativeCacheKey(file),
|
|
2331
|
-
hit: null,
|
|
2332
|
-
manifestMeshCount: null,
|
|
2333
|
-
manifestShardCount: null,
|
|
2334
|
-
},
|
|
2335
|
-
file: {
|
|
2336
|
-
path: file.path,
|
|
2337
|
-
name: file.name,
|
|
2338
|
-
sizeBytes: file.size,
|
|
2339
|
-
sizeMB: file.size / (1024 * 1024),
|
|
2340
|
-
},
|
|
2341
|
-
timings: {
|
|
2342
|
-
totalWallClockMs: performance.now() - totalStartTime,
|
|
2343
|
-
},
|
|
2344
|
-
batches: {},
|
|
2345
|
-
nativeStats: null,
|
|
2346
|
-
metadata: null,
|
|
2347
|
-
firstBatchTelemetry: null,
|
|
2348
|
-
error: err instanceof Error ? err.message : String(err),
|
|
2349
|
-
});
|
|
2350
|
-
}
|
|
2351
|
-
void logToDesktopTerminal('error', `[useIfc] Load failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2352
1184
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
2353
1185
|
setLoading(false);
|
|
2354
1186
|
setGeometryStreamingActive(false);
|