@ifc-lite/viewer 1.28.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.
Files changed (110) hide show
  1. package/.turbo/turbo-build.log +34 -41
  2. package/CHANGELOG.md +10 -0
  3. package/dist/assets/{basketViewActivator-BNRDNuUJ.js → basketViewActivator-Ce38DhXd.js} +7 -7
  4. package/dist/assets/{bcf-DCwCuP7n.js → bcf-Cv_O3JfD.js} +1 -1
  5. package/dist/assets/{deflate-DNGgs8Ur.js → deflate-HbyMq59o.js} +1 -1
  6. package/dist/assets/drawing-2d-DW98umlt.js +257 -0
  7. package/dist/assets/{exporters-B9v81gi9.js → exporters-BuD3XRzB.js} +463 -416
  8. package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
  9. package/dist/assets/{geotiff-D-YCLS4g.js → geotiff-B2HA8Bwm.js} +10 -10
  10. package/dist/assets/{ids-CCpq-5d3.js → ids-DYUFMd5f.js} +4 -4
  11. package/dist/assets/{ifc-lite_bg-DbgS5EUA.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
  12. package/dist/assets/index-E9wB0zWt.css +1 -0
  13. package/dist/assets/{index-Bgb3_Pu_.js → index-n5O1QJMM.js} +36808 -39415
  14. package/dist/assets/{index.es-CWfqZyyr.js → index.es-BKVIpZgL.js} +8 -8
  15. package/dist/assets/{jpeg-DGOAeUqU.js → jpeg-C7hjKjPX.js} +1 -1
  16. package/dist/assets/{jspdf.es.min-XPLU2Wkq.js → jspdf.es.min-oWlFc42Y.js} +4 -4
  17. package/dist/assets/{lerc-1PMSCHwX.js → lerc-BfIOGhQz.js} +1 -1
  18. package/dist/assets/{lzw-C65U9lNM.js → lzw-B0jRuuW5.js} +1 -1
  19. package/dist/assets/{native-bridge-XxXos6yI.js → native-bridge-DpB-dtEn.js} +5 -2
  20. package/dist/assets/{packbits-BdMWXC3m.js → packbits-DVvBTC39.js} +1 -1
  21. package/dist/assets/{parser.worker-Ddwo3_06.js → parser.worker-BDsWQ6rc.js} +1 -1
  22. package/dist/assets/{pdf-CRwaZf3s.js → pdf-dVIqI5ac.js} +9 -9
  23. package/dist/assets/raw-C0ZJYGmN.js +1 -0
  24. package/dist/assets/{sandbox-0sDo3g3m.js → sandbox-qpJlrNN0.js} +8 -8
  25. package/dist/assets/{server-client-cTCJ-853.js → server-client-DVZ2huNS.js} +1 -1
  26. package/dist/assets/{webimage-BtakWX7W.js → webimage-B394g0Tw.js} +1 -1
  27. package/dist/assets/{xlsx-B1YOg2QB.js → xlsx-D-oHO76J.js} +7 -7
  28. package/dist/assets/{zstd-CmwsbxmM.js → zstd-Bf38MwV2.js} +1 -1
  29. package/dist/index.html +8 -8
  30. package/package.json +5 -5
  31. package/src/App.tsx +1 -3
  32. package/src/components/viewer/BCFPanel.tsx +1 -16
  33. package/src/components/viewer/ChatPanel.tsx +11 -46
  34. package/src/components/viewer/HierarchyPanel.tsx +2 -176
  35. package/src/components/viewer/IDSPanel.tsx +1 -26
  36. package/src/components/viewer/MainToolbar.tsx +75 -185
  37. package/src/components/viewer/MobileToolbar.tsx +1 -9
  38. package/src/components/viewer/PropertiesPanel.tsx +28 -126
  39. package/src/components/viewer/ScriptPanel.tsx +8 -34
  40. package/src/components/viewer/Section2DPanel.tsx +32 -1
  41. package/src/components/viewer/ViewerLayout.tsx +0 -2
  42. package/src/components/viewer/ViewportContainer.tsx +24 -42
  43. package/src/components/viewer/ViewportOverlays.tsx +1 -4
  44. package/src/components/viewer/useGeometryStreaming.ts +0 -2
  45. package/src/hooks/ingest/federationAlign.ts +7 -0
  46. package/src/hooks/useDrawingGeneration.ts +211 -13
  47. package/src/hooks/useIfcCache.ts +94 -41
  48. package/src/hooks/useIfcFederation.ts +2 -3
  49. package/src/hooks/useIfcLoader.ts +10 -1051
  50. package/src/services/cacheService.ts +9 -25
  51. package/src/services/desktop-export.ts +2 -59
  52. package/src/services/file-dialog.ts +8 -142
  53. package/src/store/constants.ts +23 -0
  54. package/src/store/index.ts +3 -5
  55. package/src/store/slices/drawing2DSlice.ts +8 -0
  56. package/src/store/slices/visibilitySlice.ts +22 -1
  57. package/src/store/types.ts +1 -71
  58. package/src/utils/ifcConfig.ts +0 -12
  59. package/vite.config.ts +6 -3
  60. package/DESKTOP_CONTRACT_VERSION +0 -1
  61. package/dist/assets/drawing-2d-D0dDf6Lh.js +0 -257
  62. package/dist/assets/event-B0kAzHa-.js +0 -1
  63. package/dist/assets/geometry.worker-Bpa3115V.js +0 -1
  64. package/dist/assets/index-BtbXFKsX.css +0 -1
  65. package/dist/assets/raw-CJgQdyuZ.js +0 -1
  66. package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
  67. package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
  68. package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
  69. package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
  70. package/src/components/viewer/SettingsPage.tsx +0 -581
  71. package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
  72. package/src/lib/desktop-entitlement.ts +0 -43
  73. package/src/lib/desktop-product.ts +0 -130
  74. package/src/lib/platform.ts +0 -23
  75. package/src/services/desktop-cache.ts +0 -186
  76. package/src/services/desktop-harness.ts +0 -196
  77. package/src/services/desktop-logger.ts +0 -20
  78. package/src/services/desktop-native-metadata.ts +0 -230
  79. package/src/services/desktop-panel-actions.ts +0 -43
  80. package/src/services/desktop-preferences.ts +0 -44
  81. package/src/services/fs-cache.ts +0 -212
  82. package/src/services/tauri-core-stub.ts +0 -7
  83. package/src/services/tauri-dialog-stub.ts +0 -7
  84. package/src/services/tauri-fs-stub.ts +0 -7
  85. package/src/services/tauri-modules.d.ts +0 -50
  86. package/src/store/slices/desktopEntitlementSlice.ts +0 -86
  87. package/src/utils/desktopModelSnapshot.ts +0 -359
  88. package/src/utils/nativeSpatialDataStore.ts +0 -277
  89. package/src-tauri/Cargo.toml +0 -29
  90. package/src-tauri/build.rs +0 -7
  91. package/src-tauri/capabilities/default.json +0 -18
  92. package/src-tauri/icons/128x128.png +0 -0
  93. package/src-tauri/icons/128x128@2x.png +0 -0
  94. package/src-tauri/icons/32x32.png +0 -0
  95. package/src-tauri/icons/Square107x107Logo.png +0 -0
  96. package/src-tauri/icons/Square142x142Logo.png +0 -0
  97. package/src-tauri/icons/Square150x150Logo.png +0 -0
  98. package/src-tauri/icons/Square284x284Logo.png +0 -0
  99. package/src-tauri/icons/Square30x30Logo.png +0 -0
  100. package/src-tauri/icons/Square310x310Logo.png +0 -0
  101. package/src-tauri/icons/Square44x44Logo.png +0 -0
  102. package/src-tauri/icons/Square71x71Logo.png +0 -0
  103. package/src-tauri/icons/Square89x89Logo.png +0 -0
  104. package/src-tauri/icons/StoreLogo.png +0 -0
  105. package/src-tauri/icons/icon.icns +0 -0
  106. package/src-tauri/icons/icon.ico +0 -0
  107. package/src-tauri/icons/icon.png +0 -0
  108. package/src-tauri/src/lib.rs +0 -21
  109. package/src-tauri/src/main.rs +0 -10
  110. package/src-tauri/tauri.conf.json +0 -39
@@ -26,28 +26,17 @@ import {
26
26
  type GeometryResult,
27
27
  } from '@ifc-lite/geometry';
28
28
  import { acquireFileBuffer, type AcquiredBuffer } from '../utils/acquireFileBuffer.js';
29
- import initIfcLiteWasm, { IfcAPI } from '@ifc-lite/wasm';
30
29
  import { buildSpatialIndexGuarded, buildSpatialIndexForModel } from '../utils/loadingUtils.js';
31
30
  import { type GeometryData } from '@ifc-lite/cache';
32
31
 
33
- import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, CACHE_MAX_SOURCE_SIZE, HUGE_NATIVE_FILE_THRESHOLD, getDynamicBatchConfig } from '../utils/ifcConfig.js';
32
+ import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, CACHE_MAX_SOURCE_SIZE, getDynamicBatchConfig } from '../utils/ifcConfig.js';
34
33
  import {
35
34
  calculateMeshBounds,
36
35
  createCoordinateInfo,
37
36
  getRenderIntervalMs,
38
37
  calculateStoreyHeights,
39
38
  } from '../utils/localParsingUtils.js';
