@ifc-lite/geometry 1.16.5 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +43 -12
  2. package/dist/geometry-coordinate.d.ts +29 -0
  3. package/dist/geometry-coordinate.d.ts.map +1 -0
  4. package/dist/geometry-coordinate.js +83 -0
  5. package/dist/geometry-coordinate.js.map +1 -0
  6. package/dist/geometry-native.d.ts +51 -0
  7. package/dist/geometry-native.d.ts.map +1 -0
  8. package/dist/geometry-native.js +154 -0
  9. package/dist/geometry-native.js.map +1 -0
  10. package/dist/geometry-parallel.d.ts +23 -0
  11. package/dist/geometry-parallel.d.ts.map +1 -0
  12. package/dist/geometry-parallel.js +193 -0
  13. package/dist/geometry-parallel.js.map +1 -0
  14. package/dist/ifc-lite-mesh-collector.d.ts +1 -0
  15. package/dist/ifc-lite-mesh-collector.d.ts.map +1 -1
  16. package/dist/ifc-lite-mesh-collector.js +1 -0
  17. package/dist/ifc-lite-mesh-collector.js.map +1 -1
  18. package/dist/index.d.ts +18 -9
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +37 -382
  21. package/dist/index.js.map +1 -1
  22. package/dist/native-bridge-conversion.d.ts +71 -0
  23. package/dist/native-bridge-conversion.d.ts.map +1 -0
  24. package/dist/native-bridge-conversion.js +55 -0
  25. package/dist/native-bridge-conversion.js.map +1 -0
  26. package/dist/native-bridge.d.ts.map +1 -1
  27. package/dist/native-bridge.js +2 -139
  28. package/dist/native-bridge.js.map +1 -1
  29. package/dist/packed-geometry-decoder.d.ts +25 -0
  30. package/dist/packed-geometry-decoder.d.ts.map +1 -0
  31. package/dist/packed-geometry-decoder.js +106 -0
  32. package/dist/packed-geometry-decoder.js.map +1 -0
  33. package/dist/types.d.ts +40 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -28,6 +28,10 @@ import { IfcLiteMeshCollector } from './ifc-lite-mesh-collector.js';
28
28
  import { BufferBuilder } from './buffer-builder.js';
29
29
  import { CoordinateHandler } from './coordinate-handler.js';
30
30
  import { createPlatformBridge, isTauri } from './platform-bridge.js';
31
+ // Extracted sub-modules
32
+ import { getStreamingBatchSize, convertMeshCollectionToBatch, convertInstancedCollectionToBatch, withBuildingRotation } from './geometry-coordinate.js';
33
+ import { streamNativeGeometry } from './geometry-native.js';
34
+ import { processParallel } from './geometry-parallel.js';
31
35
  /**
32
36
  * Calculate dynamic batch size based on batch number
33
37
  */
@@ -42,20 +46,8 @@ export function calculateDynamicBatchSize(batchNumber, initialBatchSize = 50, ma
42
46
  return maxBatchSize; // Full throughput earlier
43
47
  }
44
48
  }
