@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/README.md +43 -12
- package/dist/geometry-coordinate.d.ts +29 -0
- package/dist/geometry-coordinate.d.ts.map +1 -0
- package/dist/geometry-coordinate.js +83 -0
- package/dist/geometry-coordinate.js.map +1 -0
- package/dist/geometry-native.d.ts +51 -0
- package/dist/geometry-native.d.ts.map +1 -0
- package/dist/geometry-native.js +154 -0
- package/dist/geometry-native.js.map +1 -0
- package/dist/geometry-parallel.d.ts +23 -0
- package/dist/geometry-parallel.d.ts.map +1 -0
- package/dist/geometry-parallel.js +193 -0
- package/dist/geometry-parallel.js.map +1 -0
- package/dist/ifc-lite-mesh-collector.d.ts +1 -0
- package/dist/ifc-lite-mesh-collector.d.ts.map +1 -1
- package/dist/ifc-lite-mesh-collector.js +1 -0
- package/dist/ifc-lite-mesh-collector.js.map +1 -1
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -386
- package/dist/index.js.map +1 -1
- package/dist/native-bridge-conversion.d.ts +71 -0
- package/dist/native-bridge-conversion.d.ts.map +1 -0
- package/dist/native-bridge-conversion.js +55 -0
- package/dist/native-bridge-conversion.js.map +1 -0
- package/dist/native-bridge.d.ts.map +1 -1
- package/dist/native-bridge.js +2 -139
- package/dist/native-bridge.js.map +1 -1
- package/dist/packed-geometry-decoder.d.ts +25 -0
- package/dist/packed-geometry-decoder.d.ts.map +1 -0
- package/dist/packed-geometry-decoder.js +106 -0
- package/dist/packed-geometry-decoder.js.map +1 -0
- 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
|
-
|
|
46
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
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 =
|
|
203
|
+
const coordinateInfo = withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
|
|
270
204
|
yield { type: 'complete', totalMeshes: 0, coordinateInfo };
|
|
271
205
|
return;
|
|
272
206
|
}
|
|
273
|
-
const batchSize =
|
|
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 +=
|
|
276
|
-
const endJob = Math.min(startJob +
|
|
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 =
|
|
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
|
-
?
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
317
|
-
|
|
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 =
|
|
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
|
-
?
|
|
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
|
-
|
|
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 =
|
|
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*
|
|
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*
|
|
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,
|
|
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
|
|
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
|
|
882
|
-
|
|
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.)
|