@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.
- 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 +37 -382
- 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/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -1
- 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,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 =
|
|
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);
|
|
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 =
|
|
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
|
-
?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
?
|
|
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 =
|
|
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 =
|
|
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*
|
|
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*
|
|
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,
|
|
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
|
|
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
|
|
890
|
-
|
|
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.)
|