@ifc-lite/viewer 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-v3mcCUPN.css +0 -1
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook for loading and processing IFC files (single-model path)
|
|
7
|
+
* Handles format detection, WASM geometry streaming, IFC parsing,
|
|
8
|
+
* cache management, and server-side parsing delegation
|
|
9
|
+
*
|
|
10
|
+
* Extracted from useIfc.ts for better separation of concerns
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback } from 'react';
|
|
14
|
+
import { useViewerStore } from '../store.js';
|
|
15
|
+
import { IfcParser, detectFormat, parseIfcx, type IfcDataStore } from '@ifc-lite/parser';
|
|
16
|
+
import { GeometryProcessor, GeometryQuality, type MeshData, type CoordinateInfo } from '@ifc-lite/geometry';
|
|
17
|
+
import { buildSpatialIndex } from '@ifc-lite/spatial';
|
|
18
|
+
import { type GeometryData, loadGLBToMeshData } from '@ifc-lite/cache';
|
|
19
|
+
|
|
20
|
+
import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, getDynamicBatchConfig } from '../utils/ifcConfig.js';
|
|
21
|
+
import {
|
|
22
|
+
calculateMeshBounds,
|
|
23
|
+
createCoordinateInfo,
|
|
24
|
+
getRenderIntervalMs,
|
|
25
|
+
calculateStoreyHeights,
|
|
26
|
+
normalizeColor,
|
|
27
|
+
} from '../utils/localParsingUtils.js';
|
|
28
|
+
|
|
29
|
+
// Cache hook
|
|
30
|
+
import { useIfcCache, getCached } from './useIfcCache.js';
|
|
31
|
+
|
|
32
|
+
// Server hook
|
|
33
|
+
import { useIfcServer } from './useIfcServer.js';
|
|
34
|
+
|
|
35
|
+
// Import IfcxDataStore type from federation hook
|
|
36
|
+
import type { IfcxDataStore } from './useIfcFederation.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute a fast content fingerprint from the first and last 4KB of a buffer.
|
|
40
|
+
* Uses FNV-1a hash for speed — no crypto overhead, sufficient to distinguish
|
|
41
|
+
* files with identical name and byte length.
|
|
42
|
+
*/
|
|
43
|
+
function computeFastFingerprint(buffer: ArrayBuffer): string {
|
|
44
|
+
const CHUNK_SIZE = 4096;
|
|
45
|
+
const view = new Uint8Array(buffer);
|
|
46
|
+
const len = view.length;
|
|
47
|
+
|
|
48
|
+
// FNV-1a hash
|
|
49
|
+
let hash = 2166136261; // FNV offset basis (32-bit)
|
|
50
|
+
const firstEnd = Math.min(CHUNK_SIZE, len);
|
|
51
|
+
for (let i = 0; i < firstEnd; i++) {
|
|
52
|
+
hash ^= view[i];
|
|
53
|
+
hash = Math.imul(hash, 16777619); // FNV prime
|
|
54
|
+
}
|
|
55
|
+
if (len > CHUNK_SIZE) {
|
|
56
|
+
const lastStart = Math.max(CHUNK_SIZE, len - CHUNK_SIZE);
|
|
57
|
+
for (let i = lastStart; i < len; i++) {
|
|
58
|
+
hash ^= view[i];
|
|
59
|
+
hash = Math.imul(hash, 16777619);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return (hash >>> 0).toString(16);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Hook providing file loading operations for single-model path
|
|
67
|
+
* Includes binary cache support for fast subsequent loads
|
|
68
|
+
*/
|
|
69
|
+
export function useIfcLoader() {
|
|
70
|
+
const {
|
|
71
|
+
setLoading,
|
|
72
|
+
setError,
|
|
73
|
+
setProgress,
|
|
74
|
+
setIfcDataStore,
|
|
75
|
+
setGeometryResult,
|
|
76
|
+
appendGeometryBatch,
|
|
77
|
+
updateMeshColors,
|
|
78
|
+
updateCoordinateInfo,
|
|
79
|
+
} = useViewerStore();
|
|
80
|
+
|
|
81
|
+
// Cache operations from extracted hook
|
|
82
|
+
const { loadFromCache, saveToCache } = useIfcCache();
|
|
83
|
+
|
|
84
|
+
// Server operations from extracted hook
|
|
85
|
+
const { loadFromServer } = useIfcServer();
|
|
86
|
+
|
|
87
|
+
const loadFile = useCallback(async (file: File) => {
|
|
88
|
+
const { resetViewerState, clearAllModels } = useViewerStore.getState();
|
|
89
|
+
|
|
90
|
+
// Track total elapsed time for complete user experience
|
|
91
|
+
const totalStartTime = performance.now();
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Reset all viewer state before loading new file
|
|
95
|
+
// Also clear models Map to ensure clean single-file state
|
|
96
|
+
resetViewerState();
|
|
97
|
+
clearAllModels();
|
|
98
|
+
|
|
99
|
+
setLoading(true);
|
|
100
|
+
setError(null);
|
|
101
|
+
setProgress({ phase: 'Loading file', percent: 0 });
|
|
102
|
+
|
|
103
|
+
// Read file from disk
|
|
104
|
+
const buffer = await file.arrayBuffer();
|
|
105
|
+
const fileSizeMB = buffer.byteLength / (1024 * 1024);
|
|
106
|
+
|
|
107
|
+
// Detect file format (IFCX/IFC5 vs IFC4 STEP vs GLB)
|
|
108
|
+
const format = detectFormat(buffer);
|
|
109
|
+
|
|
110
|
+
// IFCX files must be parsed client-side (server only supports IFC4 STEP)
|
|
111
|
+
if (format === 'ifcx') {
|
|
112
|
+
setProgress({ phase: 'Parsing IFCX (client-side)', percent: 10 });
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const ifcxResult = await parseIfcx(buffer, {
|
|
116
|
+
onProgress: (prog: { phase: string; percent: number }) => {
|
|
117
|
+
setProgress({ phase: `IFCX ${prog.phase}`, percent: 10 + (prog.percent * 0.8) });
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Convert IFCX meshes to viewer format
|
|
122
|
+
// Note: IFCX geometry extractor already handles Y-up to Z-up conversion
|
|
123
|
+
// and applies transforms correctly in Z-up space, so we just pass through
|
|
124
|
+
|
|
125
|
+
const meshes: MeshData[] = ifcxResult.meshes.map((m: { expressId?: number; express_id?: number; id?: number; positions: Float32Array | number[]; indices: Uint32Array | number[]; normals: Float32Array | number[]; color?: [number, number, number, number] | [number, number, number]; ifcType?: string; ifc_type?: string }) => {
|
|
126
|
+
// IFCX MeshData has: expressId, ifcType, positions (Float32Array), indices (Uint32Array), normals (Float32Array), color
|
|
127
|
+
const positions = m.positions instanceof Float32Array ? m.positions : new Float32Array(m.positions || []);
|
|
128
|
+
const indices = m.indices instanceof Uint32Array ? m.indices : new Uint32Array(m.indices || []);
|
|
129
|
+
const normals = m.normals instanceof Float32Array ? m.normals : new Float32Array(m.normals || []);
|
|
130
|
+
|
|
131
|
+
// Normalize color to RGBA format (4 elements)
|
|
132
|
+
const color = normalizeColor(m.color);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
expressId: m.expressId ?? m.express_id ?? m.id ?? 0,
|
|
136
|
+
positions,
|
|
137
|
+
indices,
|
|
138
|
+
normals,
|
|
139
|
+
color,
|
|
140
|
+
ifcType: m.ifcType || m.ifc_type || 'IfcProduct',
|
|
141
|
+
};
|
|
142
|
+
}).filter((m: MeshData) => m.positions.length > 0 && m.indices.length > 0); // Filter out empty meshes
|
|
143
|
+
|
|
144
|
+
// Check if this is an overlay-only file (no geometry)
|
|
145
|
+
if (meshes.length === 0) {
|
|
146
|
+
console.warn(`[useIfc] IFCX file "${file.name}" has no geometry - this appears to be an overlay file that adds properties to a base model.`);
|
|
147
|
+
console.warn('[useIfc] To use this file, load it together with a base IFCX file (select both files at once).');
|
|
148
|
+
|
|
149
|
+
// Check if file has data references that suggest it's an overlay
|
|
150
|
+
const hasReferences = ifcxResult.entityCount > 0;
|
|
151
|
+
if (hasReferences) {
|
|
152
|
+
setError(`"${file.name}" is an overlay file with no geometry. Please load it together with a base IFCX file (select all files at once).`);
|
|
153
|
+
setLoading(false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Calculate bounds and statistics
|
|
159
|
+
const { bounds, stats } = calculateMeshBounds(meshes);
|
|
160
|
+
const coordinateInfo = createCoordinateInfo(bounds);
|
|
161
|
+
|
|
162
|
+
setGeometryResult({
|
|
163
|
+
meshes,
|
|
164
|
+
totalVertices: stats.totalVertices,
|
|
165
|
+
totalTriangles: stats.totalTriangles,
|
|
166
|
+
coordinateInfo,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Convert IFCX data model to IfcDataStore format
|
|
170
|
+
// IFCX already provides entities, properties, quantities, relationships, spatialHierarchy
|
|
171
|
+
const dataStore = {
|
|
172
|
+
fileSize: ifcxResult.fileSize,
|
|
173
|
+
schemaVersion: 'IFC5' as const,
|
|
174
|
+
entityCount: ifcxResult.entityCount,
|
|
175
|
+
parseTime: ifcxResult.parseTime,
|
|
176
|
+
source: new Uint8Array(buffer),
|
|
177
|
+
entityIndex: {
|
|
178
|
+
byId: new Map(),
|
|
179
|
+
byType: new Map(),
|
|
180
|
+
},
|
|
181
|
+
strings: ifcxResult.strings,
|
|
182
|
+
entities: ifcxResult.entities,
|
|
183
|
+
properties: ifcxResult.properties,
|
|
184
|
+
quantities: ifcxResult.quantities,
|
|
185
|
+
relationships: ifcxResult.relationships,
|
|
186
|
+
spatialHierarchy: ifcxResult.spatialHierarchy,
|
|
187
|
+
} as IfcxDataStore;
|
|
188
|
+
|
|
189
|
+
// IfcxDataStore extends IfcDataStore (with schemaVersion: 'IFC5'), so this is safe
|
|
190
|
+
setIfcDataStore(dataStore);
|
|
191
|
+
|
|
192
|
+
setProgress({ phase: 'Complete', percent: 100 });
|
|
193
|
+
setLoading(false);
|
|
194
|
+
return;
|
|
195
|
+
} catch (err: unknown) {
|
|
196
|
+
console.error('[useIfc] IFCX parsing failed:', err);
|
|
197
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
198
|
+
setError(`IFCX parsing failed: ${message}`);
|
|
199
|
+
setLoading(false);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// GLB files: parse directly to MeshData (no data model, geometry only)
|
|
205
|
+
if (format === 'glb') {
|
|
206
|
+
setProgress({ phase: 'Parsing GLB', percent: 10 });
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const meshes = loadGLBToMeshData(new Uint8Array(buffer));
|
|
210
|
+
|
|
211
|
+
if (meshes.length === 0) {
|
|
212
|
+
setError('GLB file contains no geometry');
|
|
213
|
+
setLoading(false);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const { bounds, stats } = calculateMeshBounds(meshes);
|
|
218
|
+
const coordinateInfo = createCoordinateInfo(bounds);
|
|
219
|
+
|
|
220
|
+
setGeometryResult({
|
|
221
|
+
meshes,
|
|
222
|
+
totalVertices: stats.totalVertices,
|
|
223
|
+
totalTriangles: stats.totalTriangles,
|
|
224
|
+
coordinateInfo,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// GLB files have no IFC data model - set a minimal store
|
|
228
|
+
setIfcDataStore(null);
|
|
229
|
+
|
|
230
|
+
setProgress({ phase: 'Complete', percent: 100 });
|
|
231
|
+
|
|
232
|
+
const totalElapsedMs = performance.now() - totalStartTime;
|
|
233
|
+
console.log(`[useIfc] GLB loaded: ${meshes.length} meshes, ${stats.totalTriangles} triangles in ${totalElapsedMs.toFixed(0)}ms`);
|
|
234
|
+
setLoading(false);
|
|
235
|
+
return;
|
|
236
|
+
} catch (err: unknown) {
|
|
237
|
+
console.error('[useIfc] GLB parsing failed:', err);
|
|
238
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
239
|
+
setError(`GLB parsing failed: ${message}`);
|
|
240
|
+
setLoading(false);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Cache key uses filename + size + content fingerprint + format version
|
|
246
|
+
// Fingerprint prevents collisions for different files with the same name and size
|
|
247
|
+
const fingerprint = computeFastFingerprint(buffer);
|
|
248
|
+
const cacheKey = `${file.name}-${buffer.byteLength}-${fingerprint}-v4`;
|
|
249
|
+
|
|
250
|
+
if (buffer.byteLength >= CACHE_SIZE_THRESHOLD) {
|
|
251
|
+
setProgress({ phase: 'Checking cache', percent: 5 });
|
|
252
|
+
const cacheResult = await getCached(cacheKey);
|
|
253
|
+
if (cacheResult) {
|
|
254
|
+
const success = await loadFromCache(cacheResult, file.name, cacheKey);
|
|
255
|
+
if (success) {
|
|
256
|
+
const totalElapsedMs = performance.now() - totalStartTime;
|
|
257
|
+
console.log(`[useIfc] TOTAL LOAD TIME (from cache): ${totalElapsedMs.toFixed(0)}ms (${(totalElapsedMs / 1000).toFixed(1)}s)`);
|
|
258
|
+
setLoading(false);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Try server parsing first (enabled by default for multi-core performance)
|
|
265
|
+
// Only for IFC4 STEP files (server doesn't support IFCX)
|
|
266
|
+
if (format === 'ifc' && USE_SERVER && SERVER_URL && SERVER_URL !== '') {
|
|
267
|
+
// Pass buffer directly - server uses File object for parsing, buffer is only for size checks
|
|
268
|
+
const serverSuccess = await loadFromServer(file, buffer);
|
|
269
|
+
if (serverSuccess) {
|
|
270
|
+
const totalElapsedMs = performance.now() - totalStartTime;
|
|
271
|
+
console.log(`[useIfc] TOTAL LOAD TIME (server): ${totalElapsedMs.toFixed(0)}ms (${(totalElapsedMs / 1000).toFixed(1)}s)`);
|
|
272
|
+
setLoading(false);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Server not available - continue with local WASM (no error logging needed)
|
|
276
|
+
} else if (format === 'unknown') {
|
|
277
|
+
console.warn('[useIfc] Unknown file format - attempting to parse as IFC4 STEP');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Using local WASM parsing
|
|
281
|
+
setProgress({ phase: 'Starting geometry streaming', percent: 10 });
|
|
282
|
+
|
|
283
|
+
// Initialize geometry processor first (WASM init is fast if already loaded)
|
|
284
|
+
const geometryProcessor = new GeometryProcessor({
|
|
285
|
+
quality: GeometryQuality.Balanced
|
|
286
|
+
});
|
|
287
|
+
await geometryProcessor.init();
|
|
288
|
+
|
|
289
|
+
// DEFER data model parsing - start it AFTER geometry streaming begins
|
|
290
|
+
// This ensures geometry gets first crack at the CPU for fast first frame
|
|
291
|
+
// Data model parsing is lower priority - UI can work without it initially
|
|
292
|
+
let resolveDataStore: (dataStore: IfcDataStore) => void;
|
|
293
|
+
let rejectDataStore: (err: unknown) => void;
|
|
294
|
+
const dataStorePromise = new Promise<IfcDataStore>((resolve, reject) => {
|
|
295
|
+
resolveDataStore = resolve;
|
|
296
|
+
rejectDataStore = reject;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const startDataModelParsing = () => {
|
|
300
|
+
// Use main thread - worker parsing disabled (IfcDataStore has closures that can't be serialized)
|
|
301
|
+
const parser = new IfcParser();
|
|
302
|
+
const wasmApi = geometryProcessor.getApi();
|
|
303
|
+
parser.parseColumnar(buffer, {
|
|
304
|
+
wasmApi, // Pass WASM API for 5-10x faster entity scanning
|
|
305
|
+
}).then(dataStore => {
|
|
306
|
+
|
|
307
|
+
// Calculate storey heights from elevation differences if not already populated
|
|
308
|
+
if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
309
|
+
const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
|
|
310
|
+
for (const [storeyId, height] of calculatedHeights) {
|
|
311
|
+
dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
setIfcDataStore(dataStore);
|
|
316
|
+
resolveDataStore(dataStore);
|
|
317
|
+
}).catch(err => {
|
|
318
|
+
console.error('[useIfc] Data model parsing failed:', err);
|
|
319
|
+
rejectDataStore(err);
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Schedule data model parsing to start after geometry begins streaming
|
|
324
|
+
setTimeout(startDataModelParsing, 0);
|
|
325
|
+
|
|
326
|
+
// Use adaptive processing: sync for small files, streaming for large files
|
|
327
|
+
let estimatedTotal = 0;
|
|
328
|
+
let totalMeshes = 0;
|
|
329
|
+
const allMeshes: MeshData[] = []; // Collect all meshes for BVH building
|
|
330
|
+
let finalCoordinateInfo: CoordinateInfo | null = null;
|
|
331
|
+
// Capture RTC offset from WASM for proper multi-model alignment
|
|
332
|
+
let capturedRtcOffset: { x: number; y: number; z: number } | null = null;
|
|
333
|
+
|
|
334
|
+
// Clear existing geometry result
|
|
335
|
+
setGeometryResult(null);
|
|
336
|
+
|
|
337
|
+
// Timing instrumentation
|
|
338
|
+
const processingStart = performance.now();
|
|
339
|
+
let batchCount = 0;
|
|
340
|
+
let lastBatchTime = processingStart;
|
|
341
|
+
let totalWaitTime = 0; // Time waiting for WASM to yield batches
|
|
342
|
+
let totalProcessTime = 0; // Time processing batches in JS
|
|
343
|
+
let firstGeometryTime = 0; // Time to first rendered geometry
|
|
344
|
+
|
|
345
|
+
// OPTIMIZATION: Accumulate meshes and batch state updates
|
|
346
|
+
// First batch renders immediately, then accumulate for throughput
|
|
347
|
+
// Adaptive interval: larger files get less frequent updates to reduce React re-render overhead
|
|
348
|
+
let pendingMeshes: MeshData[] = [];
|
|
349
|
+
let lastRenderTime = 0;
|
|
350
|
+
const RENDER_INTERVAL_MS = getRenderIntervalMs(fileSizeMB);
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// Use dynamic batch sizing for optimal throughput
|
|
354
|
+
const dynamicBatchConfig = getDynamicBatchConfig(fileSizeMB);
|
|
355
|
+
|
|
356
|
+
for await (const event of geometryProcessor.processAdaptive(new Uint8Array(buffer), {
|
|
357
|
+
sizeThreshold: 2 * 1024 * 1024, // 2MB threshold
|
|
358
|
+
batchSize: dynamicBatchConfig, // Dynamic batches: small first, then large
|
|
359
|
+
})) {
|
|
360
|
+
const eventReceived = performance.now();
|
|
361
|
+
const waitTime = eventReceived - lastBatchTime;
|
|
362
|
+
|
|
363
|
+
switch (event.type) {
|
|
364
|
+
case 'start':
|
|
365
|
+
estimatedTotal = event.totalEstimate;
|
|
366
|
+
break;
|
|
367
|
+
case 'model-open':
|
|
368
|
+
setProgress({ phase: 'Processing geometry', percent: 50 });
|
|
369
|
+
break;
|
|
370
|
+
case 'colorUpdate': {
|
|
371
|
+
// Update colors for already-rendered meshes
|
|
372
|
+
updateMeshColors(event.updates);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case 'rtcOffset': {
|
|
376
|
+
// Capture RTC offset from WASM for multi-model alignment
|
|
377
|
+
if (event.hasRtc) {
|
|
378
|
+
capturedRtcOffset = event.rtcOffset;
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case 'batch': {
|
|
383
|
+
batchCount++;
|
|
384
|
+
totalWaitTime += waitTime;
|
|
385
|
+
|
|
386
|
+
// Track time to first geometry
|
|
387
|
+
if (batchCount === 1) {
|
|
388
|
+
firstGeometryTime = performance.now() - totalStartTime;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const processStart = performance.now();
|
|
392
|
+
|
|
393
|
+
// Collect meshes for BVH building (use loop to avoid stack overflow with large batches)
|
|
394
|
+
for (let i = 0; i < event.meshes.length; i++) allMeshes.push(event.meshes[i]);
|
|
395
|
+
finalCoordinateInfo = event.coordinateInfo ?? null;
|
|
396
|
+
totalMeshes = event.totalSoFar;
|
|
397
|
+
|
|
398
|
+
// Accumulate meshes for batched rendering
|
|
399
|
+
for (let i = 0; i < event.meshes.length; i++) pendingMeshes.push(event.meshes[i]);
|
|
400
|
+
|
|
401
|
+
// FIRST BATCH: Render immediately for fast first frame
|
|
402
|
+
// SUBSEQUENT: Throttle to reduce React re-renders
|
|
403
|
+
const timeSinceLastRender = eventReceived - lastRenderTime;
|
|
404
|
+
const shouldRender = batchCount === 1 || timeSinceLastRender >= RENDER_INTERVAL_MS;
|
|
405
|
+
|
|
406
|
+
if (shouldRender && pendingMeshes.length > 0) {
|
|
407
|
+
appendGeometryBatch(pendingMeshes, event.coordinateInfo);
|
|
408
|
+
pendingMeshes = [];
|
|
409
|
+
lastRenderTime = eventReceived;
|
|
410
|
+
|
|
411
|
+
// Update progress
|
|
412
|
+
const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal / 10, totalMeshes)) * 45);
|
|
413
|
+
setProgress({
|
|
414
|
+
phase: `Rendering geometry (${totalMeshes} meshes)`,
|
|
415
|
+
percent: progressPercent
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const processTime = performance.now() - processStart;
|
|
420
|
+
totalProcessTime += processTime;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case 'complete':
|
|
424
|
+
// Flush any remaining pending meshes
|
|
425
|
+
if (pendingMeshes.length > 0) {
|
|
426
|
+
appendGeometryBatch(pendingMeshes, event.coordinateInfo);
|
|
427
|
+
pendingMeshes = [];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
finalCoordinateInfo = event.coordinateInfo ?? null;
|
|
431
|
+
|
|
432
|
+
// Store captured RTC offset in coordinate info for multi-model alignment
|
|
433
|
+
if (finalCoordinateInfo && capturedRtcOffset) {
|
|
434
|
+
finalCoordinateInfo.wasmRtcOffset = capturedRtcOffset;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Update geometry result with final coordinate info
|
|
438
|
+
updateCoordinateInfo(finalCoordinateInfo);
|
|
439
|
+
|
|
440
|
+
setProgress({ phase: 'Complete', percent: 100 });
|
|
441
|
+
|
|
442
|
+
// Build spatial index and cache in background (non-blocking)
|
|
443
|
+
// Wait for data model to complete first
|
|
444
|
+
dataStorePromise.then(dataStore => {
|
|
445
|
+
// Build spatial index from meshes (in background)
|
|
446
|
+
if (allMeshes.length > 0) {
|
|
447
|
+
const buildIndex = () => {
|
|
448
|
+
try {
|
|
449
|
+
const spatialIndex = buildSpatialIndex(allMeshes);
|
|
450
|
+
dataStore.spatialIndex = spatialIndex;
|
|
451
|
+
setIfcDataStore({ ...dataStore });
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.warn('[useIfc] Failed to build spatial index:', err);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// Use requestIdleCallback if available (type assertion for optional browser API)
|
|
458
|
+
if ('requestIdleCallback' in window) {
|
|
459
|
+
(window as { requestIdleCallback: (cb: () => void, opts?: { timeout: number }) => void }).requestIdleCallback(buildIndex, { timeout: 2000 });
|
|
460
|
+
} else {
|
|
461
|
+
setTimeout(buildIndex, 100);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Cache the result in the background (for files above threshold)
|
|
466
|
+
if (buffer.byteLength >= CACHE_SIZE_THRESHOLD && allMeshes.length > 0 && finalCoordinateInfo) {
|
|
467
|
+
const geometryData: GeometryData = {
|
|
468
|
+
meshes: allMeshes,
|
|
469
|
+
totalVertices: allMeshes.reduce((sum, m) => sum + m.positions.length / 3, 0),
|
|
470
|
+
totalTriangles: allMeshes.reduce((sum, m) => sum + m.indices.length / 3, 0),
|
|
471
|
+
coordinateInfo: finalCoordinateInfo,
|
|
472
|
+
};
|
|
473
|
+
saveToCache(cacheKey, dataStore, geometryData, buffer, file.name);
|
|
474
|
+
}
|
|
475
|
+
}).catch(err => {
|
|
476
|
+
// Data model parsing failed - spatial index and caching skipped
|
|
477
|
+
console.warn('[useIfc] Skipping spatial index/cache - data model unavailable:', err);
|
|
478
|
+
});
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
lastBatchTime = performance.now();
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
console.error('[useIfc] Error in processing:', err);
|
|
486
|
+
setError(err instanceof Error ? err.message : 'Unknown error during geometry processing');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Log developer-friendly summary with key metrics
|
|
490
|
+
const totalElapsedMs = performance.now() - totalStartTime;
|
|
491
|
+
const totalVertices = allMeshes.reduce((sum, m) => sum + m.positions.length / 3, 0);
|
|
492
|
+
console.log(
|
|
493
|
+
`[useIfc] ✓ ${file.name} (${fileSizeMB.toFixed(1)}MB) → ` +
|
|
494
|
+
`${allMeshes.length} meshes, ${(totalVertices / 1000).toFixed(0)}k vertices | ` +
|
|
495
|
+
`first: ${firstGeometryTime.toFixed(0)}ms, total: ${totalElapsedMs.toFixed(0)}ms`
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
setLoading(false);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
501
|
+
setLoading(false);
|
|
502
|
+
}
|
|
503
|
+
}, [setLoading, setError, setProgress, setIfcDataStore, setGeometryResult, appendGeometryBatch, updateMeshColors, updateCoordinateInfo, loadFromCache, saveToCache, loadFromServer]);
|
|
504
|
+
|
|
505
|
+
return { loadFile };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export default useIfcLoader;
|