40
- import { buildDesktopMetadataSnapshot, restoreDesktopMetadataSnapshot } from '../utils/desktopModelSnapshot.js';
41
- import { buildIfcDataStoreFromNativeMetadata } from '../utils/nativeSpatialDataStore.js';
42
39
  import { applyColorUpdatesToMeshes } from './meshColorUpdates.js';
43
- import { readNativeFile, type NativeFileHandle } from '../services/file-dialog.js';
44
- import {
45
- bootstrapNativeMetadata,
46
- persistNativeMetadataSnapshot,
47
- restoreNativeMetadataSnapshot,
48
- } from '../services/desktop-native-metadata.js';
49
- import { finalizeActiveHarnessRun, getActiveHarnessRequest } from '../services/desktop-harness.js';
50
- import { logToDesktopTerminal } from '../services/desktop-logger.js';
51
40
 
52
41
  // Cache hook
53
42
  import { useIfcCache, getCached } from './useIfcCache.js';
@@ -116,25 +105,6 @@ function computeFastFingerprint(buffer: ArrayBuffer): string {
116
105
  return (hash >>> 0).toString(16);
117
106
  }
118
107
 
119
- function toExactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
120
- if (
121
- bytes.buffer instanceof ArrayBuffer &&
122
- bytes.byteOffset === 0 &&
123
- bytes.byteLength === bytes.buffer.byteLength
124
- ) {
125
- return bytes.buffer;
126
- }
127
- return bytes.slice().buffer;
128
- }
129
-
130
- function yieldToUiThread(): Promise<void> {
131
- return new Promise<void>((resolve) => {
132
- const channel = new MessageChannel();
133
- channel.port1.onmessage = () => resolve();
134
- channel.port2.postMessage(null);
135
- });
136
- }
137
-
138
108
  /**
139
109
  * Size-aware first-batch watchdog. Delegates to the package-level helper so
140
110
  * the formula stays unit-tested in `@ifc-lite/geometry`. Subsequent-batch
@@ -154,41 +124,6 @@ function getGeometryStreamWatchdogMs(
154
124
  });
155
125
  }
156
126
 
157
- function countNativeSpatialNodes(
158
- node: { children?: Array<{ children?: unknown[] }> } | null | undefined,
159
- ): number {
160
- if (!node) return 0;
161
- const children = Array.isArray(node.children) ? node.children : [];
162
- let total = 1;
163
- for (let i = 0; i < children.length; i += 1) {
164
- total += countNativeSpatialNodes(children[i] as { children?: Array<{ children?: unknown[] }> });
165
- }
166
- return total;
167
- }
168
-
169
- function computeNativeCacheKey(file: NativeFileHandle): string {
170
- const encodedPath = new TextEncoder().encode(file.path);
171
- const pathHash = computeFastFingerprint(toExactArrayBuffer(encodedPath));
172
- return `native-ifc-${file.size}-${file.modifiedMs ?? 0}-${pathHash}-v1`;
173
- }
174
-
175
- function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
176
- return typeof (file as NativeFileHandle).path === 'string';
177
- }
178
-
179
- let metadataScanApiPromise: Promise<IfcAPI> | null = null;
180
-
181
- async function getMetadataScanApi(): Promise<IfcAPI> {
182
- if (!metadataScanApiPromise) {
183
- metadataScanApiPromise = (async () => {
184
- await initIfcLiteWasm();
185
- return new IfcAPI();
186
- })();
187
- }
188
- return metadataScanApiPromise;
189
- }
190
-
191
- const ENABLE_HUGE_TIME_FLUSH = import.meta.env.VITE_IFC_ENABLE_HUGE_TIME_FLUSH === 'true';
192
127
 
193
128
  /**
194
129
  * Hook providing file loading operations for single-model path
@@ -240,7 +175,7 @@ export function useIfcLoader() {
240
175
  const { loadFromServer } = useIfcServer();
241
176
 
242
177
  const loadFile = useCallback(async (
243
- file: File | NativeFileHandle,
178
+ file: File,
244
179
  target: LoadTarget = { kind: 'primary' },
245
180
  ) => {
246
181
  const { resetViewerState, clearAllModels } = useViewerStore.getState();
@@ -305,7 +240,6 @@ export function useIfcLoader() {
305
240
  geometryLoadState: 'pending',
306
241
  metadataLoadState: 'idle',
307
242
  interactiveReady: false,
308
- nativeMetadata: null,
309
243
  cacheState: 'none',
310
244
  loadError: null,
311
245
  });
@@ -434,949 +368,13 @@ export function useIfcLoader() {
434
368
  };
435
369
 
436
370
 
437
- // Desktop native streaming path is reserved for truly large IFC files.
438
- // Mid-size files are more stable on the shared WASM/web loader and still
439
- // provide full viewer parity without the native streaming complexity.
440
- // PRIMARY only: the native path paints the active slot and isn't target-
441
- // aware, so a federated huge .ifc routes through the awaited WASM stream
442
- // (which gates active-model writes) instead — matching the former
443
- // federated path, which always used the WASM ingest regardless of size.
444
- if (
445
- target.kind === 'primary'
446
- && isNativeFileHandle(file)
447
- && fileName.toLowerCase().endsWith('.ifc')
448
- && file.size >= HUGE_NATIVE_FILE_THRESHOLD
449
- ) {
450
- const harnessRequest = getActiveHarnessRequest();
451
- const nativeCacheKey = computeNativeCacheKey(file);
452
- const shouldUseNativeCache = file.size >= CACHE_SIZE_THRESHOLD;
453
- const hugeNativeMode = file.size >= HUGE_NATIVE_FILE_THRESHOLD;
454
- const retainAllMeshes = !hugeNativeMode;
455
- console.log(`[useIfc] Native path load: ${fileName}, size: ${fileSizeMB.toFixed(2)}MB`);
456
- void logToDesktopTerminal(
457
- 'info',
458
- `[useIfc] Native path load start: ${fileName} (${fileSizeMB.toFixed(2)} MB) path=${file.path} hugeMode=${hugeNativeMode ? 'yes' : 'no'}`
459
- );
460
- setBoundedGeometryMode(hugeNativeMode);
461
- setGeometryStreamingActive(true);
462
- setIfcDataStore(null);
463
- setProgress({ phase: 'Starting native geometry streaming', percent: 10 });
464
-
465
- // Snapshot the user's "Merge Multilayer Walls" preference once
466
- // at load time — flipping the toggle mid-stream cannot affect
467
- // an in-flight WASM pipeline, the reload banner handles that.
468
- const mergeLayersAtLoad = useViewerStore.getState().mergeLayers;
469
- const geometryProcessor = new GeometryProcessor({
470
- quality: GeometryQuality.Balanced,
471
- preferNative: true,
472
- mergeLayers: mergeLayersAtLoad,
473
- });
474
-
475
- let estimatedTotal = 0;
476
- let totalMeshes = 0;
477
- let totalVertices = 0;
478
- let totalTriangles = 0;
479
- const allMeshes: MeshData[] = [];
480
- let finalCoordinateInfo: CoordinateInfo | null = null;
481
- let batchCount = 0;
482
- let modelOpenMs: number | null = null;
483
- let firstGeometryTime = 0;
484
- let firstAppendGeometryBatchMs: number | null = null;
485
- let firstVisibleGeometryMs: number | null = null;
486
- let jsFirstChunkReceivedMs: number | null = null;
487
- let lastTotalMeshes = 0;
488
- let pendingMeshes: MeshData[] = [];
489
- let loggedFirstAppendStoreState = false;
490
- let lastRenderTime = 0;
491
- let streamCompleteMs: number | null = null;
492
- let metadataStartMs: number | null = null;
493
- let metadataReadCompleteMs: number | null = null;
494
- let metadataParseStartMs: number | null = null;
495
- let spatialReadyMs: number | null = null;
496
- let metadataCompleteMs: number | null = null;
497
- let metadataFailedMs: number | null = null;
498
- let metadataReadDurationMs: number | null = null;
499
- let metadataBufferCopyDurationMs: number | null = null;
500
- let metadataParseDurationMs: number | null = null;
501
- let metadataParsingPromise: Promise<void> | null = null;
502
- let metadataStallWatchId: ReturnType<typeof globalThis.setInterval> | null = null;
503
- let lastMetadataActivityTime = 0;
504
- let currentMetadataActivity = 'idle';
505
- let firstNativeBatchTelemetry: {
506
- batchSequence: number;
507
- payloadKind: string;
508
- meshCount: number;
509
- positionsLen: number;
510
- normalsLen: number;
511
- indicesLen: number;
512
- chunkReadyTimeMs: number;
513
- packTimeMs: number;
514
- emittedTimeMs: number;
515
- emitTimeMs: number;
516
- jsReceivedTimeMs?: number;
517
- } | null = null;
518
- let nativeStats: {
519
- parseTimeMs?: number;
520
- entityScanTimeMs?: number;
521
- lookupTimeMs?: number;
522
- preprocessTimeMs?: number;
523
- geometryTimeMs?: number;
524
- totalTimeMs?: number;
525
- firstChunkReadyTimeMs?: number;
526
- firstChunkPackTimeMs?: number;
527
- firstChunkEmittedTimeMs?: number;
528
- firstChunkEmitTimeMs?: number;
529
- } | null = null;
530
- const RENDER_INTERVAL_MS = getRenderIntervalMs(fileSizeMB);
531
- const NATIVE_PENDING_MESH_THRESHOLD =
532
- fileSizeMB > 768 ? 8192 :
533
- fileSizeMB > 512 ? 6144 :
534
- fileSizeMB > 256 ? 4096 :
535
- fileSizeMB > 100 ? 2048 :
536
- 512;
537
- const HUGE_NATIVE_APPEND_CHUNK_SIZE = fileSizeMB > 768 ? 2048 : hugeNativeMode ? 1536 : 0;
538
- const HUGE_NATIVE_APPEND_YIELD_THRESHOLD = fileSizeMB > 768 ? 8192 : 6144;
539
- const HUGE_NATIVE_APPEND_YIELD_BUDGET_MS = 10;
540
- let metadataParsingStarted = false;
541
- let geometryCompleted = false;
542
- let fullNativeDataStore: IfcDataStore | null = null;
543
- let nativeLoadStage: 'open' | 'streamGeometry' | 'finalizeGeometry' | 'hydrateMetadata' | 'complete' = 'open';
544
- let nativeMetadataSource: 'snapshot' | 'ifc-parse' = 'ifc-parse';
545
- let nativeMetadataStartGate = 'immediate' as 'immediate' | 'afterInteractiveGeometry' | 'afterGeometryComplete';
546
-
547
- setGeometryResult(null);
548
-
549
- const maybeBuildNativeSpatialIndex = () => {
550
- if (
551
- !retainAllMeshes ||
552
- !geometryCompleted ||
553
- !fullNativeDataStore ||
554
- allMeshes.length === 0 ||
555
- hugeNativeMode ||
556
- loadSessionRef.current !== currentSession
557
- ) {
558
- return;
559
- }
560
- buildSpatialIndexGuarded(allMeshes, fullNativeDataStore, setIfcDataStore);
561
- };
562
-
563
- const flushPendingNativeMeshes = async (
564
- coordinateInfo: CoordinateInfo | null | undefined,
565
- totalMeshesSoFar: number,
566
- ) => {
567
- if (pendingMeshes.length === 0) {
568
- return;
569
- }
570
-
571
- if (firstAppendGeometryBatchMs === null) {
572
- firstAppendGeometryBatchMs = performance.now() - totalStartTime;
573
- void logToDesktopTerminal(
574
- 'info',
575
- `[useIfc] Native first appendGeometryBatch for ${fileName}: ${firstAppendGeometryBatchMs.toFixed(0)}ms`
576
- );
577
- }
578
-
579
- void totalMeshesSoFar;
580
-
581
- const appendMeshesToStore = (meshesToAppend: MeshData[]) => {
582
- const appendGeometryBatchToStore = getViewerStoreApi().getState().appendGeometryBatch;
583
- if (hugeNativeMode) {
584
- flushSync(() => {
585
- appendGeometryBatchToStore(meshesToAppend, coordinateInfo ?? undefined);
586
- });
587
- return;
588
- }
589
- appendGeometryBatchToStore(meshesToAppend, coordinateInfo ?? undefined);
590
- };
591
-
592
- if (!hugeNativeMode || HUGE_NATIVE_APPEND_CHUNK_SIZE <= 0 || pendingMeshes.length <= HUGE_NATIVE_APPEND_CHUNK_SIZE) {
593
- appendMeshesToStore(pendingMeshes);
594
- if (!loggedFirstAppendStoreState) {
595
- const stateAfterAppend = useViewerStore.getState();
596
- void logToDesktopTerminal(
597
- 'info',
598
- `[useIfc] Store after append for ${fileName}: activeModelId=${stateAfterAppend.activeModelId ?? 'null'} legacyMeshes=${stateAfterAppend.geometryResult?.meshes.length ?? 0} modelMeshes=${stateAfterAppend.models.get(modelId)?.geometryResult?.meshes.length ?? 0} geometryTick=${stateAfterAppend.geometryUpdateTick}`
599
- );
600
- loggedFirstAppendStoreState = true;
601
- }
602
- if (hugeNativeMode) {
603
- await yieldToUiThread();
604
- if (typeof requestAnimationFrame === 'function') {
605
- await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
606
- }
607
- }
608
- pendingMeshes = [];
609
- markFirstVisibleGeometry();
610
- return;
611
- }
612
-
613
- let appendedSinceYield = 0;
614
- let appendWindowStart = performance.now();
615
- while (pendingMeshes.length > 0) {
616
- const chunk = pendingMeshes.splice(0, HUGE_NATIVE_APPEND_CHUNK_SIZE);
617
- appendMeshesToStore(chunk);
618
- if (!loggedFirstAppendStoreState) {
619
- const stateAfterAppend = useViewerStore.getState();
620
- void logToDesktopTerminal(
621
- 'info',
622
- `[useIfc] Store after append for ${fileName}: activeModelId=${stateAfterAppend.activeModelId ?? 'null'} legacyMeshes=${stateAfterAppend.geometryResult?.meshes.length ?? 0} modelMeshes=${stateAfterAppend.models.get(modelId)?.geometryResult?.meshes.length ?? 0} geometryTick=${stateAfterAppend.geometryUpdateTick}`
623
- );
624
- loggedFirstAppendStoreState = true;
625
- }
626
- appendedSinceYield += chunk.length;
627
- markFirstVisibleGeometry();
628
- if (pendingMeshes.length === 0) {
629
- break;
630
- }
631
-
632
- const shouldYield =
633
- appendedSinceYield >= HUGE_NATIVE_APPEND_YIELD_THRESHOLD ||
634
- performance.now() - appendWindowStart >= HUGE_NATIVE_APPEND_YIELD_BUDGET_MS;
635
- if (shouldYield) {
636
- await yieldToUiThread();
637
- if (typeof requestAnimationFrame === 'function') {
638
- await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
639
- }
640
- appendedSinceYield = 0;
641
- appendWindowStart = performance.now();
642
- }
643
- }
644
- };
645
-
646
- const markFirstVisibleGeometry = () => {
647
- if (firstVisibleGeometryMs !== null) return;
648
- requestAnimationFrame(() => {
649
- if (firstVisibleGeometryMs !== null || loadSessionRef.current !== currentSession) return;
650
- firstVisibleGeometryMs = performance.now() - totalStartTime;
651
- void logToDesktopTerminal(
652
- 'info',
653
- `[useIfc] Native first visible geometry for ${fileName}: ${firstVisibleGeometryMs.toFixed(0)}ms`
654
- );
655
- });
656
- };
657
-
658
- const finalizeNativeDataStore = (dataStore: IfcDataStore) => {
659
- if (dataStore.spatialHierarchy && dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
660
- const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
661
- for (const [storeyId, height] of calculatedHeights) {
662
- dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
663
- }
664
- }
665
- fullNativeDataStore = dataStore;
666
- setIfcDataStore(dataStore);
667
- if (geometryCompleted) {
668
- nativeLoadStage = 'complete';
669
- }
670
- void finalizeModel(
671
- dataStore,
672
- useViewerStore.getState().geometryResult,
673
- getSchemaVersion(dataStore),
674
- {
675
- loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
676
- cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
677
- },
678
- );
679
- updateModel(modelId, {
680
- geometryLoadState: geometryCompleted ? 'complete' : 'interactive',
681
- metadataLoadState: 'complete',
682
- interactiveReady: true,
683
- });
684
- maybeBuildNativeSpatialIndex();
685
- };
686
-
687
- const hydrateNativeSpatialDataStore = (
688
- nativeMetadata: NonNullable<Awaited<ReturnType<typeof restoreNativeMetadataSnapshot>>>,
689
- ) => {
690
- const spatialDataStore = buildIfcDataStoreFromNativeMetadata(nativeMetadata);
691
- if (!spatialDataStore) {
692
- return;
693
- }
694
- if (spatialDataStore.spatialHierarchy && spatialDataStore.spatialHierarchy.storeyHeights.size === 0 && spatialDataStore.spatialHierarchy.storeyElevations.size > 1) {
695
- const calculatedHeights = calculateStoreyHeights(spatialDataStore.spatialHierarchy.storeyElevations);
696
- for (const [storeyId, height] of calculatedHeights) {
697
- spatialDataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
698
- }
699
- }
700
- const state = useViewerStore.getState();
701
- const currentGeometryResult =
702
- state.models.get(modelId)?.geometryResult ??
703
- state.geometryResult;
704
- setIfcDataStore(spatialDataStore);
705
- void finalizeModel(
706
- spatialDataStore,
707
- currentGeometryResult,
708
- nativeMetadata.schemaVersion,
709
- {
710
- loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
711
- cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
712
- },
713
- );
714
- };
715
-
716
- let nativeMetadataSnapshotHit = false;
717
- let metadataSnapshotWritePromise: Promise<void> | null = null;
718
-
719
- const queueNativeMetadataSnapshotWrite = (
720
- dataStore: IfcDataStore,
721
- sourceBuffer: ArrayBuffer,
722
- ) => {
723
- metadataSnapshotWritePromise = (async () => {
724
- await new Promise<void>((resolve) => {
725
- const channel = new MessageChannel();
726
- channel.port1.onmessage = () => resolve();
727
- channel.port2.postMessage(null);
728
- });
729
- if (typeof requestAnimationFrame === 'function') {
730
- await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
731
- }
732
- await writeNativeMetadataSnapshot(dataStore, sourceBuffer);
733
- })();
734
- };
735
-
736
- const writeNativeMetadataSnapshot = async (
737
- dataStore: IfcDataStore,
738
- sourceBuffer: ArrayBuffer,
739
- ): Promise<void> => {
740
- if (!shouldUseNativeCache || !nativeCacheKey) return;
741
- try {
742
- const { setNativeModelSnapshot } = await import('../services/desktop-cache.js');
743
- const snapshotBuffer = await buildDesktopMetadataSnapshot(dataStore, sourceBuffer);
744
- await setNativeModelSnapshot(nativeCacheKey, snapshotBuffer);
745
- } catch (error) {
746
- console.warn('[useIfc] Failed to persist native metadata snapshot:', error);
747
- void logToDesktopTerminal(
748
- 'warn',
749
- `[useIfc] Native metadata snapshot write failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
750
- );
751
- }
752
- };
753
-
754
- const noteMetadataActivity = (activity: string) => {
755
- currentMetadataActivity = activity;
756
- lastMetadataActivityTime = performance.now();
757
- };
758
-
759
- const stopMetadataStallWatch = () => {
760
- if (metadataStallWatchId !== null) {
761
- globalThis.clearInterval(metadataStallWatchId);
762
- metadataStallWatchId = null;
763
- }
764
- };
765
-
766
- const startMetadataStallWatch = () => {
767
- stopMetadataStallWatch();
768
- noteMetadataActivity('starting');
769
- metadataStallWatchId = globalThis.setInterval(() => {
770
- if (loadSessionRef.current !== currentSession) {
771
- stopMetadataStallWatch();
772
- return;
773
- }
774
- const idleForMs = performance.now() - lastMetadataActivityTime;
775
- if (idleForMs < 8000) return;
776
- lastMetadataActivityTime = performance.now();
777
- void logToDesktopTerminal(
778
- 'warn',
779
- `[useIfc] Metadata stall watch for ${fileName}: stage=${nativeLoadStage} idle=${idleForMs.toFixed(0)}ms phase=${currentMetadataActivity} batches=${batchCount} meshes=${lastTotalMeshes} geometryCompleted=${geometryCompleted}`
780
- );
781
- }, 5000);
782
- };
783
-
784
- const startNativeMetadataParsing = (): Promise<void> | null => {
785
- if (metadataParsingStarted) return metadataParsingPromise;
786
- metadataParsingStarted = true;
787
- nativeLoadStage = 'hydrateMetadata';
788
- const metadataStartTime = performance.now();
789
- metadataStartMs = metadataStartTime - totalStartTime;
790
- let lastMetadataProgressPhase = '';
791
- let lastMetadataProgressPercent = -1;
792
- startMetadataStallWatch();
793
- setMetadataProgress({ phase: 'Bootstrapping metadata', percent: 5, indeterminate: hugeNativeMode });
794
- updateModel(modelId, {
795
- loadState: 'hydrating-metadata',
796
- metadataLoadState: 'bootstrapping',
797
- });
798
- void logToDesktopTerminal(
799
- 'info',
800
- `[useIfc] Native metadata parse start for ${fileName} source=${nativeMetadataSource} gate=${nativeMetadataStartGate}`
801
- );
802
-
803
- const metadataReadStartTime = performance.now();
804
- let parseStartTime = 0;
805
- metadataParsingPromise = (async () => {
806
- if (hugeNativeMode) {
807
- noteMetadataActivity('native bootstrap');
808
- metadataParseStartMs = performance.now() - totalStartTime;
809
- parseStartTime = performance.now();
810
- if (nativeMetadataSnapshotHit) {
811
- const restoredSnapshot = await restoreNativeMetadataSnapshot(nativeCacheKey);
812
- if (restoredSnapshot && loadSessionRef.current === currentSession) {
813
- try {
814
- spatialReadyMs = performance.now() - totalStartTime;
815
- hydrateNativeSpatialDataStore(restoredSnapshot);
816
- updateModel(modelId, {
817
- nativeMetadata: restoredSnapshot,
818
- schemaVersion: restoredSnapshot.schemaVersion,
819
- metadataLoadState: 'spatial-ready',
820
- interactiveReady: true,
821
- });
822
- setMetadataProgress({ phase: 'Restored metadata sidecar', percent: 70 });
823
- } catch (error) {
824
- nativeMetadataSnapshotHit = false;
825
- nativeMetadataSource = 'ifc-parse';
826
- void logToDesktopTerminal(
827
- 'warn',
828
- `[useIfc] Native metadata snapshot restore incompatible for ${fileName}, continuing with live bootstrap: ${error instanceof Error ? error.message : String(error)}`
829
- );
830
- }
831
- }
832
- }
833
- void logToDesktopTerminal(
834
- 'info',
835
- `[useIfc] Awaiting native metadata bootstrap for ${fileName}`
836
- );
837
- const nativeMetadata = await bootstrapNativeMetadata(file.path, nativeCacheKey);
838
- if (loadSessionRef.current !== currentSession) {
839
- return null;
840
- }
841
- const spatialNodeCount = countNativeSpatialNodes(nativeMetadata.spatialTree);
842
- void logToDesktopTerminal(
843
- 'info',
844
- `[useIfc] Native metadata bootstrap resolved for ${fileName}: elapsed=${(performance.now() - parseStartTime).toFixed(0)}ms hasTree=${nativeMetadata.spatialTree ? 'yes' : 'no'} spatialNodes=${spatialNodeCount}`
845
- );
846
- metadataReadCompleteMs = performance.now() - totalStartTime;
847
- metadataReadDurationMs = metadataReadCompleteMs - metadataStartMs;
848
- spatialReadyMs = performance.now() - totalStartTime;
849
- void logToDesktopTerminal(
850
- 'info',
851
- `[useIfc] Applying native metadata to store for ${fileName}`
852
- );
853
- hydrateNativeSpatialDataStore(nativeMetadata);
854
- updateModel(modelId, {
855
- nativeMetadata,
856
- schemaVersion: nativeMetadata.schemaVersion,
857
- metadataLoadState: 'spatial-ready',
858
- interactiveReady: true,
859
- });
860
- void logToDesktopTerminal(
861
- 'info',
862
- `[useIfc] Native metadata store update complete for ${fileName}`
863
- );
864
- setMetadataProgress({ phase: 'Spatial tree ready', percent: 70 });
865
- if (!nativeMetadataSnapshotHit) {
866
- void persistNativeMetadataSnapshot(nativeMetadata);
867
- }
868
- metadataCompleteMs = performance.now() - totalStartTime;
869
- metadataParseDurationMs = performance.now() - parseStartTime;
870
- updateModel(modelId, {
871
- loadState: geometryCompleted ? 'complete' : 'hydrating-metadata',
872
- metadataLoadState: 'lazy',
873
- });
874
- setMetadataProgress({ phase: 'Metadata ready on demand', percent: 100 });
875
- return null;
876
- }
877
-
878
- if (nativeGeometryCacheHit && nativeMetadataSnapshotHit) {
879
- try {
880
- const { getNativeModelSnapshot } = await import('../services/desktop-cache.js');
881
- const snapshotBuffer = await getNativeModelSnapshot(nativeCacheKey);
882
- if (!snapshotBuffer) {
883
- throw new Error(`missing-native-metadata-snapshot:${nativeCacheKey}`);
884
- }
885
- metadataReadCompleteMs = performance.now() - totalStartTime;
886
- metadataReadDurationMs = performance.now() - metadataReadStartTime;
887
- metadataParseStartMs = performance.now() - totalStartTime;
888
- parseStartTime = performance.now();
889
- noteMetadataActivity('snapshot hydrate');
890
- if (spatialReadyMs === null) {
891
- spatialReadyMs = performance.now() - totalStartTime;
892
- }
893
- setMetadataProgress({ phase: 'Restoring cached metadata', percent: 80 });
894
- return restoreDesktopMetadataSnapshot(snapshotBuffer);
895
- } catch (error) {
896
- nativeMetadataSnapshotHit = false;
897
- nativeMetadataSource = 'ifc-parse';
898
- void logToDesktopTerminal(
899
- 'warn',
900
- `[useIfc] Native metadata snapshot hydration failed for ${fileName}, falling back to IFC parse: ${error instanceof Error ? error.message : String(error)}`
901
- );
902
- }
903
- }
904
-
905
- const bytes = await readNativeFile(file.path);
906
- if (loadSessionRef.current !== currentSession) {
907
- return null;
908
- }
909
- metadataReadCompleteMs = performance.now() - totalStartTime;
910
- metadataReadDurationMs = performance.now() - metadataReadStartTime;
911
- void logToDesktopTerminal(
912
- 'info',
913
- `[useIfc] Native metadata file read complete for ${fileName}: ${metadataReadDurationMs.toFixed(0)}ms`
914
- );
915
- const copyStartTime = performance.now();
916
- const metadataBuffer = toExactArrayBuffer(bytes);
917
- metadataBufferCopyDurationMs = performance.now() - copyStartTime;
918
- metadataParseStartMs = performance.now() - totalStartTime;
919
- parseStartTime = performance.now();
920
- noteMetadataActivity('parse setup');
921
- void logToDesktopTerminal(
922
- 'info',
923
- `[useIfc] Native metadata buffer copy complete for ${fileName}: ${metadataBufferCopyDurationMs.toFixed(0)}ms`
924
- );
925
-
926
- const parser = new IfcParser();
927
- const wasmApi = hugeNativeMode ? await getMetadataScanApi() : undefined;
928
- const dataStore = await parser.parseColumnar(metadataBuffer, {
929
- wasmApi,
930
- yieldIntervalMs: hugeNativeMode ? 32 : undefined,
931
- deferPropertyAtomIndex: hugeNativeMode,
932
- disableWorkerScan: false,
933
- onProgress: (progress) => {
934
- if (!hugeNativeMode) return;
935
- noteMetadataActivity(`progress:${progress.phase}:${Math.round(progress.percent)}`);
936
- const roundedPercent = Math.round(progress.percent);
937
- const shouldLog =
938
- progress.phase !== lastMetadataProgressPhase ||
939
- roundedPercent >= lastMetadataProgressPercent + 5 ||
940
- roundedPercent === 100;
941
- if (!shouldLog) return;
942
- setMetadataProgress({
943
- phase: `Metadata ${progress.phase}`,
944
- percent: roundedPercent,
945
- indeterminate: false,
946
- });
947
- lastMetadataProgressPhase = progress.phase;
948
- lastMetadataProgressPercent = roundedPercent;
949
- void logToDesktopTerminal(
950
- 'info',
951
- `[useIfc] Native metadata progress for ${fileName}: ${progress.phase} ${roundedPercent}%`
952
- );
953
- },
954
- onSpatialReady: (partialStore) => {
955
- if (loadSessionRef.current !== currentSession) return;
956
- noteMetadataActivity('spatial ready');
957
- if (spatialReadyMs === null) {
958
- spatialReadyMs = performance.now() - totalStartTime;
959
- }
960
- setMetadataProgress({ phase: 'Spatial tree ready', percent: 70 });
961
- if (partialStore.spatialHierarchy && partialStore.spatialHierarchy.storeyHeights.size === 0 && partialStore.spatialHierarchy.storeyElevations.size > 1) {
962
- const calculatedHeights = calculateStoreyHeights(partialStore.spatialHierarchy.storeyElevations);
963
- for (const [storeyId, height] of calculatedHeights) {
964
- partialStore.spatialHierarchy.storeyHeights.set(storeyId, height);
965
- }
966
- }
967
- setIfcDataStore(partialStore);
968
- void logToDesktopTerminal(
969
- 'info',
970
- `[useIfc] Native spatial tree ready for ${fileName} at ${(performance.now() - totalStartTime).toFixed(0)}ms`
971
- );
972
- },
973
- onDiagnostic: (message) => {
974
- noteMetadataActivity(`diag:${message}`);
975
- void logToDesktopTerminal('info', `[useIfc][diag] ${fileName}: ${message}`);
976
- },
977
- });
978
- queueNativeMetadataSnapshotWrite(dataStore, metadataBuffer);
979
- return dataStore;
980
- })()
981
- .then((dataStore) => {
982
- stopMetadataStallWatch();
983
- if (loadSessionRef.current !== currentSession || !dataStore) return;
984
- metadataCompleteMs = performance.now() - totalStartTime;
985
- metadataParseDurationMs = parseStartTime > 0 ? performance.now() - parseStartTime : null;
986
- setMetadataProgress({ phase: 'Metadata ready', percent: 100 });
987
- finalizeNativeDataStore(dataStore);
988
- void logToDesktopTerminal(
989
- 'info',
990
- `[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`
991
- );
992
- })
993
- .catch((error) => {
994
- if (loadSessionRef.current !== currentSession) return;
995
- stopMetadataStallWatch();
996
- metadataFailedMs = performance.now() - totalStartTime;
997
- console.warn('[useIfc] Native metadata parsing failed:', error);
998
- updateModel(modelId, {
999
- loadState: 'error',
1000
- metadataLoadState: 'error',
1001
- loadError: error instanceof Error ? error.message : String(error),
1002
- });
1003
- setMetadataProgress({ phase: 'Metadata failed', percent: 100 });
1004
- void logToDesktopTerminal(
1005
- 'warn',
1006
- `[useIfc] Native metadata parse failed for ${fileName}: ${error instanceof Error ? error.message : String(error)}`
1007
- );
1008
- });
1009
- return metadataParsingPromise;
1010
- };
1011
-
1012
- const HUGE_NATIVE_METADATA_START_BATCH = 20;
1013
- let metadataStartQueued = false;
1014
- const queueNativeMetadataStart = (reason: string) => {
1015
- if (metadataParsingStarted || metadataStartQueued) return;
1016
- metadataStartQueued = true;
1017
- void logToDesktopTerminal('info', `[useIfc] Queueing metadata hydration for ${fileName} after ${reason}`);
1018
- metadataStartQueued = false;
1019
- if (loadSessionRef.current !== currentSession || metadataParsingStarted) return;
1020
- void logToDesktopTerminal('info', `[useIfc] Starting metadata hydration after ${reason} for ${fileName}`);
1021
- startNativeMetadataParsing();
1022
- };
1023
-
1024
- let nativeGeometryCacheHit = false;
1025
- if (shouldUseNativeCache) {
1026
- const { hasNativeGeometryCache, hasNativeModelSnapshot } = await import('../services/desktop-cache.js');
1027
- setProgress({ phase: 'Checking cache', percent: 5 });
1028
- setGeometryProgress({ phase: 'Checking geometry cache', percent: 5 });
1029
- nativeGeometryCacheHit = await hasNativeGeometryCache(nativeCacheKey);
1030
- nativeMetadataSnapshotHit = nativeGeometryCacheHit
1031
- ? await hasNativeModelSnapshot(nativeCacheKey)
1032
- : false;
1033
- nativeMetadataSource = nativeMetadataSnapshotHit ? 'snapshot' : 'ifc-parse';
1034
- nativeMetadataStartGate = 'immediate';
1035
- updateModel(modelId, { cacheState: nativeGeometryCacheHit ? 'hit' : 'miss' });
1036
- void logToDesktopTerminal(
1037
- 'info',
1038
- nativeGeometryCacheHit
1039
- ? `[useIfc] Native geometry cache hit for ${fileName}`
1040
- : `[useIfc] Native geometry cache miss for ${fileName}`
1041
- );
1042
- if (nativeMetadataStartGate === 'immediate') {
1043
- startNativeMetadataParsing();
1044
- } else {
1045
- void logToDesktopTerminal(
1046
- 'info',
1047
- nativeMetadataStartGate === 'afterInteractiveGeometry'
1048
- ? `[useIfc] Deferring metadata hydration until geometry batch ${HUGE_NATIVE_METADATA_START_BATCH} for ${fileName}`
1049
- : `[useIfc] Deferring metadata hydration until geometry complete for ${fileName}`
1050
- );
1051
- }
1052
- }
1053
-
1054
- if (!shouldUseNativeCache) {
1055
- if (nativeMetadataStartGate === 'immediate') {
1056
- startNativeMetadataParsing();
1057
- } else {
1058
- void logToDesktopTerminal(
1059
- 'info',
1060
- `[useIfc] Deferring metadata hydration until geometry complete for ${fileName}`
1061
- );
1062
- }
1063
- }
1064
- await geometryProcessor.init();
1065
- void logToDesktopTerminal('info', `[useIfc] GeometryProcessor.init complete for ${fileName}`);
1066
-
1067
- const nativeStream = nativeGeometryCacheHit
1068
- ? geometryProcessor.processStreamingCache(nativeCacheKey)
1069
- : geometryProcessor.processStreamingPath(
1070
- file.path,
1071
- file.size,
1072
- shouldUseNativeCache ? nativeCacheKey : undefined,
1073
- );
1074
-
1075
- for await (const event of nativeStream) {
1076
- const eventReceived = performance.now();
1077
-
1078
- switch (event.type) {
1079
- case 'start':
1080
- estimatedTotal = event.totalEstimate;
1081
- void logToDesktopTerminal('info', `[useIfc] Native stream start for ${fileName}: estimate=${Math.round(estimatedTotal)}`);
1082
- break;
1083
- case 'model-open':
1084
- nativeLoadStage = 'streamGeometry';
1085
- setProgress({ phase: 'Processing geometry (native precompute)', percent: 50, indeterminate: true });
1086
- setGeometryProgress({ phase: 'Opening native geometry stream', percent: 10, indeterminate: true });
1087
- modelOpenMs = performance.now() - totalStartTime;
1088
- console.log(`[useIfc] Native model opened at ${modelOpenMs.toFixed(0)}ms`);
1089
- void logToDesktopTerminal('info', `[useIfc] Native model opened for ${fileName} at ${modelOpenMs.toFixed(0)}ms`);
1090
- break;
1091
- case 'batch': {
1092
- batchCount++;
1093
-
1094
- if (batchCount === 1) {
1095
- firstGeometryTime = performance.now() - totalStartTime;
1096
- jsFirstChunkReceivedMs = event.nativeTelemetry?.jsReceivedTimeMs ?? firstGeometryTime;
1097
- firstNativeBatchTelemetry = event.nativeTelemetry ?? null;
1098
- updateModel(modelId, {
1099
- geometryLoadState: 'interactive',
1100
- interactiveReady: true,
1101
- });
1102
- console.log(`[useIfc] Native batch #1: ${event.meshes.length} meshes, wait: ${firstGeometryTime.toFixed(0)}ms`);
1103
- void logToDesktopTerminal('info', `[useIfc] Native first batch for ${fileName}: meshes=${event.meshes.length}, wait=${firstGeometryTime.toFixed(0)}ms`);
1104
- if (event.nativeTelemetry) {
1105
- const transferLagMs = (event.nativeTelemetry.jsReceivedTimeMs ?? 0) - event.nativeTelemetry.emittedTimeMs;
1106
- void logToDesktopTerminal(
1107
- 'info',
1108
- `[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`
1109
- );
1110
- }
1111
- } else if (batchCount % 20 === 0) {
1112
- void logToDesktopTerminal('info', `[useIfc] Native batch milestone for ${fileName}: batch=${batchCount}, totalMeshes=${event.totalSoFar}`);
1113
- }
1114
-
1115
- for (let i = 0; i < event.meshes.length; i++) {
1116
- const mesh = event.meshes[i];
1117
- if (retainAllMeshes) {
1118
- allMeshes.push(mesh);
1119
- }
1120
- totalVertices += mesh.positions.length / 3;
1121
- totalTriangles += mesh.indices.length / 3;
1122
- }
1123
- finalCoordinateInfo = event.coordinateInfo ?? null;
1124
- totalMeshes = event.totalSoFar;
1125
- lastTotalMeshes = event.totalSoFar;
1126
-
1127
- for (let i = 0; i < event.meshes.length; i++) pendingMeshes.push(event.meshes[i]);
1128
-
1129
- if (
1130
- nativeMetadataStartGate === 'afterInteractiveGeometry' &&
1131
- !metadataParsingStarted &&
1132
- batchCount >= HUGE_NATIVE_METADATA_START_BATCH &&
1133
- firstAppendGeometryBatchMs !== null
1134
- ) {
1135
- queueNativeMetadataStart(`geometry batch ${batchCount}`);
1136
- }
1137
-
1138
- const timeSinceLastRender = eventReceived - lastRenderTime;
1139
- const allowTimeBasedFlush = !hugeNativeMode || ENABLE_HUGE_TIME_FLUSH;
1140
- const shouldRender =
1141
- batchCount === 1 ||
1142
- pendingMeshes.length >= NATIVE_PENDING_MESH_THRESHOLD ||
1143
- (allowTimeBasedFlush && timeSinceLastRender >= RENDER_INTERVAL_MS);
1144
-
1145
- if (shouldRender && pendingMeshes.length > 0) {
1146
- await flushPendingNativeMeshes(event.coordinateInfo, totalMeshes);
1147
- lastRenderTime = eventReceived;
1148
-
1149
- const progressPercent = 50 + Math.min(45, (totalMeshes / Math.max(estimatedTotal / 10, totalMeshes || 1)) * 45);
1150
- setProgress({
1151
- phase: `Rendering geometry (${totalMeshes} meshes)`,
1152
- percent: progressPercent,
1153
- indeterminate: false,
1154
- });
1155
- setGeometryProgress({
1156
- phase: `Rendering geometry (${totalMeshes} meshes)`,
1157
- percent: Math.min(99, progressPercent),
1158
- indeterminate: false,
1159
- });
1160
- }
1161
- break;
1162
- }
1163
- case 'complete':
1164
- nativeLoadStage = 'finalizeGeometry';
1165
- geometryCompleted = true;
1166
- streamCompleteMs = performance.now() - totalStartTime;
1167
- if (pendingMeshes.length > 0) {
1168
- await flushPendingNativeMeshes(event.coordinateInfo, lastTotalMeshes);
1169
- }
1170
-
1171
- finalCoordinateInfo = event.coordinateInfo;
1172
- updateCoordinateInfo(finalCoordinateInfo);
1173
- maybeBuildNativeSpatialIndex();
1174
- if (nativeMetadataStartGate === 'afterGeometryComplete' && !metadataParsingStarted) {
1175
- queueNativeMetadataStart('geometry complete');
1176
- }
1177
- setProgress({
1178
- phase: hugeNativeMode ? 'Geometry ready, hydrating metadata' : 'Complete',
1179
- percent: 100,
1180
- });
1181
- setGeometryProgress({
1182
- phase: 'Geometry interactive',
1183
- percent: 100,
1184
- });
1185
- setMetadataProgress(
1186
- hugeNativeMode
1187
- ? { phase: 'Preparing metadata', percent: nativeMetadataStartGate === 'afterGeometryComplete' ? 5 : 0, indeterminate: false }
1188
- : { phase: 'Metadata complete', percent: 100 }
1189
- );
1190
- updateModel(modelId, {
1191
- loadState: hugeNativeMode ? 'hydrating-metadata' : 'complete',
1192
- geometryLoadState: 'complete',
1193
- metadataLoadState: hugeNativeMode ? 'bootstrapping' : 'complete',
1194
- interactiveReady: true,
1195
- cacheState: nativeGeometryCacheHit ? 'hit' : shouldUseNativeCache ? 'writing' : 'none',
1196
- });
1197
- console.log(`[useIfc] Native geometry streaming complete: ${batchCount} batches, ${lastTotalMeshes} meshes`);
1198
- void logToDesktopTerminal(
1199
- 'info',
1200
- `[useIfc] Native stream complete for ${fileName}: stage=${nativeLoadStage} batches=${batchCount}, meshes=${lastTotalMeshes}`
1201
- );
1202
- await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
1203
- if (loadSessionRef.current === currentSession) {
1204
- setGeometryStreamingActive(false);
1205
- }
1206
- break;
1207
- }
1208
- }
1209
-
1210
- nativeStats = geometryProcessor.getLastNativeStats();
1211
-
1212
- const totalElapsedMs = performance.now() - totalStartTime;
1213
- console.log(
1214
- `[useIfc] ✓ ${fileName} (${fileSizeMB.toFixed(1)}MB) → ` +
1215
- `${lastTotalMeshes} meshes, ${(totalVertices / 1000).toFixed(0)}k vertices | ` +
1216
- `first: ${firstGeometryTime.toFixed(0)}ms, total: ${totalElapsedMs.toFixed(0)}ms`
1217
- );
1218
- if (nativeStats) {
1219
- void logToDesktopTerminal(
1220
- 'info',
1221
- `[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`
1222
- );
1223
- }
1224
- if (!metadataParsingStarted) {
1225
- console.warn('[useIfc] Native large-file mode completed without metadata parsing');
1226
- void logToDesktopTerminal('warn', `[useIfc] Native large-file mode completed without metadata parsing for ${fileName}`);
1227
- }
1228
- if (harnessRequest?.waitForMetadataCompletion) {
1229
- if (!metadataParsingStarted) {
1230
- startNativeMetadataParsing();
1231
- }
1232
- if (metadataParsingPromise) {
1233
- await metadataParsingPromise;
1234
- }
1235
- if (metadataSnapshotWritePromise) {
1236
- await metadataSnapshotWritePromise;
1237
- }
1238
- }
1239
- if (firstVisibleGeometryMs === null && firstAppendGeometryBatchMs !== null) {
1240
- await new Promise<void>((resolve) => {
1241
- const fallbackTimer = globalThis.setTimeout(() => {
1242
- if (firstVisibleGeometryMs === null && loadSessionRef.current === currentSession) {
1243
- firstVisibleGeometryMs = firstAppendGeometryBatchMs;
1244
- }
1245
- resolve();
1246
- }, 250);
1247
- requestAnimationFrame(() => {
1248
- globalThis.clearTimeout(fallbackTimer);
1249
- if (firstVisibleGeometryMs === null && loadSessionRef.current === currentSession) {
1250
- firstVisibleGeometryMs = performance.now() - totalStartTime;
1251
- }
1252
- resolve();
1253
- });
1254
- });
1255
- }
1256
- if (hugeNativeMode) {
1257
- setLoading(false);
1258
- }
1259
- const telemetryElapsedMs = performance.now() - totalStartTime;
1260
- await finalizeActiveHarnessRun({
1261
- schemaVersion: 1,
1262
- source: 'desktop-native',
1263
- mode: harnessRequest ? 'startup-harness' : 'manual',
1264
- success: true,
1265
- runLabel: harnessRequest?.runLabel,
1266
- cache: {
1267
- key: nativeCacheKey,
1268
- hit: nativeGeometryCacheHit,
1269
- manifestMeshCount: null,
1270
- manifestShardCount: null,
1271
- },
1272
- file: {
1273
- path: file.path,
1274
- name: file.name,
1275
- sizeBytes: file.size,
1276
- sizeMB: fileSizeMB,
1277
- },
1278
- timings: {
1279
- modelOpenMs,
1280
- firstBatchWaitMs: firstGeometryTime || null,
1281
- firstAppendGeometryBatchMs,
1282
- firstVisibleGeometryMs,
1283
- streamCompleteMs,
1284
- totalWallClockMs: telemetryElapsedMs,
1285
- metadataStartMs,
1286
- metadataReadCompleteMs,
1287
- metadataParseStartMs,
1288
- spatialReadyMs,
1289
- metadataCompleteMs,
1290
- metadataFailedMs,
1291
- metadataReadDurationMs,
1292
- metadataBufferCopyDurationMs,
1293
- metadataParseDurationMs,
1294
- },
1295
- batches: {
1296
- estimatedTotal,
1297
- totalBatches: batchCount,
1298
- totalMeshes: lastTotalMeshes,
1299
- firstBatchMeshes: firstNativeBatchTelemetry?.meshCount ?? null,
1300
- firstPayloadKind: firstNativeBatchTelemetry?.payloadKind ?? null,
1301
- },
1302
- nativeStats: nativeStats
1303
- ? {
1304
- parseTimeMs: nativeStats.parseTimeMs ?? null,
1305
- entityScanTimeMs: nativeStats.entityScanTimeMs ?? null,
1306
- lookupTimeMs: nativeStats.lookupTimeMs ?? null,
1307
- preprocessTimeMs: nativeStats.preprocessTimeMs ?? null,
1308
- geometryTimeMs: nativeStats.geometryTimeMs ?? null,
1309
- totalTimeMs: nativeStats.totalTimeMs ?? null,
1310
- firstChunkReadyTimeMs: nativeStats.firstChunkReadyTimeMs ?? null,
1311
- firstChunkPackTimeMs: nativeStats.firstChunkPackTimeMs ?? null,
1312
- firstChunkEmittedTimeMs: nativeStats.firstChunkEmittedTimeMs ?? null,
1313
- firstChunkEmitTimeMs: nativeStats.firstChunkEmitTimeMs ?? null,
1314
- }
1315
- : null,
1316
- metadata: {
1317
- started: metadataParsingStarted,
1318
- metadataStartMs,
1319
- metadataReadCompleteMs,
1320
- metadataParseStartMs,
1321
- spatialReadyMs,
1322
- metadataCompleteMs,
1323
- metadataFailedMs,
1324
- metadataReadDurationMs,
1325
- metadataBufferCopyDurationMs,
1326
- metadataParseDurationMs,
1327
- },
1328
- firstBatchTelemetry: firstNativeBatchTelemetry
1329
- ? {
1330
- batchSequence: firstNativeBatchTelemetry.batchSequence,
1331
- payloadKind: firstNativeBatchTelemetry.payloadKind,
1332
- meshCount: firstNativeBatchTelemetry.meshCount,
1333
- positionsLen: firstNativeBatchTelemetry.positionsLen,
1334
- normalsLen: firstNativeBatchTelemetry.normalsLen,
1335
- indicesLen: firstNativeBatchTelemetry.indicesLen,
1336
- rustChunkReadyMs: firstNativeBatchTelemetry.chunkReadyTimeMs,
1337
- rustPackMs: firstNativeBatchTelemetry.packTimeMs,
1338
- rustEmittedMs: firstNativeBatchTelemetry.emittedTimeMs,
1339
- rustEmitMs: firstNativeBatchTelemetry.emitTimeMs,
1340
- jsReceivedMs: jsFirstChunkReceivedMs,
1341
- transportToJsMs:
1342
- jsFirstChunkReceivedMs !== null
1343
- ? jsFirstChunkReceivedMs - firstNativeBatchTelemetry.emittedTimeMs
1344
- : null,
1345
- appendAfterReceiveMs:
1346
- jsFirstChunkReceivedMs !== null && firstAppendGeometryBatchMs !== null
1347
- ? firstAppendGeometryBatchMs - jsFirstChunkReceivedMs
1348
- : null,
1349
- visibleAfterAppendMs:
1350
- firstVisibleGeometryMs !== null && firstAppendGeometryBatchMs !== null
1351
- ? firstVisibleGeometryMs - firstAppendGeometryBatchMs
1352
- : null,
1353
- }
1354
- : null,
1355
- });
1356
- if (!hugeNativeMode) {
1357
- setLoading(false);
1358
- }
1359
- return;
1360
- }
1361
371
 
1362
372
  // Read file from disk. The browser path streams files ≥
1363
373
  // STREAM_SAB_THRESHOLD directly into a SharedArrayBuffer, which avoids
1364
374
  // a doubled-peak ArrayBuffer + SAB allocation when the geometry
1365
- // pipeline copies into its own SAB. The native path still reads via
1366
- // Tauri's Rust IPC because it bounds memory differently. (#600)
375
+ // pipeline copies into its own SAB. (#600)
1367
376
  const fileReadStart = performance.now();
1368
- let acquired: AcquiredBuffer;
1369
- if (isNativeFileHandle(file)) {
1370
- const nativeBytes = await readNativeFile(file.path);
1371
- const nativeBuffer = toExactArrayBuffer(nativeBytes);
1372
- acquired = {
1373
- buffer: nativeBuffer,
1374
- view: new Uint8Array(nativeBuffer),
1375
- isShared: false,
1376
- };
1377
- } else {
1378
- acquired = await acquireFileBuffer(file as File);
1379
- }
377
+ const acquired: AcquiredBuffer = await acquireFileBuffer(file);
1380
378
  // `buffer` retains its previous semantics (ArrayBuffer-shaped) for
1381
379
  // every downstream consumer. When `acquired.isShared` is true the
1382
380
  // backing store is a SharedArrayBuffer; downstream code only ever
@@ -1403,7 +401,7 @@ export function useIfcLoader() {
1403
401
  }
1404
402
  setProgress({ phase: `Streaming ${format.toUpperCase()}`, percent: 5 });
1405
403
  setGeometryStreamingActive(false);
1406
- const blob = isNativeFileHandle(file) ? new Blob([buffer]) : (file as File);
404
+ const blob = file;
1407
405
  const incCount = useViewerStore.getState().incrementPointCloudAssetCount;
1408
406
  const ingest = ingestPointCloud({
1409
407
  format,
@@ -1594,7 +592,7 @@ export function useIfcLoader() {
1594
592
  // Only for IFC4 STEP files (server doesn't support IFCX). Native
1595
593
  // file handles (Tauri) don't have an HTTP-uploadable body, so skip
1596
594
  // the server path and fall through to the WASM loader.
1597
- if (target.kind === 'primary' && format === 'ifc' && USE_SERVER && SERVER_URL && SERVER_URL !== '' && !isNativeFileHandle(file)) {
595
+ if (target.kind === 'primary' && format === 'ifc' && USE_SERVER && SERVER_URL && SERVER_URL !== '') {
1598
596
  // Pass buffer directly - server uses File object for parsing, buffer is only for size checks
1599
597
  const serverSuccess = await loadFromServer(file, buffer, () => loadSessionRef.current !== currentSession);
1600
598
  if (serverSuccess) {
@@ -1616,11 +614,6 @@ export function useIfcLoader() {
1616
614
  setGeometryStreamingActive(true);
1617
615
  }
1618
616
 
1619
- const shouldUseDesktopStableWasmGeometry =
1620
- isNativeFileHandle(file)
1621
- && fileName.toLowerCase().endsWith('.ifc')
1622
- && file.size < HUGE_NATIVE_FILE_THRESHOLD;
1623
-
1624
617
  // Initialize geometry processor first (WASM init is fast if already loaded)
1625
618
  const mergeLayersAtLoad = useViewerStore.getState().mergeLayers;
1626
619
  const geometryProcessor = new GeometryProcessor({
@@ -1645,7 +638,7 @@ export function useIfcLoader() {
1645
638
  // available, AND TextDecoder accepts SAB-backed views (Firefox fails
1646
639
  // the third check; we skip the worker path entirely there so the
1647
640
  // SAB allocation isn't wasted).
1648
- const useParserWorker = WorkerParser.isSupported() && !isNativeFileHandle(file);
641
+ const useParserWorker = WorkerParser.isSupported();
1649
642
  let sharedSource: SharedArrayBuffer | null = null;
1650
643
  if (useParserWorker) {
1651
644
  if (acquired.isShared && acquired.buffer instanceof SharedArrayBuffer) {
@@ -1715,7 +708,7 @@ export function useIfcLoader() {
1715
708
  // Same `wasmApi` heuristic as before — desktop loads cannot share
1716
709
  // the geometry processor's WASM instance with the parser without
1717
710
  // risking corruption.
1718
- const parserWasmApi = isNativeFileHandle(file) ? undefined : geometryProcessor.getApi();
711
+ const parserWasmApi = geometryProcessor.getApi();
1719
712
  return new IfcParser().parseColumnar(buffer, {
1720
713
  wasmApi: parserWasmApi ?? undefined,
1721
714
  onSpatialReady: onPartialDataStore,
@@ -1736,7 +729,6 @@ export function useIfcLoader() {
1736
729
  const ADAPTIVE_SYNC_THRESHOLD_MB = 2;
1737
730
  const geometryWillEmitEntityIndex =
1738
731
  useParserWorker
1739
- && !shouldUseDesktopStableWasmGeometry
1740
732
  && fileSizeMB >= ADAPTIVE_SYNC_THRESHOLD_MB;
1741
733
 
1742
734
  const startDataModelParsing = () => {
@@ -1852,9 +844,7 @@ export function useIfcLoader() {
1852
844
  // When the parser worker is in use, hand the geometry workers the
1853
845
  // same SAB so we don't pay the file-bytes copy twice.
1854
846
  const geometryView = sharedSource ? new Uint8Array(sharedSource) : new Uint8Array(buffer);
1855
- const geometryEvents = shouldUseDesktopStableWasmGeometry
1856
- ? geometryProcessor.processStreaming(geometryView, undefined, dynamicBatchConfig)
1857
- : geometryProcessor.processAdaptive(geometryView, {
847
+ const geometryEvents = geometryProcessor.processAdaptive(geometryView, {
1858
848
  sizeThreshold: 2 * 1024 * 1024, // 2MB threshold
1859
849
  batchSize: dynamicBatchConfig, // Dynamic batches: small first, then large
1860
850
  existingSab: sharedSource ?? undefined,
@@ -1884,7 +874,7 @@ export function useIfcLoader() {
1884
874
 
1885
875
  while (true) {
1886
876
  const watchdogMs = getGeometryStreamWatchdogMs(
1887
- shouldUseDesktopStableWasmGeometry,
877
+ false,
1888
878
  batchCount,
1889
879
  fileSizeMB,
1890
880
  );
@@ -2191,37 +1181,6 @@ export function useIfcLoader() {
2191
1181
  loadState: 'error',
2192
1182
  loadError: err instanceof Error ? err.message : String(err),
2193
1183
  });
2194
- if (isNativeFileHandle(file)) {
2195
- const harnessRequest = getActiveHarnessRequest();
2196
- await finalizeActiveHarnessRun({
2197
- schemaVersion: 1,
2198
- source: 'desktop-native',
2199
- mode: harnessRequest ? 'startup-harness' : 'manual',
2200
- success: false,
2201
- runLabel: harnessRequest?.runLabel,
2202
- cache: {
2203
- key: computeNativeCacheKey(file),
2204
- hit: null,
2205
- manifestMeshCount: null,
2206
- manifestShardCount: null,
2207
- },
2208
- file: {
2209
- path: file.path,
2210
- name: file.name,
2211
- sizeBytes: file.size,
2212
- sizeMB: file.size / (1024 * 1024),
2213
- },
2214
- timings: {
2215
- totalWallClockMs: performance.now() - totalStartTime,
2216
- },
2217
- batches: {},
2218
- nativeStats: null,
2219
- metadata: null,
2220
- firstBatchTelemetry: null,
2221
- error: err instanceof Error ? err.message : String(err),
2222
- });
2223
- }
2224
- void logToDesktopTerminal('error', `[useIfc] Load failed: ${err instanceof Error ? err.message : String(err)}`);
2225
1184
  setError(err instanceof Error ? err.message : 'Unknown error');
2226
1185
  setLoading(false);
2227
1186
  setGeometryStreamingActive(false);