45
- const MAX_NATIVE_STREAM_QUEUE_EVENTS = 8;
46
- const MAX_NATIVE_STREAM_QUEUE_MESHES = 32768;
47
- const MAX_NATIVE_STREAM_EVENTS_PER_TURN = 4;
48
- const MAX_NATIVE_STREAM_MESHES_PER_TURN = 8192;
49
- const MAX_NATIVE_STREAM_DRAIN_MS = 10;
50
- function yieldToEventLoop() {
51
- const maybeScheduler = globalThis.scheduler;
52
- if (typeof maybeScheduler?.yield === 'function') {
53
- return maybeScheduler.yield();
54
- }
55
- return new Promise((resolve) => {
56
- globalThis.setTimeout(resolve, 0);
57
- });
58
- }
49
+ // QueuedNativeStreamingEvent, native stream constants, and yieldToEventLoop
50
+ // have been extracted to ./geometry-native.ts
59
51
  export class GeometryProcessor {
60
52
  static largeFileByteStreamingThreshold = 256 * 1024 * 1024;
61
53
  bridge = null;
@@ -185,67 +177,9 @@ export class GeometryProcessor {
185
177
  const buildingRotation = collector.getBuildingRotation();
186
178
  return { meshes, buildingRotation };
187
179
  }
188
- getStreamingBatchSize(buffer, batchConfig) {
189
- if (typeof batchConfig === 'number') {
190
- return batchConfig;
191
- }
192
- const fileSizeMB = batchConfig.fileSizeMB
193
- ? batchConfig.fileSizeMB
194
- : buffer.length / (1024 * 1024);
195
- return fileSizeMB < 10 ? 100
196
- : fileSizeMB < 50 ? 200
197
- : fileSizeMB < 100 ? 300
198
- : fileSizeMB < 300 ? 500
199
- : fileSizeMB < 500 ? 1500
200
- : 3000;
201
- }
202
- convertMeshCollectionToBatch(collection) {
203
- const batch = [];
204
- try {
205
- for (let i = 0; i < collection.length; i++) {
206
- const mesh = collection.get(i);
207
- if (!mesh)
208
- continue;
209
- try {
210
- batch.push({
211
- expressId: mesh.expressId,
212
- ifcType: mesh.ifcType,
213
- positions: mesh.positions,
214
- normals: mesh.normals,
215
- indices: mesh.indices,
216
- color: [mesh.color[0], mesh.color[1], mesh.color[2], mesh.color[3]],
217
- });
218
- }
219
- finally {
220
- mesh.free();
221
- }
222
- }
223
- }
224
- finally {
225
- collection.free();
226
- }
227
- return batch;
228
- }
229
- withBuildingRotation(coordinateInfo, buildingRotation) {
230
- return buildingRotation !== undefined
231
- ? { ...coordinateInfo, buildingRotation }
232
- : coordinateInfo;
233
- }
234
- convertInstancedCollectionToBatch(collection) {
235
- const batch = [];
236
- try {
237
- for (let i = 0; i < collection.length; i++) {
238
- const geometry = collection.get(i);
239
- if (geometry) {
240
- batch.push(geometry);
241
- }
242
- }
243
- }
244
- finally {
245
- collection.free();
246
- }
247
- return batch;
248
- }
180
+ // getStreamingBatchSize, convertMeshCollectionToBatch,
181
+ // convertInstancedCollectionToBatch, and withBuildingRotation have been
182
+ // extracted to ./geometry-coordinate.ts and are used as free functions.
249
183
  async *processStreamingBytes(buffer, batchConfig) {
250
184
  if (!this.bridge) {
251
185
  throw new Error('WASM bridge not initialized');
@@ -266,11 +200,11 @@ export class GeometryProcessor {
266
200
  }
267
201
  const buildingRotation = prePass.buildingRotation ?? undefined;
268
202
  if (!prePass.jobs || prePass.totalJobs === 0) {
269
- const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
203
+ const coordinateInfo = withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
270
204
  yield { type: 'complete', totalMeshes: 0, coordinateInfo };
271
205
  return;
272
206
  }
273
- const batchSize = this.getStreamingBatchSize(buffer, batchConfig);
207
+ const batchSize = getStreamingBatchSize(buffer, batchConfig);
274
208
  // Cap at ~30 batches max to avoid excessive per-batch overhead
275
209
  const maxBatches = 30;
276
210
  const effectiveBatchSize = Math.max(batchSize, Math.ceil(prePass.totalJobs / maxBatches));
@@ -279,7 +213,7 @@ export class GeometryProcessor {
279
213
  const endJob = Math.min(startJob + effectiveBatchSize, prePass.totalJobs);
280
214
  const jobSlice = prePass.jobs.slice(startJob * 3, endJob * 3);
281
215
  const collection = api.processGeometryBatch(buffer, jobSlice, prePass.unitScale, prePass.rtcOffset?.[0] ?? 0, prePass.rtcOffset?.[1] ?? 0, prePass.rtcOffset?.[2] ?? 0, prePass.needsShift, prePass.voidKeys, prePass.voidCounts, prePass.voidValues, prePass.styleIds, prePass.styleColors);
282
- const batch = this.convertMeshCollectionToBatch(collection);
216
+ const batch = convertMeshCollectionToBatch(collection);
283
217
  if (batch.length === 0) {
284
218
  await new Promise(resolve => setTimeout(resolve, 0));
285
219
  continue;
@@ -288,7 +222,7 @@ export class GeometryProcessor {
288
222
  totalMeshes += batch.length;
289
223
  const currentCoordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
290
224
  const coordinateInfo = currentCoordinateInfo
291
- ? this.withBuildingRotation(currentCoordinateInfo, buildingRotation)
225
+ ? withBuildingRotation(currentCoordinateInfo, buildingRotation)
292
226
  : null;
293
227
  yield {
294
228
  type: 'batch',
@@ -299,7 +233,7 @@ export class GeometryProcessor {
299
233
  await new Promise(resolve => setTimeout(resolve, 0));
300
234
  }
301
235
  api.clearPrePassCache?.();
302
- const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
236
+ const coordinateInfo = withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
303
237
  yield { type: 'complete', totalMeshes, coordinateInfo };
304
238
  }
305
239
  async *processInstancedStreamingBytes(buffer, batchSize) {
@@ -311,7 +245,7 @@ export class GeometryProcessor {
311
245
  const buildingRotation = prePass.buildingRotation ?? undefined;
312
246
  yield { type: 'model-open', modelID: 0 };
313
247
  if (!prePass.jobs || prePass.totalJobs === 0) {
314
- const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
248
+ const coordinateInfo = withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
315
249
  yield { type: 'complete', totalGeometries: 0, totalInstances: 0, coordinateInfo };
316
250
  return;
317
251
  }
@@ -324,7 +258,7 @@ export class GeometryProcessor {
324
258
  const endJob = Math.min(startJob + effectiveBatchSize, prePass.totalJobs);
325
259
  const jobSlice = prePass.jobs.slice(startJob * 3, endJob * 3);
326
260
  const collection = api.processInstancedGeometryBatch(buffer, jobSlice, prePass.unitScale, prePass.rtcOffset?.[0] ?? 0, prePass.rtcOffset?.[1] ?? 0, prePass.rtcOffset?.[2] ?? 0, prePass.needsShift, prePass.styleIds, prePass.styleColors);
327
- const batch = this.convertInstancedCollectionToBatch(collection);
261
+ const batch = convertInstancedCollectionToBatch(collection);
328
262
  if (batch.length === 0) {
329
263
  await new Promise(resolve => setTimeout(resolve, 0));
330
264
  continue;
@@ -355,7 +289,7 @@ export class GeometryProcessor {
355
289
  totalInstances += batch.reduce((sum, geometry) => sum + geometry.instance_count, 0);
356
290
  const currentCoordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
357
291
  const coordinateInfo = currentCoordinateInfo
358
- ? this.withBuildingRotation(currentCoordinateInfo, buildingRotation)
292
+ ? withBuildingRotation(currentCoordinateInfo, buildingRotation)
359
293
  : null;
360
294
  yield {
361
295
  type: 'batch',
@@ -366,7 +300,7 @@ export class GeometryProcessor {
366
300
  await new Promise(resolve => setTimeout(resolve, 0));
367
301
  }
368
302
  api.clearPrePassCache?.();
369
- const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
303
+ const coordinateInfo = withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
370
304
  yield { type: 'complete', totalGeometries, totalInstances, coordinateInfo };
371
305
  }
372
306
  /**
@@ -376,7 +310,11 @@ export class GeometryProcessor {
376
310
  * @param entityIndex Optional entity index for priority-based loading
377
311
  * @param batchConfig Dynamic batch configuration or fixed batch size
378
312
  */
379
- async *processStreaming(buffer, _entityIndex, batchConfig = 25) {
313
+ async *processStreaming(buffer, _entityIndex, batchConfig = 25,
314
+ // TODO: sharedRtcOffset is accepted but not yet threaded through to the
315
+ // WASM streaming collector. The WASM layer detects its own RTC offset
316
+ // per-model; federation-level override requires collector API changes.
317
+ sharedRtcOffset) {
380
318
  // Initialize if needed
381
319
  if (this.isNative) {
382
320
  if (!this.platformBridge) {
@@ -394,7 +332,7 @@ export class GeometryProcessor {
394
332
  await new Promise(resolve => setTimeout(resolve, 0));
395
333
  if (this.isNative && this.platformBridge) {
396
334
  yield { type: 'model-open', modelID: 0 };
397
- // NATIVE PATH - Use Tauri streaming
335
+ // NATIVE PATH - Use Tauri streaming (simpler queue without coalescing)
398
336
  console.time('[GeometryProcessor] native-streaming');
399
337
  const queuedEvents = [];
400
338
  let resolvePending = null;
@@ -477,7 +415,7 @@ export class GeometryProcessor {
477
415
  const collector = new IfcLiteMeshCollector(this.bridge.getApi(), content);
478
416
  let totalMeshes = 0;
479
417
  let extractedBuildingRotation = undefined;
480
- const wasmBatchSize = this.getStreamingBatchSize(buffer, batchConfig);
418
+ const wasmBatchSize = getStreamingBatchSize(buffer, batchConfig);
481
419
  // Use WASM batches directly for maximum throughput
482
420
  for await (const item of collector.collectMeshesStreaming(wasmBatchSize)) {
483
421
  // Handle color update events
@@ -526,7 +464,7 @@ export class GeometryProcessor {
526
464
  if (!this.platformBridge?.processGeometryStreamingPath) {
527
465
  throw new Error('Native platform bridge does not support file-path streaming');
528
466
  }
529
- yield* this.streamNativeGeometry((options) => this.platformBridge.processGeometryStreamingPath(path, options, cacheKey), estimatedBytes > 0 ? estimatedBytes / 1000 : 0);
467
+ yield* streamNativeGeometry((options) => this.platformBridge.processGeometryStreamingPath(path, options, cacheKey), estimatedBytes > 0 ? estimatedBytes / 1000 : 0, this.coordinateHandler, (stats) => { this.lastNativeStats = stats; });
530
468
  }
531
469
  async *processStreamingCache(cacheKey) {
532
470
  if (!this.isNative) {
@@ -538,7 +476,7 @@ export class GeometryProcessor {
538
476
  if (!this.platformBridge?.processGeometryStreamingCache) {
539
477
  throw new Error('Native platform bridge does not support cached geometry streaming');
540
478
  }
541
- yield* this.streamNativeGeometry((options) => this.platformBridge.processGeometryStreamingCache(cacheKey, options), 0);
479
+ yield* streamNativeGeometry((options) => this.platformBridge.processGeometryStreamingCache(cacheKey, options), 0, this.coordinateHandler, (stats) => { this.lastNativeStats = stats; });
542
480
  }
543
481
  /**
544
482
  * Process IFC file with streaming instanced geometry output for progressive rendering
@@ -566,7 +504,7 @@ export class GeometryProcessor {
566
504
  // Larger batches = fewer callbacks = less overhead for huge models
567
505
  const fileSizeMB = buffer.length / (1024 * 1024);
568
506
  const effectiveBatchSize = fileSizeMB < 50 ? batchSize : fileSizeMB < 200 ? Math.max(batchSize, 50) : fileSizeMB < 300 ? Math.max(batchSize, 100) : Math.max(batchSize, 200);
569
- const byteBatchSize = Math.max(effectiveBatchSize, this.getStreamingBatchSize(buffer, batchSize));
507
+ const byteBatchSize = Math.max(effectiveBatchSize, getStreamingBatchSize(buffer, batchSize));
570
508
  if (buffer.length >= GeometryProcessor.largeFileByteStreamingThreshold) {
571
509
  yield* this.processInstancedStreamingBytes(buffer, byteBatchSize);
572
510
  return;
@@ -629,181 +567,12 @@ export class GeometryProcessor {
629
567
  *
630
568
  * @param buffer IFC file buffer
631
569
  */
632
- async *processParallel(buffer) {
570
+ async *processParallel(buffer, sharedRtcOffset) {
633
571
  // Initialize if needed
634
572
  if (!this.bridge?.isInitialized()) {
635
573
  await this.init();
636
574
  }
637
- this.coordinateHandler.reset();
638
- yield { type: 'start', totalEstimate: buffer.length / 1000 };
639
- yield { type: 'model-open', modelID: 0 };
640
- // Copy file bytes into SharedArrayBuffer for zero-copy sharing with workers
641
- const sharedBuffer = new SharedArrayBuffer(buffer.byteLength);
642
- new Uint8Array(sharedBuffer).set(buffer);
643
- // ── PHASE 1: Full pre-pass in worker ──
644
- const makeWorker = () => new Worker(new URL('./geometry.worker.js', import.meta.url), { type: 'module' });
645
- const prePassResult = await new Promise((resolve, reject) => {
646
- const w = makeWorker();
647
- w.onmessage = (e) => {
648
- if (e.data.type === 'prepass-result') {
649
- w.terminate();
650
- resolve(e.data.result);
651
- }
652
- else if (e.data.type === 'error') {
653
- w.terminate();
654
- reject(new Error(e.data.message));
655
- }
656
- };
657
- w.onerror = (e) => { w.terminate(); reject(new Error(e.message)); };
658
- w.postMessage({ type: 'prepass', sharedBuffer });
659
- });
660
- if (!prePassResult || !prePassResult.jobs || prePassResult.totalJobs === 0) {
661
- const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
662
- yield { type: 'complete', totalMeshes: 0, coordinateInfo };
663
- return;
664
- }
665
- const { jobs: jobsFlat, totalJobs, unitScale, rtcOffset, needsShift, voidKeys, voidCounts, voidValues, styleIds, styleColors } = prePassResult;
666
- const rtcX = rtcOffset?.[0] ?? 0;
667
- const rtcY = rtcOffset?.[1] ?? 0;
668
- const rtcZ = rtcOffset?.[2] ?? 0;
669
- // ── PHASE 2: Dynamic worker provisioning based on device capability ──
670
- const cores = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 2) : 2;
671
- const deviceMemoryGB = typeof navigator !== 'undefined' ? (navigator.deviceMemory ?? 8) : 8;
672
- const fileSizeGB = buffer.byteLength / (1024 * 1024 * 1024);
673
- // Determine optimal workers:
674
- // - Desktop (16+ cores, 16+ GB): up to 8 workers
675
- // - Laptop (8 cores, 8 GB): 2-4 workers (avoid thermal throttling on fanless)
676
- // - Low-end (4 cores, 4 GB): 1-2 workers
677
- // - Large files need more memory per worker, so fewer workers
678
- let maxWorkers;
679
- if (cores >= 16 && deviceMemoryGB >= 16) {
680
- maxWorkers = Math.min(8, Math.floor(cores / 2));
681
- }
682
- else if (cores >= 8 && deviceMemoryGB >= 8) {
683
- // MacBook Air M-series: 8 cores but fanless → throttles with too many workers
684
- // Use 3 workers: enough parallelism without severe throttling
685
- maxWorkers = fileSizeGB > 0.5 ? 2 : 3;
686
- }
687
- else {
688
- maxWorkers = Math.max(1, Math.min(2, Math.floor(cores / 2)));
689
- }
690
- const workerCount = Math.min(maxWorkers, totalJobs);
691
- const jobsPerWorker = Math.ceil(totalJobs / workerCount);
692
- const chunks = [];
693
- for (let i = 0; i < workerCount; i++) {
694
- const start = i * jobsPerWorker;
695
- const end = Math.min(start + jobsPerWorker, totalJobs);
696
- if (start < end)
697
- chunks.push([start, end]);
698
- }
699
- // Queue-based async generator: workers push batches, generator yields them
700
- const batchQueue = [];
701
- let resolveWaiting = null;
702
- let workersCompleted = 0;
703
- let totalMeshes = 0;
704
- let workerError = null;
705
- const workers = [];
706
- for (let i = 0; i < chunks.length; i++) {
707
- const [jobStart, jobEnd] = chunks[i];
708
- if (jobStart >= jobEnd) {
709
- workersCompleted++;
710
- continue;
711
- }
712
- const workerJobs = jobsFlat.slice(jobStart * 3, jobEnd * 3);
713
- const worker = new Worker(new URL('./geometry.worker.js', import.meta.url), { type: 'module' });
714
- workers.push(worker);
715
- worker.onmessage = (e) => {
716
- const msg = e.data;
717
- if (msg.type === 'batch') {
718
- // Convert transferable data back to MeshData[]
719
- const meshes = msg.meshes.map((m) => ({
720
- expressId: m.expressId,
721
- ifcType: m.ifcType,
722
- positions: m.positions instanceof Float32Array ? m.positions : new Float32Array(m.positions),
723
- normals: m.normals instanceof Float32Array ? m.normals : new Float32Array(m.normals),
724
- indices: m.indices instanceof Uint32Array ? m.indices : new Uint32Array(m.indices),
725
- color: m.color,
726
- }));
727
- if (meshes.length > 0) {
728
- batchQueue.push(meshes);
729
- if (resolveWaiting) {
730
- resolveWaiting();
731
- resolveWaiting = null;
732
- }
733
- }
734
- }
735
- else if (msg.type === 'complete') {
736
- totalMeshes += msg.totalMeshes;
737
- workersCompleted++;
738
- worker.terminate();
739
- if (resolveWaiting) {
740
- resolveWaiting();
741
- resolveWaiting = null;
742
- }
743
- }
744
- else if (msg.type === 'error') {
745
- workerError = new Error(`Geometry worker error: ${msg.message}`);
746
- workersCompleted++;
747
- worker.terminate();
748
- if (resolveWaiting) {
749
- resolveWaiting();
750
- resolveWaiting = null;
751
- }
752
- }
753
- };
754
- worker.onerror = (e) => {
755
- workerError = new Error(`Geometry worker failed: ${e.message}`);
756
- workersCompleted++;
757
- worker.terminate();
758
- if (resolveWaiting) {
759
- resolveWaiting();
760
- resolveWaiting = null;
761
- }
762
- };
763
- // Send work — sharedBuffer is zero-copy, typed arrays are transferred
764
- worker.postMessage({
765
- type: 'process',
766
- sharedBuffer,
767
- jobsFlat: workerJobs,
768
- unitScale,
769
- rtcX, rtcY, rtcZ,
770
- needsShift,
771
- voidKeys, voidCounts, voidValues,
772
- styleIds, styleColors,
773
- });
774
- }
775
- // Yield batches as they arrive from any worker
776
- while (true) {
777
- while (batchQueue.length > 0) {
778
- const batch = batchQueue.shift();
779
- this.coordinateHandler.processMeshesIncremental(batch);
780
- const coordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
781
- yield {
782
- type: 'batch',
783
- meshes: batch,
784
- totalSoFar: totalMeshes,
785
- coordinateInfo: coordinateInfo || undefined,
786
- };
787
- }
788
- if (workerError) {
789
- // Terminate remaining workers
790
- for (const w of workers) {
791
- try {
792
- w.terminate();
793
- }
794
- catch { /* ignore */ }
795
- }
796
- throw workerError;
797
- }
798
- if (workersCompleted >= chunks.length && batchQueue.length === 0) {
799
- break;
800
- }
801
- await new Promise((resolve) => {
802
- resolveWaiting = resolve;
803
- });
804
- }
805
- const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
806
- yield { type: 'complete', totalMeshes, coordinateInfo };
575
+ yield* processParallel(buffer, this.coordinateHandler, sharedRtcOffset);
807
576
  }
808
577
  /**
809
578
  * Adaptive processing: Choose sync or streaming based on file size
@@ -848,6 +617,9 @@ export class GeometryProcessor {
848
617
  const collector = new IfcLiteMeshCollector(this.bridge.getApi(), content);
849
618
  allMeshes = collector.collectMeshes();
850
619
  }
620
+ // NOTE: The sync path (<2MB) does not support sharedRtcOffset override.
621
+ // Infrastructure models with large coordinates are always >2MB and use
622
+ // the parallel/streaming paths where shared RTC is properly threaded.
851
623
  // Process coordinate shifts
852
624
  this.coordinateHandler.processMeshesIncremental(allMeshes);
853
625
  const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
@@ -867,10 +639,10 @@ export class GeometryProcessor {
867
639
  && typeof navigator !== 'undefined'
868
640
  && (navigator.hardwareConcurrency ?? 1) > 1;
869
641
  if (useParallel) {
870
- yield* this.processParallel(buffer);
642
+ yield* this.processParallel(buffer, options.sharedRtcOffset);
871
643
  }
872
644
  else {
873
- yield* this.processStreaming(buffer, options.entityIndex, batchConfig);
645
+ yield* this.processStreaming(buffer, options.entityIndex, batchConfig, options.sharedRtcOffset);
874
646
  }
875
647
  }
876
648
  }
@@ -886,125 +658,8 @@ export class GeometryProcessor {
886
658
  getLastNativeStats() {
887
659
  return this.lastNativeStats;
888
660
  }
889
- enqueueNativeStreamingEvent(queuedEvents, event, queueState) {
890
- if (event.type === 'colorUpdate') {
891
- const lastEvent = queuedEvents[queuedEvents.length - 1];
892
- if (lastEvent?.type === 'colorUpdate') {
893
- for (const [expressId, color] of event.updates) {
894
- lastEvent.updates.set(expressId, color);
895
- }
896
- return;
897
- }
898
- queuedEvents.push(event);
899
- return;
900
- }
901
- const lastEvent = queuedEvents[queuedEvents.length - 1];
902
- const shouldCoalesce = lastEvent?.type === 'batch' &&
903
- (queuedEvents.length >= MAX_NATIVE_STREAM_QUEUE_EVENTS || queueState.queuedMeshes >= MAX_NATIVE_STREAM_QUEUE_MESHES);
904
- if (shouldCoalesce) {
905
- for (let i = 0; i < event.meshes.length; i++) {
906
- lastEvent.meshes.push(event.meshes[i]);
907
- }
908
- lastEvent.nativeTelemetry = event.nativeTelemetry;
909
- queueState.coalescedBatchCount += 1;
910
- }
911
- else {
912
- queuedEvents.push(event);
913
- }
914
- queueState.queuedMeshes += event.meshes.length;
915
- }
916
- async *streamNativeGeometry(startStream, totalEstimate) {
917
- this.coordinateHandler.reset();
918
- yield { type: 'start', totalEstimate };
919
- await yieldToEventLoop();
920
- yield { type: 'model-open', modelID: 0 };
921
- const queuedEvents = [];
922
- const queueState = { queuedMeshes: 0, coalescedBatchCount: 0 };
923
- let resolvePending = null;
924
- let completed = false;
925
- let streamError = null;
926
- let completedTotalMeshes;
927
- let totalMeshes = 0;
928
- const wake = () => {
929
- if (resolvePending) {
930
- resolvePending();
931
- resolvePending = null;
932
- }
933
- };
934
- const streamingPromise = startStream({
935
- onBatch: (batch) => {
936
- this.enqueueNativeStreamingEvent(queuedEvents, { type: 'batch', meshes: batch.meshes, nativeTelemetry: batch.nativeTelemetry }, queueState);
937
- wake();
938
- },
939
- onColorUpdate: (updates) => {
940
- this.enqueueNativeStreamingEvent(queuedEvents, { type: 'colorUpdate', updates: new Map(updates) }, queueState);
941
- wake();
942
- },
943
- onComplete: (stats) => {
944
- this.lastNativeStats = stats;
945
- completedTotalMeshes = stats.totalMeshes;
946
- completed = true;
947
- wake();
948
- },
949
- onError: (error) => {
950
- streamError = error;
951
- completed = true;
952
- wake();
953
- },
954
- });
955
- while (!completed || queuedEvents.length > 0) {
956
- let drainedEventCount = 0;
957
- let drainedMeshCount = 0;
958
- let drainStartedAt = performance.now();
959
- while (queuedEvents.length > 0) {
960
- const event = queuedEvents.shift();
961
- if (event.type === 'colorUpdate') {
962
- yield { type: 'colorUpdate', updates: event.updates };
963
- continue;
964
- }
965
- queueState.queuedMeshes = Math.max(0, queueState.queuedMeshes - event.meshes.length);
966
- // Native desktop streaming already produces site-local geometry, so
967
- // avoid the generic JS RTC/outlier scan on every streamed batch.
968
- this.coordinateHandler.processTrustedMeshesIncremental(event.meshes);
969
- totalMeshes += event.meshes.length;
970
- const coordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
971
- yield {
972
- type: 'batch',
973
- meshes: event.meshes,
974
- totalSoFar: totalMeshes,
975
- coordinateInfo: coordinateInfo || undefined,
976
- nativeTelemetry: event.nativeTelemetry,
977
- };
978
- drainedEventCount += 1;
979
- drainedMeshCount += event.meshes.length;
980
- if (queuedEvents.length > 0) {
981
- const shouldYield = drainedEventCount >= MAX_NATIVE_STREAM_EVENTS_PER_TURN ||
982
- drainedMeshCount >= MAX_NATIVE_STREAM_MESHES_PER_TURN ||
983
- performance.now() - drainStartedAt >= MAX_NATIVE_STREAM_DRAIN_MS;
984
- if (shouldYield) {
985
- await yieldToEventLoop();
986
- drainedEventCount = 0;
987
- drainedMeshCount = 0;
988
- drainStartedAt = performance.now();
989
- }
990
- }
991
- }
992
- if (streamError) {
993
- throw streamError;
994
- }
995
- if (!completed) {
996
- await new Promise((resolve) => {
997
- resolvePending = resolve;
998
- });
999
- }
1000
- }
1001
- await streamingPromise;
1002
- if (queueState.coalescedBatchCount > 0) {
1003
- console.info(`[GeometryProcessor] Coalesced ${queueState.coalescedBatchCount} native batches while JS drained the queue`);
1004
- }
1005
- const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
1006
- yield { type: 'complete', totalMeshes: completedTotalMeshes ?? totalMeshes, coordinateInfo };
1007
- }
661
+ // enqueueNativeStreamingEvent and streamNativeGeometry have been
662
+ // extracted to ./geometry-native.ts
1008
663
  /**
1009
664
  * Parse symbolic representations (Plan, Annotation, FootPrint) from IFC content
1010
665
  * These are pre-authored 2D curves for architectural drawings (door swings, window cuts, etc.)