@ifc-lite/geometry 1.16.4 → 1.16.6

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/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,17 +200,20 @@ 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);
208
+ // Cap at ~30 batches max to avoid excessive per-batch overhead
209
+ const maxBatches = 30;
210
+ const effectiveBatchSize = Math.max(batchSize, Math.ceil(prePass.totalJobs / maxBatches));
274
211
  let totalMeshes = 0;
275
- for (let startJob = 0; startJob < prePass.totalJobs; startJob += batchSize) {
276
- const endJob = Math.min(startJob + batchSize, prePass.totalJobs);
212
+ for (let startJob = 0; startJob < prePass.totalJobs; startJob += effectiveBatchSize) {
213
+ const endJob = Math.min(startJob + effectiveBatchSize, prePass.totalJobs);
277
214
  const jobSlice = prePass.jobs.slice(startJob * 3, endJob * 3);
278
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);
279
- const batch = this.convertMeshCollectionToBatch(collection);
216
+ const batch = convertMeshCollectionToBatch(collection);
280
217
  if (batch.length === 0) {
281
218
  await new Promise(resolve => setTimeout(resolve, 0));
282
219
  continue;
@@ -285,7 +222,7 @@ export class GeometryProcessor {
285
222
  totalMeshes += batch.length;
286
223
  const currentCoordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
287
224
  const coordinateInfo = currentCoordinateInfo
288
- ? this.withBuildingRotation(currentCoordinateInfo, buildingRotation)
225
+ ? withBuildingRotation(currentCoordinateInfo, buildingRotation)
289
226
  : null;
290
227
  yield {
291
228
  type: 'batch',
@@ -295,7 +232,8 @@ export class GeometryProcessor {
295
232
  };
296
233
  await new Promise(resolve => setTimeout(resolve, 0));
297
234
  }
298
- const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
235
+ api.clearPrePassCache?.();
236
+ const coordinateInfo = withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
299
237
  yield { type: 'complete', totalMeshes, coordinateInfo };
300
238
  }
301
239
  async *processInstancedStreamingBytes(buffer, batchSize) {
@@ -307,17 +245,20 @@ export class GeometryProcessor {
307
245
  const buildingRotation = prePass.buildingRotation ?? undefined;
308
246
  yield { type: 'model-open', modelID: 0 };
309
247
  if (!prePass.jobs || prePass.totalJobs === 0) {
310
- const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
248
+ const coordinateInfo = withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
311
249
  yield { type: 'complete', totalGeometries: 0, totalInstances: 0, coordinateInfo };
312
250
  return;
313
251
  }
314
252
  let totalGeometries = 0;
315
253
  let totalInstances = 0;
316
- for (let startJob = 0; startJob < prePass.totalJobs; startJob += batchSize) {
317
- const endJob = Math.min(startJob + batchSize, prePass.totalJobs);
254
+ // Cap at ~30 batches max to avoid excessive per-batch overhead
255
+ const maxBatches = 30;
256
+ const effectiveBatchSize = Math.max(batchSize, Math.ceil(prePass.totalJobs / maxBatches));
257
+ for (let startJob = 0; startJob < prePass.totalJobs; startJob += effectiveBatchSize) {
258
+ const endJob = Math.min(startJob + effectiveBatchSize, prePass.totalJobs);
318
259
  const jobSlice = prePass.jobs.slice(startJob * 3, endJob * 3);
319
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);
320
- const batch = this.convertInstancedCollectionToBatch(collection);
261
+ const batch = convertInstancedCollectionToBatch(collection);
321
262
  if (batch.length === 0) {
322
263
  await new Promise(resolve => setTimeout(resolve, 0));
323
264
  continue;
@@ -348,7 +289,7 @@ export class GeometryProcessor {
348
289
  totalInstances += batch.reduce((sum, geometry) => sum + geometry.instance_count, 0);
349
290
  const currentCoordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
350
291
  const coordinateInfo = currentCoordinateInfo
351
- ? this.withBuildingRotation(currentCoordinateInfo, buildingRotation)
292
+ ? withBuildingRotation(currentCoordinateInfo, buildingRotation)
352
293
  : null;
353
294
  yield {
354
295
  type: 'batch',
@@ -358,7 +299,8 @@ export class GeometryProcessor {
358
299
  };
359
300
  await new Promise(resolve => setTimeout(resolve, 0));
360
301
  }
361
- const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
302
+ api.clearPrePassCache?.();
303
+ const coordinateInfo = withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
362
304
  yield { type: 'complete', totalGeometries, totalInstances, coordinateInfo };
363
305
  }
364
306
  /**
@@ -368,7 +310,11 @@ export class GeometryProcessor {
368
310
  * @param entityIndex Optional entity index for priority-based loading
369
311
  * @param batchConfig Dynamic batch configuration or fixed batch size
370
312
  */
371
- 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) {
372
318
  // Initialize if needed
373
319
  if (this.isNative) {
374
320
  if (!this.platformBridge) {
@@ -386,7 +332,7 @@ export class GeometryProcessor {
386
332
  await new Promise(resolve => setTimeout(resolve, 0));
387
333
  if (this.isNative && this.platformBridge) {
388
334
  yield { type: 'model-open', modelID: 0 };
389
- // NATIVE PATH - Use Tauri streaming
335
+ // NATIVE PATH - Use Tauri streaming (simpler queue without coalescing)
390
336
  console.time('[GeometryProcessor] native-streaming');
391
337
  const queuedEvents = [];
392
338
  let resolvePending = null;
@@ -469,7 +415,7 @@ export class GeometryProcessor {
469
415
  const collector = new IfcLiteMeshCollector(this.bridge.getApi(), content);
470
416
  let totalMeshes = 0;
471
417
  let extractedBuildingRotation = undefined;
472
- const wasmBatchSize = this.getStreamingBatchSize(buffer, batchConfig);
418
+ const wasmBatchSize = getStreamingBatchSize(buffer, batchConfig);
473
419
  // Use WASM batches directly for maximum throughput
474
420
  for await (const item of collector.collectMeshesStreaming(wasmBatchSize)) {
475
421
  // Handle color update events
@@ -518,7 +464,7 @@ export class GeometryProcessor {
518
464
  if (!this.platformBridge?.processGeometryStreamingPath) {
519
465
  throw new Error('Native platform bridge does not support file-path streaming');
520
466
  }
521
- 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; });
522
468
  }
523
469
  async *processStreamingCache(cacheKey) {
524
470
  if (!this.isNative) {
@@ -530,7 +476,7 @@ export class GeometryProcessor {
530
476
  if (!this.platformBridge?.processGeometryStreamingCache) {
531
477
  throw new Error('Native platform bridge does not support cached geometry streaming');
532
478
  }
533
- 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; });
534
480
  }
535
481
  /**
536
482
  * Process IFC file with streaming instanced geometry output for progressive rendering
@@ -558,7 +504,7 @@ export class GeometryProcessor {
558
504
  // Larger batches = fewer callbacks = less overhead for huge models
559
505
  const fileSizeMB = buffer.length / (1024 * 1024);
560
506
  const effectiveBatchSize = fileSizeMB < 50 ? batchSize : fileSizeMB < 200 ? Math.max(batchSize, 50) : fileSizeMB < 300 ? Math.max(batchSize, 100) : Math.max(batchSize, 200);
561
- const byteBatchSize = Math.max(effectiveBatchSize, this.getStreamingBatchSize(buffer, batchSize));
507
+ const byteBatchSize = Math.max(effectiveBatchSize, getStreamingBatchSize(buffer, batchSize));
562
508
  if (buffer.length >= GeometryProcessor.largeFileByteStreamingThreshold) {
563
509
  yield* this.processInstancedStreamingBytes(buffer, byteBatchSize);
564
510
  return;
@@ -621,181 +567,12 @@ export class GeometryProcessor {
621
567
  *
622
568
  * @param buffer IFC file buffer
623
569
  */
624
- async *processParallel(buffer) {
570
+ async *processParallel(buffer, sharedRtcOffset) {
625
571
  // Initialize if needed
626
572
  if (!this.bridge?.isInitialized()) {
627
573
  await this.init();
628
574
  }
629
- this.coordinateHandler.reset();
630
- yield { type: 'start', totalEstimate: buffer.length / 1000 };
631
- yield { type: 'model-open', modelID: 0 };
632
- // Copy file bytes into SharedArrayBuffer for zero-copy sharing with workers
633
- const sharedBuffer = new SharedArrayBuffer(buffer.byteLength);
634
- new Uint8Array(sharedBuffer).set(buffer);
635
- // ── PHASE 1: Full pre-pass in worker ──
636
- const makeWorker = () => new Worker(new URL('./geometry.worker.js', import.meta.url), { type: 'module' });
637
- const prePassResult = await new Promise((resolve, reject) => {
638
- const w = makeWorker();
639
- w.onmessage = (e) => {
640
- if (e.data.type === 'prepass-result') {
641
- w.terminate();
642
- resolve(e.data.result);
643
- }
644
- else if (e.data.type === 'error') {
645
- w.terminate();
646
- reject(new Error(e.data.message));
647
- }
648
- };
649
- w.onerror = (e) => { w.terminate(); reject(new Error(e.message)); };
650
- w.postMessage({ type: 'prepass', sharedBuffer });
651
- });
652
- if (!prePassResult || !prePassResult.jobs || prePassResult.totalJobs === 0) {
653
- const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
654
- yield { type: 'complete', totalMeshes: 0, coordinateInfo };
655
- return;
656
- }
657
- const { jobs: jobsFlat, totalJobs, unitScale, rtcOffset, needsShift, voidKeys, voidCounts, voidValues, styleIds, styleColors } = prePassResult;
658
- const rtcX = rtcOffset?.[0] ?? 0;
659
- const rtcY = rtcOffset?.[1] ?? 0;
660
- const rtcZ = rtcOffset?.[2] ?? 0;
661
- // ── PHASE 2: Dynamic worker provisioning based on device capability ──
662
- const cores = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 2) : 2;
663
- const deviceMemoryGB = typeof navigator !== 'undefined' ? (navigator.deviceMemory ?? 8) : 8;
664
- const fileSizeGB = buffer.byteLength / (1024 * 1024 * 1024);
665
- // Determine optimal workers:
666
- // - Desktop (16+ cores, 16+ GB): up to 8 workers
667
- // - Laptop (8 cores, 8 GB): 2-4 workers (avoid thermal throttling on fanless)
668
- // - Low-end (4 cores, 4 GB): 1-2 workers
669
- // - Large files need more memory per worker, so fewer workers
670
- let maxWorkers;
671
- if (cores >= 16 && deviceMemoryGB >= 16) {
672
- maxWorkers = Math.min(8, Math.floor(cores / 2));
673
- }
674
- else if (cores >= 8 && deviceMemoryGB >= 8) {
675
- // MacBook Air M-series: 8 cores but fanless → throttles with too many workers
676
- // Use 3 workers: enough parallelism without severe throttling
677
- maxWorkers = fileSizeGB > 0.5 ? 2 : 3;
678
- }
679
- else {
680
- maxWorkers = Math.max(1, Math.min(2, Math.floor(cores / 2)));
681
- }
682
- const workerCount = Math.min(maxWorkers, totalJobs);
683
- const jobsPerWorker = Math.ceil(totalJobs / workerCount);
684
- const chunks = [];
685
- for (let i = 0; i < workerCount; i++) {
686
- const start = i * jobsPerWorker;
687
- const end = Math.min(start + jobsPerWorker, totalJobs);
688
- if (start < end)
689
- chunks.push([start, end]);
690
- }
691
- // Queue-based async generator: workers push batches, generator yields them
692
- const batchQueue = [];
693
- let resolveWaiting = null;
694
- let workersCompleted = 0;
695
- let totalMeshes = 0;
696
- let workerError = null;
697
- const workers = [];
698
- for (let i = 0; i < chunks.length; i++) {
699
- const [jobStart, jobEnd] = chunks[i];
700
- if (jobStart >= jobEnd) {
701
- workersCompleted++;
702
- continue;
703
- }
704
- const workerJobs = jobsFlat.slice(jobStart * 3, jobEnd * 3);
705
- const worker = new Worker(new URL('./geometry.worker.js', import.meta.url), { type: 'module' });
706
- workers.push(worker);
707
- worker.onmessage = (e) => {
708
- const msg = e.data;
709
- if (msg.type === 'batch') {
710
- // Convert transferable data back to MeshData[]
711
- const meshes = msg.meshes.map((m) => ({
712
- expressId: m.expressId,
713
- ifcType: m.ifcType,
714
- positions: m.positions instanceof Float32Array ? m.positions : new Float32Array(m.positions),
715
- normals: m.normals instanceof Float32Array ? m.normals : new Float32Array(m.normals),
716
- indices: m.indices instanceof Uint32Array ? m.indices : new Uint32Array(m.indices),
717
- color: m.color,
718
- }));
719
- if (meshes.length > 0) {
720
- batchQueue.push(meshes);
721
- if (resolveWaiting) {
722
- resolveWaiting();
723
- resolveWaiting = null;
724
- }
725
- }
726
- }
727
- else if (msg.type === 'complete') {
728
- totalMeshes += msg.totalMeshes;
729
- workersCompleted++;
730
- worker.terminate();
731
- if (resolveWaiting) {
732
- resolveWaiting();
733
- resolveWaiting = null;
734
- }
735
- }
736
- else if (msg.type === 'error') {
737
- workerError = new Error(`Geometry worker error: ${msg.message}`);
738
- workersCompleted++;
739
- worker.terminate();
740
- if (resolveWaiting) {
741
- resolveWaiting();
742
- resolveWaiting = null;
743
- }
744
- }
745
- };
746
- worker.onerror = (e) => {
747
- workerError = new Error(`Geometry worker failed: ${e.message}`);
748
- workersCompleted++;
749
- worker.terminate();
750
- if (resolveWaiting) {
751
- resolveWaiting();
752
- resolveWaiting = null;
753
- }
754
- };
755
- // Send work — sharedBuffer is zero-copy, typed arrays are transferred
756
- worker.postMessage({
757
- type: 'process',
758
- sharedBuffer,
759
- jobsFlat: workerJobs,
760
- unitScale,
761
- rtcX, rtcY, rtcZ,
762
- needsShift,
763
- voidKeys, voidCounts, voidValues,
764
- styleIds, styleColors,
765
- });
766
- }
767
- // Yield batches as they arrive from any worker
768
- while (true) {
769
- while (batchQueue.length > 0) {
770
- const batch = batchQueue.shift();
771
- this.coordinateHandler.processMeshesIncremental(batch);
772
- const coordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
773
- yield {
774
- type: 'batch',
775
- meshes: batch,
776
- totalSoFar: totalMeshes,
777
- coordinateInfo: coordinateInfo || undefined,
778
- };
779
- }
780
- if (workerError) {
781
- // Terminate remaining workers
782
- for (const w of workers) {
783
- try {
784
- w.terminate();
785
- }
786
- catch { /* ignore */ }
787
- }
788
- throw workerError;
789
- }
790
- if (workersCompleted >= chunks.length && batchQueue.length === 0) {
791
- break;
792
- }
793
- await new Promise((resolve) => {
794
- resolveWaiting = resolve;
795
- });
796
- }
797
- const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
798
- yield { type: 'complete', totalMeshes, coordinateInfo };
575
+ yield* processParallel(buffer, this.coordinateHandler, sharedRtcOffset);
799
576
  }
800
577
  /**
801
578
  * Adaptive processing: Choose sync or streaming based on file size
@@ -840,6 +617,9 @@ export class GeometryProcessor {
840
617
  const collector = new IfcLiteMeshCollector(this.bridge.getApi(), content);
841
618
  allMeshes = collector.collectMeshes();
842
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.
843
623
  // Process coordinate shifts
844
624
  this.coordinateHandler.processMeshesIncremental(allMeshes);
845
625
  const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
@@ -859,10 +639,10 @@ export class GeometryProcessor {
859
639
  && typeof navigator !== 'undefined'
860
640
  && (navigator.hardwareConcurrency ?? 1) > 1;
861
641
  if (useParallel) {
862
- yield* this.processParallel(buffer);
642
+ yield* this.processParallel(buffer, options.sharedRtcOffset);
863
643
  }
864
644
  else {
865
- yield* this.processStreaming(buffer, options.entityIndex, batchConfig);
645
+ yield* this.processStreaming(buffer, options.entityIndex, batchConfig, options.sharedRtcOffset);
866
646
  }
867
647
  }
868
648
  }
@@ -878,125 +658,8 @@ export class GeometryProcessor {
878
658
  getLastNativeStats() {
879
659
  return this.lastNativeStats;
880
660
  }
881
- enqueueNativeStreamingEvent(queuedEvents, event, queueState) {
882
- if (event.type === 'colorUpdate') {
883
- const lastEvent = queuedEvents[queuedEvents.length - 1];
884
- if (lastEvent?.type === 'colorUpdate') {
885
- for (const [expressId, color] of event.updates) {
886
- lastEvent.updates.set(expressId, color);
887
- }
888
- return;
889
- }
890
- queuedEvents.push(event);
891
- return;
892
- }
893
- const lastEvent = queuedEvents[queuedEvents.length - 1];
894
- const shouldCoalesce = lastEvent?.type === 'batch' &&
895
- (queuedEvents.length >= MAX_NATIVE_STREAM_QUEUE_EVENTS || queueState.queuedMeshes >= MAX_NATIVE_STREAM_QUEUE_MESHES);
896
- if (shouldCoalesce) {
897
- for (let i = 0; i < event.meshes.length; i++) {
898
- lastEvent.meshes.push(event.meshes[i]);
899
- }
900
- lastEvent.nativeTelemetry = event.nativeTelemetry;
901
- queueState.coalescedBatchCount += 1;
902
- }
903
- else {
904
- queuedEvents.push(event);
905
- }
906
- queueState.queuedMeshes += event.meshes.length;
907
- }
908
- async *streamNativeGeometry(startStream, totalEstimate) {
909
- this.coordinateHandler.reset();
910
- yield { type: 'start', totalEstimate };
911
- await yieldToEventLoop();
912
- yield { type: 'model-open', modelID: 0 };
913
- const queuedEvents = [];
914
- const queueState = { queuedMeshes: 0, coalescedBatchCount: 0 };
915
- let resolvePending = null;
916
- let completed = false;
917
- let streamError = null;
918
- let completedTotalMeshes;
919
- let totalMeshes = 0;
920
- const wake = () => {
921
- if (resolvePending) {
922
- resolvePending();
923
- resolvePending = null;
924
- }
925
- };
926
- const streamingPromise = startStream({
927
- onBatch: (batch) => {
928
- this.enqueueNativeStreamingEvent(queuedEvents, { type: 'batch', meshes: batch.meshes, nativeTelemetry: batch.nativeTelemetry }, queueState);
929
- wake();
930
- },
931
- onColorUpdate: (updates) => {
932
- this.enqueueNativeStreamingEvent(queuedEvents, { type: 'colorUpdate', updates: new Map(updates) }, queueState);
933
- wake();
934
- },
935
- onComplete: (stats) => {
936
- this.lastNativeStats = stats;
937
- completedTotalMeshes = stats.totalMeshes;
938
- completed = true;
939
- wake();
940
- },
941
- onError: (error) => {
942
- streamError = error;
943
- completed = true;
944
- wake();
945
- },
946
- });
947
- while (!completed || queuedEvents.length > 0) {
948
- let drainedEventCount = 0;
949
- let drainedMeshCount = 0;
950
- let drainStartedAt = performance.now();
951
- while (queuedEvents.length > 0) {
952
- const event = queuedEvents.shift();
953
- if (event.type === 'colorUpdate') {
954
- yield { type: 'colorUpdate', updates: event.updates };
955
- continue;
956
- }
957
- queueState.queuedMeshes = Math.max(0, queueState.queuedMeshes - event.meshes.length);
958
- // Native desktop streaming already produces site-local geometry, so
959
- // avoid the generic JS RTC/outlier scan on every streamed batch.
960
- this.coordinateHandler.processTrustedMeshesIncremental(event.meshes);
961
- totalMeshes += event.meshes.length;
962
- const coordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
963
- yield {
964
- type: 'batch',
965
- meshes: event.meshes,
966
- totalSoFar: totalMeshes,
967
- coordinateInfo: coordinateInfo || undefined,
968
- nativeTelemetry: event.nativeTelemetry,
969
- };
970
- drainedEventCount += 1;
971
- drainedMeshCount += event.meshes.length;
972
- if (queuedEvents.length > 0) {
973
- const shouldYield = drainedEventCount >= MAX_NATIVE_STREAM_EVENTS_PER_TURN ||
974
- drainedMeshCount >= MAX_NATIVE_STREAM_MESHES_PER_TURN ||
975
- performance.now() - drainStartedAt >= MAX_NATIVE_STREAM_DRAIN_MS;
976
- if (shouldYield) {
977
- await yieldToEventLoop();
978
- drainedEventCount = 0;
979
- drainedMeshCount = 0;
980
- drainStartedAt = performance.now();
981
- }
982
- }
983
- }
984
- if (streamError) {
985
- throw streamError;
986
- }
987
- if (!completed) {
988
- await new Promise((resolve) => {
989
- resolvePending = resolve;
990
- });
991
- }
992
- }
993
- await streamingPromise;
994
- if (queueState.coalescedBatchCount > 0) {
995
- console.info(`[GeometryProcessor] Coalesced ${queueState.coalescedBatchCount} native batches while JS drained the queue`);
996
- }
997
- const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
998
- yield { type: 'complete', totalMeshes: completedTotalMeshes ?? totalMeshes, coordinateInfo };
999
- }
661
+ // enqueueNativeStreamingEvent and streamNativeGeometry have been
662
+ // extracted to ./geometry-native.ts
1000
663
  /**
1001
664
  * Parse symbolic representations (Plan, Annotation, FootPrint) from IFC content
1002
665
  * These are pre-authored 2D curves for architectural drawings (door swings, window cuts, etc.)