@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.
- package/.turbo/turbo-build.log +34 -41
- package/CHANGELOG.md +10 -0
- package/dist/assets/{basketViewActivator-BNRDNuUJ.js → basketViewActivator-Ce38DhXd.js} +7 -7
- package/dist/assets/{bcf-DCwCuP7n.js → bcf-Cv_O3JfD.js} +1 -1
- package/dist/assets/{deflate-DNGgs8Ur.js → deflate-HbyMq59o.js} +1 -1
- package/dist/assets/drawing-2d-DW98umlt.js +257 -0
- package/dist/assets/{exporters-B9v81gi9.js → exporters-BuD3XRzB.js} +463 -416
- package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
- package/dist/assets/{geotiff-D-YCLS4g.js → geotiff-B2HA8Bwm.js} +10 -10
- package/dist/assets/{ids-CCpq-5d3.js → ids-DYUFMd5f.js} +4 -4
- package/dist/assets/{ifc-lite_bg-DbgS5EUA.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
- package/dist/assets/index-E9wB0zWt.css +1 -0
- package/dist/assets/{index-Bgb3_Pu_.js → index-n5O1QJMM.js} +36808 -39415
- package/dist/assets/{index.es-CWfqZyyr.js → index.es-BKVIpZgL.js} +8 -8
- package/dist/assets/{jpeg-DGOAeUqU.js → jpeg-C7hjKjPX.js} +1 -1
- package/dist/assets/{jspdf.es.min-XPLU2Wkq.js → jspdf.es.min-oWlFc42Y.js} +4 -4
- package/dist/assets/{lerc-1PMSCHwX.js → lerc-BfIOGhQz.js} +1 -1
- package/dist/assets/{lzw-C65U9lNM.js → lzw-B0jRuuW5.js} +1 -1
- package/dist/assets/{native-bridge-XxXos6yI.js → native-bridge-DpB-dtEn.js} +5 -2
- package/dist/assets/{packbits-BdMWXC3m.js → packbits-DVvBTC39.js} +1 -1
- package/dist/assets/{parser.worker-Ddwo3_06.js → parser.worker-BDsWQ6rc.js} +1 -1
- package/dist/assets/{pdf-CRwaZf3s.js → pdf-dVIqI5ac.js} +9 -9
- package/dist/assets/raw-C0ZJYGmN.js +1 -0
- package/dist/assets/{sandbox-0sDo3g3m.js → sandbox-qpJlrNN0.js} +8 -8
- package/dist/assets/{server-client-cTCJ-853.js → server-client-DVZ2huNS.js} +1 -1
- package/dist/assets/{webimage-BtakWX7W.js → webimage-B394g0Tw.js} +1 -1
- package/dist/assets/{xlsx-B1YOg2QB.js → xlsx-D-oHO76J.js} +7 -7
- package/dist/assets/{zstd-CmwsbxmM.js → zstd-Bf38MwV2.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +5 -5
- package/src/App.tsx +1 -3
- package/src/components/viewer/BCFPanel.tsx +1 -16
- package/src/components/viewer/ChatPanel.tsx +11 -46
- package/src/components/viewer/HierarchyPanel.tsx +2 -176
- package/src/components/viewer/IDSPanel.tsx +1 -26
- package/src/components/viewer/MainToolbar.tsx +75 -185
- package/src/components/viewer/MobileToolbar.tsx +1 -9
- package/src/components/viewer/PropertiesPanel.tsx +28 -126
- package/src/components/viewer/ScriptPanel.tsx +8 -34
- package/src/components/viewer/Section2DPanel.tsx +32 -1
- package/src/components/viewer/ViewerLayout.tsx +0 -2
- package/src/components/viewer/ViewportContainer.tsx +24 -42
- package/src/components/viewer/ViewportOverlays.tsx +1 -4
- package/src/components/viewer/useGeometryStreaming.ts +0 -2
- package/src/hooks/ingest/federationAlign.ts +7 -0
- package/src/hooks/useDrawingGeneration.ts +211 -13
- package/src/hooks/useIfcCache.ts +94 -41
- package/src/hooks/useIfcFederation.ts +2 -3
- package/src/hooks/useIfcLoader.ts +10 -1051
- package/src/services/cacheService.ts +9 -25
- package/src/services/desktop-export.ts +2 -59
- package/src/services/file-dialog.ts +8 -142
- package/src/store/constants.ts +23 -0
- package/src/store/index.ts +3 -5
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/visibilitySlice.ts +22 -1
- package/src/store/types.ts +1 -71
- package/src/utils/ifcConfig.ts +0 -12
- package/vite.config.ts +6 -3
- package/DESKTOP_CONTRACT_VERSION +0 -1
- package/dist/assets/drawing-2d-D0dDf6Lh.js +0 -257
- package/dist/assets/event-B0kAzHa-.js +0 -1
- package/dist/assets/geometry.worker-Bpa3115V.js +0 -1
- package/dist/assets/index-BtbXFKsX.css +0 -1
- package/dist/assets/raw-CJgQdyuZ.js +0 -1
- package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
- package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
- package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
- package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
- package/src/components/viewer/SettingsPage.tsx +0 -581
- package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
- package/src/lib/desktop-entitlement.ts +0 -43
- package/src/lib/desktop-product.ts +0 -130
- package/src/lib/platform.ts +0 -23
- package/src/services/desktop-cache.ts +0 -186
- package/src/services/desktop-harness.ts +0 -196
- package/src/services/desktop-logger.ts +0 -20
- package/src/services/desktop-native-metadata.ts +0 -230
- package/src/services/desktop-panel-actions.ts +0 -43
- package/src/services/desktop-preferences.ts +0 -44
- package/src/services/fs-cache.ts +0 -212
- package/src/services/tauri-core-stub.ts +0 -7
- package/src/services/tauri-dialog-stub.ts +0 -7
- package/src/services/tauri-fs-stub.ts +0 -7
- package/src/services/tauri-modules.d.ts +0 -50
- package/src/store/slices/desktopEntitlementSlice.ts +0 -86
- package/src/utils/desktopModelSnapshot.ts +0 -359
- package/src/utils/nativeSpatialDataStore.ts +0 -277
- package/src-tauri/Cargo.toml +0 -29
- package/src-tauri/build.rs +0 -7
- package/src-tauri/capabilities/default.json +0 -18
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +0 -21
- package/src-tauri/src/main.rs +0 -10
- package/src-tauri/tauri.conf.json +0 -39
|
@@ -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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 !== ''
|
|
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()
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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);
|