@ifc-lite/viewer 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +46 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +15 -0
- package/dist/assets/{Arrow.dom--gdrQd-q.js → Arrow.dom-CcoDLP6E.js} +1 -1
- package/dist/assets/{basketViewActivator-CI3y6VYQ.js → basketViewActivator-FtbS__bG.js} +1 -1
- package/dist/assets/{browser-vWDubxDI.js → browser-CXd3z0DO.js} +1 -1
- package/dist/assets/ifc-lite-TI3u_Zyw.js +7 -0
- package/dist/assets/ifc-lite_bg-DeZrXTKQ.wasm +0 -0
- package/dist/assets/index-Ba4eoTe7.css +1 -0
- package/dist/assets/{index-BImINgzG.js → index-D99fzcwI.js} +28962 -26947
- package/dist/assets/{index-RXIK18da.js → index-DqNiuQep.js} +4 -4
- package/dist/assets/{native-bridge-4rLidc3f.js → native-bridge-DjDj2M6p.js} +1 -1
- package/dist/assets/{wasm-bridge-BkfXfw8O.js → wasm-bridge-CDTF4ZQc.js} +1 -1
- package/dist/assets/workerHelpers-G7llXNMi.js +36 -0
- package/dist/index.html +2 -2
- package/package.json +15 -14
- package/src/components/viewer/BCFPanel.tsx +12 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +315 -154
- package/src/components/viewer/CommandPalette.tsx +0 -6
- package/src/components/viewer/DataConnector.tsx +489 -284
- package/src/components/viewer/ExportDialog.tsx +66 -6
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +44 -1
- package/src/components/viewer/MainToolbar.tsx +1 -5
- package/src/components/viewer/Viewport.tsx +42 -56
- package/src/components/viewer/ViewportContainer.tsx +3 -0
- package/src/components/viewer/ViewportOverlays.tsx +12 -10
- package/src/components/viewer/bcf/BCFOverlay.tsx +254 -0
- package/src/components/viewer/lists/ListPanel.tsx +0 -21
- package/src/components/viewer/lists/ListResultsTable.tsx +93 -5
- package/src/components/viewer/measureHandlers.ts +558 -0
- package/src/components/viewer/mouseHandlerTypes.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +86 -0
- package/src/components/viewer/useAnimationLoop.ts +116 -44
- package/src/components/viewer/useGeometryStreaming.ts +155 -367
- package/src/components/viewer/useKeyboardControls.ts +30 -46
- package/src/components/viewer/useMouseControls.ts +169 -695
- package/src/components/viewer/useRenderUpdates.ts +9 -59
- package/src/components/viewer/useTouchControls.ts +55 -40
- package/src/hooks/bcfIdLookup.ts +70 -0
- package/src/hooks/useBCF.ts +12 -31
- package/src/hooks/useIfcCache.ts +2 -20
- package/src/hooks/useIfcFederation.ts +5 -11
- package/src/hooks/useIfcLoader.ts +47 -56
- package/src/hooks/useIfcServer.ts +9 -1
- package/src/hooks/useKeyboardShortcuts.ts +0 -10
- package/src/hooks/useLatestRef.ts +24 -0
- package/src/sdk/adapters/export-adapter.ts +2 -2
- package/src/sdk/adapters/model-adapter.ts +1 -0
- package/src/sdk/local-backend.ts +2 -0
- package/src/store/basketVisibleSet.ts +12 -0
- package/src/store/slices/bcfSlice.ts +9 -0
- package/src/utils/loadingUtils.ts +46 -0
- package/src/utils/serverDataModel.ts +4 -3
- package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
- package/dist/assets/index-ax1X2WPd.css +0 -1
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
* Extracted from useIfc.ts for better separation of concerns
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { useCallback } from 'react';
|
|
13
|
+
import { useCallback, useRef } from 'react';
|
|
14
14
|
import { useShallow } from 'zustand/react/shallow';
|
|
15
15
|
import { useViewerStore } from '../store.js';
|
|
16
16
|
import { IfcParser, detectFormat, parseIfcx, type IfcDataStore } from '@ifc-lite/parser';
|
|
17
17
|
import { GeometryProcessor, GeometryQuality, type MeshData, type CoordinateInfo } from '@ifc-lite/geometry';
|
|
18
|
-
import {
|
|
18
|
+
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
19
19
|
import { type GeometryData, loadGLBToMeshData } from '@ifc-lite/cache';
|
|
20
20
|
|
|
21
21
|
import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, CACHE_MAX_SOURCE_SIZE, getDynamicBatchConfig } from '../utils/ifcConfig.js';
|
|
@@ -69,6 +69,10 @@ function computeFastFingerprint(buffer: ArrayBuffer): string {
|
|
|
69
69
|
* Includes binary cache support for fast subsequent loads
|
|
70
70
|
*/
|
|
71
71
|
export function useIfcLoader() {
|
|
72
|
+
// Guard against stale async writes when user loads a new file before previous completes.
|
|
73
|
+
// Incremented on each loadFile call; deferred callbacks check their captured session.
|
|
74
|
+
const loadSessionRef = useRef(0);
|
|
75
|
+
|
|
72
76
|
const {
|
|
73
77
|
setLoading,
|
|
74
78
|
setError,
|
|
@@ -97,6 +101,7 @@ export function useIfcLoader() {
|
|
|
97
101
|
|
|
98
102
|
const loadFile = useCallback(async (file: File) => {
|
|
99
103
|
const { resetViewerState, clearAllModels } = useViewerStore.getState();
|
|
104
|
+
const currentSession = ++loadSessionRef.current;
|
|
100
105
|
|
|
101
106
|
// Track total elapsed time for complete user experience
|
|
102
107
|
const totalStartTime = performance.now();
|
|
@@ -243,8 +248,6 @@ export function useIfcLoader() {
|
|
|
243
248
|
|
|
244
249
|
setProgress({ phase: 'Complete', percent: 100 });
|
|
245
250
|
|
|
246
|
-
const totalElapsedMs = performance.now() - totalStartTime;
|
|
247
|
-
console.log(`[useIfc] GLB loaded: ${meshes.length} meshes, ${stats.totalTriangles} triangles in ${totalElapsedMs.toFixed(0)}ms`);
|
|
248
251
|
setLoading(false);
|
|
249
252
|
return;
|
|
250
253
|
} catch (err: unknown) {
|
|
@@ -267,8 +270,7 @@ export function useIfcLoader() {
|
|
|
267
270
|
if (cacheResult) {
|
|
268
271
|
const success = await loadFromCache(cacheResult, file.name, cacheKey);
|
|
269
272
|
if (success) {
|
|
270
|
-
|
|
271
|
-
console.log(`[useIfc] TOTAL LOAD TIME (from cache): ${totalElapsedMs.toFixed(0)}ms (${(totalElapsedMs / 1000).toFixed(1)}s)`);
|
|
273
|
+
console.log(`[useIfc] TOTAL LOAD TIME (from cache): ${(performance.now() - totalStartTime).toFixed(0)}ms`);
|
|
272
274
|
setLoading(false);
|
|
273
275
|
return;
|
|
274
276
|
}
|
|
@@ -279,10 +281,9 @@ export function useIfcLoader() {
|
|
|
279
281
|
// Only for IFC4 STEP files (server doesn't support IFCX)
|
|
280
282
|
if (format === 'ifc' && USE_SERVER && SERVER_URL && SERVER_URL !== '') {
|
|
281
283
|
// Pass buffer directly - server uses File object for parsing, buffer is only for size checks
|
|
282
|
-
const serverSuccess = await loadFromServer(file, buffer);
|
|
284
|
+
const serverSuccess = await loadFromServer(file, buffer, () => loadSessionRef.current !== currentSession);
|
|
283
285
|
if (serverSuccess) {
|
|
284
|
-
|
|
285
|
-
console.log(`[useIfc] TOTAL LOAD TIME (server): ${totalElapsedMs.toFixed(0)}ms (${(totalElapsedMs / 1000).toFixed(1)}s)`);
|
|
286
|
+
console.log(`[useIfc] TOTAL LOAD TIME (server): ${(performance.now() - totalStartTime).toFixed(0)}ms`);
|
|
286
287
|
setLoading(false);
|
|
287
288
|
return;
|
|
288
289
|
}
|
|
@@ -300,9 +301,10 @@ export function useIfcLoader() {
|
|
|
300
301
|
});
|
|
301
302
|
await geometryProcessor.init();
|
|
302
303
|
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
304
|
+
// Data model parsing runs IN PARALLEL with geometry streaming.
|
|
305
|
+
// Entity scanning uses a Web Worker (non-blocking, ~1.2s).
|
|
306
|
+
// Columnar parse uses time-sliced yielding (~2.3s, 60fps maintained).
|
|
307
|
+
// Neither depends on geometry output — both just need the raw buffer.
|
|
306
308
|
let resolveDataStore: (dataStore: IfcDataStore) => void;
|
|
307
309
|
let rejectDataStore: (err: unknown) => void;
|
|
308
310
|
const dataStorePromise = new Promise<IfcDataStore>((resolve, reject) => {
|
|
@@ -311,13 +313,25 @@ export function useIfcLoader() {
|
|
|
311
313
|
});
|
|
312
314
|
|
|
313
315
|
const startDataModelParsing = () => {
|
|
314
|
-
// Use main thread - worker parsing disabled (IfcDataStore has closures that can't be serialized)
|
|
315
316
|
const parser = new IfcParser();
|
|
317
|
+
// wasmApi as fallback if Web Worker unavailable
|
|
316
318
|
const wasmApi = geometryProcessor.getApi();
|
|
317
319
|
parser.parseColumnar(buffer, {
|
|
318
|
-
wasmApi,
|
|
320
|
+
wasmApi,
|
|
321
|
+
// Emit spatial hierarchy EARLY — lets the panel render while
|
|
322
|
+
// property/association parsing continues (~0.5-1s earlier).
|
|
323
|
+
onSpatialReady: (partialStore) => {
|
|
324
|
+
if (loadSessionRef.current !== currentSession) return;
|
|
325
|
+
if (partialStore.spatialHierarchy && partialStore.spatialHierarchy.storeyHeights.size === 0 && partialStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
326
|
+
const calculatedHeights = calculateStoreyHeights(partialStore.spatialHierarchy.storeyElevations);
|
|
327
|
+
for (const [storeyId, height] of calculatedHeights) {
|
|
328
|
+
partialStore.spatialHierarchy.storeyHeights.set(storeyId, height);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
setIfcDataStore(partialStore);
|
|
332
|
+
},
|
|
319
333
|
}).then(dataStore => {
|
|
320
|
-
|
|
334
|
+
if (loadSessionRef.current !== currentSession) return;
|
|
321
335
|
// Calculate storey heights from elevation differences if not already populated
|
|
322
336
|
if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
|
|
323
337
|
const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
|
|
@@ -326,6 +340,7 @@ export function useIfcLoader() {
|
|
|
326
340
|
}
|
|
327
341
|
}
|
|
328
342
|
|
|
343
|
+
// Update with full data (includes property/association maps)
|
|
329
344
|
setIfcDataStore(dataStore);
|
|
330
345
|
resolveDataStore(dataStore);
|
|
331
346
|
}).catch(err => {
|
|
@@ -334,9 +349,10 @@ export function useIfcLoader() {
|
|
|
334
349
|
});
|
|
335
350
|
};
|
|
336
351
|
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
352
|
+
// Start data model parsing IMMEDIATELY — runs in parallel with geometry.
|
|
353
|
+
// Entity scan uses Web Worker (off main thread), columnar parse yields
|
|
354
|
+
// every ~4ms to maintain 60fps navigation during geometry streaming.
|
|
355
|
+
setTimeout(startDataModelParsing, 0);
|
|
340
356
|
|
|
341
357
|
// Use adaptive processing: sync for small files, streaming for large files
|
|
342
358
|
let estimatedTotal = 0;
|
|
@@ -352,11 +368,7 @@ export function useIfcLoader() {
|
|
|
352
368
|
setGeometryResult(null);
|
|
353
369
|
|
|
354
370
|
// Timing instrumentation
|
|
355
|
-
const processingStart = performance.now();
|
|
356
371
|
let batchCount = 0;
|
|
357
|
-
let lastBatchTime = processingStart;
|
|
358
|
-
let totalWaitTime = 0; // Time waiting for WASM to yield batches
|
|
359
|
-
let totalProcessTime = 0; // Time processing batches in JS
|
|
360
372
|
let firstGeometryTime = 0; // Time to first rendered geometry
|
|
361
373
|
let modelOpenMs = 0;
|
|
362
374
|
let lastTotalMeshes = 0;
|
|
@@ -377,7 +389,6 @@ export function useIfcLoader() {
|
|
|
377
389
|
batchSize: dynamicBatchConfig, // Dynamic batches: small first, then large
|
|
378
390
|
})) {
|
|
379
391
|
const eventReceived = performance.now();
|
|
380
|
-
const waitTime = eventReceived - lastBatchTime;
|
|
381
392
|
|
|
382
393
|
switch (event.type) {
|
|
383
394
|
case 'start':
|
|
@@ -410,7 +421,6 @@ export function useIfcLoader() {
|
|
|
410
421
|
}
|
|
411
422
|
case 'batch': {
|
|
412
423
|
batchCount++;
|
|
413
|
-
totalWaitTime += waitTime;
|
|
414
424
|
|
|
415
425
|
// Track time to first geometry
|
|
416
426
|
if (batchCount === 1) {
|
|
@@ -418,7 +428,6 @@ export function useIfcLoader() {
|
|
|
418
428
|
console.log(`[useIfc] Batch #1: ${event.meshes.length} meshes, wait: ${firstGeometryTime.toFixed(0)}ms`);
|
|
419
429
|
}
|
|
420
430
|
|
|
421
|
-
const processStart = performance.now();
|
|
422
431
|
|
|
423
432
|
// Collect meshes for BVH building (use loop to avoid stack overflow with large batches)
|
|
424
433
|
for (let i = 0; i < event.meshes.length; i++) allMeshes.push(event.meshes[i]);
|
|
@@ -447,8 +456,6 @@ export function useIfcLoader() {
|
|
|
447
456
|
});
|
|
448
457
|
}
|
|
449
458
|
|
|
450
|
-
const processTime = performance.now() - processStart;
|
|
451
|
-
totalProcessTime += processTime;
|
|
452
459
|
break;
|
|
453
460
|
}
|
|
454
461
|
case 'complete':
|
|
@@ -460,11 +467,8 @@ export function useIfcLoader() {
|
|
|
460
467
|
|
|
461
468
|
finalCoordinateInfo = event.coordinateInfo ?? null;
|
|
462
469
|
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
// synchronously calls scanEntitiesFast() which blocks the main
|
|
466
|
-
// thread for ~7s on large files (487MB → 8.4M entities).
|
|
467
|
-
setTimeout(startDataModelParsing, 0);
|
|
470
|
+
// Data model parsing already started in parallel (see above).
|
|
471
|
+
// No need to start it here — it runs concurrently with geometry.
|
|
468
472
|
|
|
469
473
|
// Apply all accumulated color updates in a single store update
|
|
470
474
|
// instead of one updateMeshColors() call per colorUpdate event.
|
|
@@ -482,31 +486,17 @@ export function useIfcLoader() {
|
|
|
482
486
|
|
|
483
487
|
setProgress({ phase: 'Complete', percent: 100 });
|
|
484
488
|
console.log(`[useIfc] Geometry streaming complete: ${batchCount} batches, ${lastTotalMeshes} meshes`);
|
|
485
|
-
console.log(`Total wait (WASM): ${totalWaitTime.toFixed(0)}ms`);
|
|
486
|
-
console.log(`Total process (JS): ${totalProcessTime.toFixed(0)}ms`);
|
|
487
489
|
|
|
488
490
|
// Build spatial index and cache in background (non-blocking)
|
|
489
491
|
// Wait for data model to complete first
|
|
490
492
|
dataStorePromise.then(dataStore => {
|
|
491
|
-
//
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
} catch (err) {
|
|
499
|
-
console.warn('[useIfc] Failed to build spatial index:', err);
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
// Use requestIdleCallback if available (type assertion for optional browser API)
|
|
504
|
-
if ('requestIdleCallback' in window) {
|
|
505
|
-
(window as { requestIdleCallback: (cb: () => void, opts?: { timeout: number }) => void }).requestIdleCallback(buildIndex, { timeout: 2000 });
|
|
506
|
-
} else {
|
|
507
|
-
setTimeout(buildIndex, 100);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
493
|
+
// Guard: skip if user loaded a new file since this load started
|
|
494
|
+
if (loadSessionRef.current !== currentSession) return;
|
|
495
|
+
// Build spatial index from meshes in time-sliced chunks (non-blocking).
|
|
496
|
+
// Previously this was synchronous inside requestIdleCallback, blocking
|
|
497
|
+
// the main thread for seconds on 200K+ mesh models (190M+ float reads
|
|
498
|
+
// for bounds computation alone).
|
|
499
|
+
buildSpatialIndexGuarded(allMeshes, dataStore, setIfcDataStore);
|
|
510
500
|
|
|
511
501
|
// Cache the result in the background (files between 10 MB and 150 MB).
|
|
512
502
|
// Files above CACHE_MAX_SOURCE_SIZE are not cached because the
|
|
@@ -536,14 +526,15 @@ export function useIfcLoader() {
|
|
|
536
526
|
break;
|
|
537
527
|
}
|
|
538
528
|
|
|
539
|
-
lastBatchTime = performance.now();
|
|
540
529
|
}
|
|
541
530
|
} catch (err) {
|
|
531
|
+
if (loadSessionRef.current !== currentSession) return;
|
|
542
532
|
console.error('[useIfc] Error in processing:', err);
|
|
543
533
|
setError(err instanceof Error ? err.message : 'Unknown error during geometry processing');
|
|
544
534
|
}
|
|
545
535
|
|
|
546
|
-
|
|
536
|
+
if (loadSessionRef.current !== currentSession) return;
|
|
537
|
+
|
|
547
538
|
const totalElapsedMs = performance.now() - totalStartTime;
|
|
548
539
|
const totalVertices = allMeshes.reduce((sum, m) => sum + m.positions.length / 3, 0);
|
|
549
540
|
console.log(
|
|
@@ -552,9 +543,9 @@ export function useIfcLoader() {
|
|
|
552
543
|
`first: ${firstGeometryTime.toFixed(0)}ms, total: ${totalElapsedMs.toFixed(0)}ms`
|
|
553
544
|
);
|
|
554
545
|
console.log(`[useIfc] TOTAL LOAD TIME (local): ${totalElapsedMs.toFixed(0)}ms (${(totalElapsedMs / 1000).toFixed(1)}s)`);
|
|
555
|
-
|
|
556
546
|
setLoading(false);
|
|
557
547
|
} catch (err) {
|
|
548
|
+
if (loadSessionRef.current !== currentSession) return;
|
|
558
549
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
559
550
|
setLoading(false);
|
|
560
551
|
}
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
|
|
38
38
|
// Server data model conversion
|
|
39
39
|
import { convertServerDataModel, type ServerParseResult } from '../utils/serverDataModel.js';
|
|
40
|
+
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
40
41
|
|
|
41
42
|
/** Convert server mesh data (snake_case) to viewer format (camelCase) */
|
|
42
43
|
function convertServerMesh(m: ServerMeshData): MeshData {
|
|
@@ -125,7 +126,9 @@ export function useIfcServer() {
|
|
|
125
126
|
*/
|
|
126
127
|
const loadFromServer = useCallback(async (
|
|
127
128
|
file: File,
|
|
128
|
-
buffer: ArrayBuffer
|
|
129
|
+
buffer: ArrayBuffer,
|
|
130
|
+
/** Optional staleness check — returns true if this load has been superseded. */
|
|
131
|
+
isStale?: () => boolean,
|
|
129
132
|
): Promise<boolean> => {
|
|
130
133
|
const { setProgress, setIfcDataStore, setGeometryResult } = useViewerStore.getState();
|
|
131
134
|
try {
|
|
@@ -140,6 +143,7 @@ export function useIfcServer() {
|
|
|
140
143
|
return false; // Silently fall back - caller handles logging
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
if (isStale?.()) return false;
|
|
143
147
|
setProgress({ phase: 'Processing on server (parallel)', percent: 15 });
|
|
144
148
|
|
|
145
149
|
// Check if Parquet is supported (requires parquet-wasm)
|
|
@@ -397,6 +401,7 @@ export function useIfcServer() {
|
|
|
397
401
|
|
|
398
402
|
// Start data model fetch in background - don't block rendering
|
|
399
403
|
(async () => {
|
|
404
|
+
if (isStale?.()) return;
|
|
400
405
|
setProgress({ phase: 'Fetching data model', percent: 85 });
|
|
401
406
|
const dataModelStart = performance.now();
|
|
402
407
|
|
|
@@ -436,9 +441,12 @@ export function useIfcServer() {
|
|
|
436
441
|
allMeshes
|
|
437
442
|
);
|
|
438
443
|
|
|
444
|
+
if (isStale?.()) return;
|
|
439
445
|
setIfcDataStore(dataStore);
|
|
440
446
|
console.log('[useIfc] ✅ Property panel ready with server data model');
|
|
441
447
|
console.log(`[useIfc] Data model loaded in ${(performance.now() - dataModelStart).toFixed(0)}ms (background)`);
|
|
448
|
+
|
|
449
|
+
buildSpatialIndexGuarded(allMeshes, dataStore, setIfcDataStore);
|
|
442
450
|
} catch (err) {
|
|
443
451
|
console.warn('[useIfc] Failed to decode data model:', err);
|
|
444
452
|
console.log('[useIfc] ⚡ Skipping data model (decoding failed)');
|
|
@@ -76,14 +76,6 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
|
76
76
|
e.preventDefault();
|
|
77
77
|
setActiveTool('select');
|
|
78
78
|
}
|
|
79
|
-
if (key === 'p' && !ctrl && !shift) {
|
|
80
|
-
e.preventDefault();
|
|
81
|
-
setActiveTool('pan');
|
|
82
|
-
}
|
|
83
|
-
if (key === 'o' && !ctrl && !shift) {
|
|
84
|
-
e.preventDefault();
|
|
85
|
-
setActiveTool('orbit');
|
|
86
|
-
}
|
|
87
79
|
if (key === 'c' && !ctrl && !shift) {
|
|
88
80
|
e.preventDefault();
|
|
89
81
|
setActiveTool('walk');
|
|
@@ -249,8 +241,6 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
|
|
|
249
241
|
// Export shortcut definitions for UI display
|
|
250
242
|
export const KEYBOARD_SHORTCUTS = [
|
|
251
243
|
{ key: 'V', description: 'Select tool', category: 'Tools' },
|
|
252
|
-
{ key: 'P', description: 'Pan tool', category: 'Tools' },
|
|
253
|
-
{ key: 'O', description: 'Orbit tool', category: 'Tools' },
|
|
254
244
|
{ key: 'C', description: 'Walk mode', category: 'Tools' },
|
|
255
245
|
{ key: 'M', description: 'Measure tool', category: 'Tools' },
|
|
256
246
|
{ key: 'X', description: 'Section tool', category: 'Tools' },
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
import { useRef, useEffect, type MutableRefObject } from 'react';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Keep a ref in sync with a value on every render.
|
|
9
|
+
*
|
|
10
|
+
* This replaces the common pattern of:
|
|
11
|
+
* const fooRef = useRef(foo);
|
|
12
|
+
* useEffect(() => { fooRef.current = foo; }, [foo]);
|
|
13
|
+
*
|
|
14
|
+
* The ref is updated synchronously during render (before effects),
|
|
15
|
+
* so event handlers and animation loops always see the latest value
|
|
16
|
+
* without needing to be re-created.
|
|
17
|
+
*/
|
|
18
|
+
export function useLatestRef<T>(value: T): MutableRefObject<T> {
|
|
19
|
+
const ref = useRef(value);
|
|
20
|
+
// Update synchronously during render — no useEffect needed.
|
|
21
|
+
// This is safe because we're only writing to a ref, not causing side effects.
|
|
22
|
+
ref.current = value;
|
|
23
|
+
return ref;
|
|
24
|
+
}
|
|
@@ -369,7 +369,7 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
|
|
|
369
369
|
return exporter.export(exportOptions).content;
|
|
370
370
|
},
|
|
371
371
|
|
|
372
|
-
download(content: string, filename: string, mimeType?: string) {
|
|
372
|
+
download(content: string | Uint8Array, filename: string, mimeType?: string) {
|
|
373
373
|
triggerDownload(content, filename, mimeType ?? 'text/plain');
|
|
374
374
|
return undefined;
|
|
375
375
|
},
|
|
@@ -377,7 +377,7 @@ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
/** Trigger a browser file download */
|
|
380
|
-
function triggerDownload(content: string, filename: string, mimeType: string): void {
|
|
380
|
+
function triggerDownload(content: string | Uint8Array, filename: string, mimeType: string): void {
|
|
381
381
|
if (typeof document === 'undefined') {
|
|
382
382
|
throw new Error('download() requires a browser environment (document is unavailable)');
|
|
383
383
|
}
|
|
@@ -15,6 +15,7 @@ export function createModelAdapter(store: StoreApi): ModelBackendMethods {
|
|
|
15
15
|
result.push({
|
|
16
16
|
id: model.id,
|
|
17
17
|
name: model.name,
|
|
18
|
+
schema: model.schemaVersion,
|
|
18
19
|
schemaVersion: model.schemaVersion,
|
|
19
20
|
entityCount: model.ifcDataStore?.entities?.count ?? 0,
|
|
20
21
|
fileSize: model.fileSize,
|
package/src/sdk/local-backend.ts
CHANGED
|
@@ -82,6 +82,7 @@ export class LocalBackend implements BimBackend {
|
|
|
82
82
|
model: {
|
|
83
83
|
id: model.id,
|
|
84
84
|
name: model.name,
|
|
85
|
+
schema: model.schemaVersion,
|
|
85
86
|
schemaVersion: model.schemaVersion,
|
|
86
87
|
entityCount: model.ifcDataStore?.entities?.count ?? 0,
|
|
87
88
|
fileSize: model.fileSize,
|
|
@@ -96,6 +97,7 @@ export class LocalBackend implements BimBackend {
|
|
|
96
97
|
model: {
|
|
97
98
|
id: LEGACY_MODEL_ID,
|
|
98
99
|
name: 'Model',
|
|
100
|
+
schema: state.ifcDataStore.schemaVersion ?? 'IFC4',
|
|
99
101
|
schemaVersion: state.ifcDataStore.schemaVersion ?? 'IFC4',
|
|
100
102
|
entityCount: state.ifcDataStore.entities?.count ?? 0,
|
|
101
103
|
fileSize: state.ifcDataStore.source?.byteLength ?? 0,
|
|
@@ -54,6 +54,15 @@ function digestModelEntityMap(map: Map<string, Set<number>>): string {
|
|
|
54
54
|
|
|
55
55
|
function visibilityFingerprint(state: ViewerStateSnapshot): string {
|
|
56
56
|
const tv = state.typeVisibility;
|
|
57
|
+
|
|
58
|
+
// Include per-model visible flag and geometry mesh count so the cache
|
|
59
|
+
// invalidates when model visibility is toggled or geometry finishes loading.
|
|
60
|
+
const modelParts: string[] = [];
|
|
61
|
+
for (const [modelId, model] of state.models) {
|
|
62
|
+
modelParts.push(`${modelId}:${model.visible ? 1 : 0}:${model.geometryResult?.meshes?.length ?? 0}`);
|
|
63
|
+
}
|
|
64
|
+
modelParts.sort();
|
|
65
|
+
|
|
57
66
|
return [
|
|
58
67
|
digestNumberSet(state.hiddenEntities),
|
|
59
68
|
state.isolatedEntities ? digestNumberSet(state.isolatedEntities) : 'none',
|
|
@@ -61,10 +70,13 @@ function visibilityFingerprint(state: ViewerStateSnapshot): string {
|
|
|
61
70
|
digestNumberSet(state.lensHiddenIds),
|
|
62
71
|
digestModelEntityMap(state.hiddenEntitiesByModel),
|
|
63
72
|
digestModelEntityMap(state.isolatedEntitiesByModel),
|
|
73
|
+
digestNumberSet(state.selectedStoreys),
|
|
64
74
|
tv.spaces ? 1 : 0,
|
|
65
75
|
tv.openings ? 1 : 0,
|
|
66
76
|
tv.site ? 1 : 0,
|
|
67
77
|
state.models.size,
|
|
78
|
+
modelParts.join(';'),
|
|
79
|
+
state.geometryResult?.meshes?.length ?? 0,
|
|
68
80
|
state.activeBasketViewId ?? 'none',
|
|
69
81
|
].join(':');
|
|
70
82
|
}
|
|
@@ -35,6 +35,8 @@ export interface BCFSliceState {
|
|
|
35
35
|
bcfError: string | null;
|
|
36
36
|
/** Default author for new topics/comments */
|
|
37
37
|
bcfAuthor: string;
|
|
38
|
+
/** Whether 3D overlay markers are shown in the viewport */
|
|
39
|
+
bcfOverlayVisible: boolean;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface BCFSlice extends BCFSliceState {
|
|
@@ -65,6 +67,8 @@ export interface BCFSlice extends BCFSliceState {
|
|
|
65
67
|
setBcfLoading: (loading: boolean) => void;
|
|
66
68
|
setBcfError: (error: string | null) => void;
|
|
67
69
|
setBcfAuthor: (author: string) => void;
|
|
70
|
+
setBcfOverlayVisible: (visible: boolean) => void;
|
|
71
|
+
toggleBcfOverlay: () => void;
|
|
68
72
|
|
|
69
73
|
// Utility getters
|
|
70
74
|
getActiveTopic: () => BCFTopic | null;
|
|
@@ -102,6 +106,7 @@ export const createBcfSlice: StateCreator<BCFSlice, [], [], BCFSlice> = (set, ge
|
|
|
102
106
|
bcfLoading: false,
|
|
103
107
|
bcfError: null,
|
|
104
108
|
bcfAuthor: getDefaultBcfAuthor(),
|
|
109
|
+
bcfOverlayVisible: false,
|
|
105
110
|
|
|
106
111
|
// Project actions
|
|
107
112
|
setBcfProject: (bcfProject) => set({
|
|
@@ -350,6 +355,10 @@ export const createBcfSlice: StateCreator<BCFSlice, [], [], BCFSlice> = (set, ge
|
|
|
350
355
|
set({ bcfAuthor });
|
|
351
356
|
},
|
|
352
357
|
|
|
358
|
+
setBcfOverlayVisible: (bcfOverlayVisible) => set({ bcfOverlayVisible }),
|
|
359
|
+
|
|
360
|
+
toggleBcfOverlay: () => set((state) => ({ bcfOverlayVisible: !state.bcfOverlayVisible })),
|
|
361
|
+
|
|
353
362
|
// Utility getters
|
|
354
363
|
getActiveTopic: () => {
|
|
355
364
|
const state = get();
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
* Shared loading utilities used across all IFC loading hooks.
|
|
7
|
+
*
|
|
8
|
+
* Consolidates the guarded spatial-index build pattern that was
|
|
9
|
+
* duplicated across useIfcLoader, useIfcCache, useIfcServer, and
|
|
10
|
+
* useIfcFederation.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { MeshData } from '@ifc-lite/geometry';
|
|
14
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
15
|
+
import { buildSpatialIndexAsync } from '@ifc-lite/spatial';
|
|
16
|
+
import { useViewerStore } from '../store.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a spatial index in the background (time-sliced, non-blocking)
|
|
20
|
+
* with a guard against stale loads.
|
|
21
|
+
*
|
|
22
|
+
* The guard captures the dataStore reference and compares it to the
|
|
23
|
+
* current store when the async build completes. If the store has been
|
|
24
|
+
* replaced (e.g. user loaded a new file), the result is discarded.
|
|
25
|
+
*
|
|
26
|
+
* @param meshes - Final mesh array with correct IDs and world-space positions
|
|
27
|
+
* @param dataStore - The IfcDataStore to attach the spatial index to
|
|
28
|
+
* @param setIfcDataStore - Store setter to trigger re-render
|
|
29
|
+
*/
|
|
30
|
+
export function buildSpatialIndexGuarded(
|
|
31
|
+
meshes: MeshData[],
|
|
32
|
+
dataStore: IfcDataStore,
|
|
33
|
+
setIfcDataStore: (store: IfcDataStore) => void,
|
|
34
|
+
): void {
|
|
35
|
+
if (meshes.length === 0) return;
|
|
36
|
+
|
|
37
|
+
const capturedStore = dataStore;
|
|
38
|
+
buildSpatialIndexAsync(meshes).then(spatialIndex => {
|
|
39
|
+
const { ifcDataStore: currentStore } = useViewerStore.getState();
|
|
40
|
+
if (currentStore !== capturedStore) return;
|
|
41
|
+
capturedStore.spatialIndex = spatialIndex;
|
|
42
|
+
setIfcDataStore({ ...capturedStore });
|
|
43
|
+
}).catch(err => {
|
|
44
|
+
console.warn('[loadingUtils] Failed to build spatial index:', err);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type QuantitySet,
|
|
33
33
|
} from '@ifc-lite/data';
|
|
34
34
|
import { StringTable } from '@ifc-lite/data';
|
|
35
|
-
import {
|
|
35
|
+
import type { SpatialIndex } from '@ifc-lite/spatial';
|
|
36
36
|
|
|
37
37
|
// ============================================================================
|
|
38
38
|
// Types
|
|
@@ -713,8 +713,9 @@ export function convertServerDataModel(
|
|
|
713
713
|
},
|
|
714
714
|
};
|
|
715
715
|
|
|
716
|
-
//
|
|
717
|
-
|
|
716
|
+
// Spatial index is built asynchronously by the caller after this returns
|
|
717
|
+
// to avoid blocking the main thread for seconds on large models.
|
|
718
|
+
const spatialIndex: SpatialIndex | undefined = undefined;
|
|
718
719
|
|
|
719
720
|
// Validate schemaVersion against allowed values
|
|
720
721
|
const VALID_SCHEMA_VERSIONS = ['IFC2X3', 'IFC4', 'IFC4X3', 'IFC5'] as const;
|
|
Binary file
|