@ifc-lite/geometry 2.2.0 → 2.4.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 +1 -1
- package/dist/geometry-coordinate.d.ts.map +1 -1
- package/dist/geometry-coordinate.js +41 -1
- package/dist/geometry-coordinate.js.map +1 -1
- package/dist/geometry-native.d.ts.map +1 -1
- package/dist/geometry-native.js +53 -41
- package/dist/geometry-native.js.map +1 -1
- package/dist/geometry-parallel.d.ts +8 -0
- package/dist/geometry-parallel.d.ts.map +1 -1
- package/dist/geometry-parallel.js +275 -241
- package/dist/geometry-parallel.js.map +1 -1
- package/dist/geometry.worker.d.ts +16 -1
- package/dist/geometry.worker.d.ts.map +1 -1
- package/dist/geometry.worker.js +106 -33
- package/dist/geometry.worker.js.map +1 -1
- package/dist/ifc-lite-bridge.d.ts +25 -0
- package/dist/ifc-lite-bridge.d.ts.map +1 -1
- package/dist/ifc-lite-bridge.js +51 -0
- package/dist/ifc-lite-bridge.js.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -23
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -102,6 +102,13 @@ existingSab, options) {
|
|
|
102
102
|
// renderer (transferables; already typed arrays from the worker).
|
|
103
103
|
...(m.uvs ? { uvs: m.uvs } : {}),
|
|
104
104
|
...(m.texture ? { texture: m.texture } : {}),
|
|
105
|
+
// Carry the model-diff fingerprint through the worker boundary
|
|
106
|
+
// (issue #924); undefined when hashing is off.
|
|
107
|
+
...(m.geometryHash !== undefined ? { geometryHash: m.geometryHash } : {}),
|
|
108
|
+
// #957 follow-up: carry the Model/Types geometry class through the
|
|
109
|
+
// worker→main re-map (else the viewer's view-mode filter sees only
|
|
110
|
+
// class 0 and the Types view renders nothing).
|
|
111
|
+
...(m.geometryClass !== undefined ? { geometryClass: m.geometryClass } : {}),
|
|
105
112
|
}));
|
|
106
113
|
if (meshes.length > 0) {
|
|
107
114
|
// Update totalMeshes per batch so consumers see a live
|
|
@@ -187,6 +194,12 @@ existingSab, options) {
|
|
|
187
194
|
type: 'set-merge-layers',
|
|
188
195
|
enabled: options?.mergeLayers === true,
|
|
189
196
|
});
|
|
197
|
+
// Issue #924: forward the geometry-hash tolerance the same way — always
|
|
198
|
+
// sent so the controller path stays uniform; null is a cheap no-op.
|
|
199
|
+
worker.postMessage({
|
|
200
|
+
type: 'set-compute-geometry-hashes',
|
|
201
|
+
tolerance: options?.geometryHashTolerance ?? null,
|
|
202
|
+
});
|
|
190
203
|
}
|
|
191
204
|
const sendStreamEnd = () => {
|
|
192
205
|
if (endSentToWorkers)
|
|
@@ -292,285 +305,306 @@ existingSab, options) {
|
|
|
292
305
|
const elapsed = () => Math.round(performance.now() - t0);
|
|
293
306
|
console.log(`[stream] processParallel start, fileSizeMB=${fileSizeMB.toFixed(1)} workerCount=${workerCount}`);
|
|
294
307
|
const prepassWorker = makePrepassWorker();
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
wake();
|
|
312
|
-
return;
|
|
308
|
+
// Wrap the rest of the pipeline so worker teardown runs not only on
|
|
309
|
+
// normal completion / error / zero-jobs branches, but also when the
|
|
310
|
+
// consumer abandons the generator via `.return()` / `.throw()` while it
|
|
311
|
+
// is suspended at a `yield` or the `resolveWaiting` await. The viewer's
|
|
312
|
+
// `watchedGeometryStream` relies on this `finally` to tear down workers
|
|
313
|
+
// on break / abort / watchdog (see boundedIteratorReturn). The existing
|
|
314
|
+
// branch-local `terminate()` calls remain — `terminate()` is idempotent.
|
|
315
|
+
try {
|
|
316
|
+
// Forward the consumer-supplied wasm URL to the pre-pass worker so it
|
|
317
|
+
// doesn't fall back to wasm-bindgen's `import.meta.url` default. The
|
|
318
|
+
// pre-pass worker uses the same `geometry.worker.js` bundle and the
|
|
319
|
+
// legacy (non-threaded) wasm, so `wasmUrls.wasm` is the right key.
|
|
320
|
+
// Skipped entirely when no URL was provided — keeps Vite/webpack
|
|
321
|
+
// consumers on the bundler-native resolution path.
|
|
322
|
+
if (options?.wasmUrls?.wasm) {
|
|
323
|
+
prepassWorker.postMessage({ type: 'init', wasmUrl: options.wasmUrls.wasm });
|
|
313
324
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
buildingRotation: evt.buildingRotation ?? null,
|
|
322
|
-
};
|
|
323
|
-
console.log(`[stream] meta @ ${elapsed()}ms unitScale=${prepassMeta.unitScale} rtc=[${(prepassMeta.rtcOffset[0]).toFixed(0)},${(prepassMeta.rtcOffset[1]).toFixed(0)},${(prepassMeta.rtcOffset[2]).toFixed(0)}]`);
|
|
324
|
-
sendStreamStartIfReady();
|
|
325
|
+
let chunkArrivals = 0;
|
|
326
|
+
let totalDispatchedJobs = 0;
|
|
327
|
+
let firstChunkAt = -1;
|
|
328
|
+
prepassWorker.onmessage = (e) => {
|
|
329
|
+
const data = e.data;
|
|
330
|
+
if (data.type === 'prepass-progress') {
|
|
331
|
+
eventQueue.push({ type: 'progress', phase: 'prepass' });
|
|
325
332
|
wake();
|
|
333
|
+
return;
|
|
326
334
|
}
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
if (data.type === 'prepass-stream') {
|
|
336
|
+
const evt = data.event;
|
|
337
|
+
if (evt.type === 'meta') {
|
|
338
|
+
prepassMeta = {
|
|
339
|
+
unitScale: evt.unitScale,
|
|
340
|
+
rtcOffset: evt.rtcOffset,
|
|
341
|
+
needsShift: evt.needsShift,
|
|
342
|
+
buildingRotation: evt.buildingRotation ?? null,
|
|
343
|
+
};
|
|
344
|
+
console.log(`[stream] meta @ ${elapsed()}ms unitScale=${prepassMeta.unitScale} rtc=[${(prepassMeta.rtcOffset[0]).toFixed(0)},${(prepassMeta.rtcOffset[1]).toFixed(0)},${(prepassMeta.rtcOffset[2]).toFixed(0)}]`);
|
|
345
|
+
sendStreamStartIfReady();
|
|
346
|
+
wake();
|
|
338
347
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const styleColors = evt.styleColors;
|
|
348
|
-
const voidKeys = evt.voidKeys;
|
|
349
|
-
const voidCounts = evt.voidCounts;
|
|
350
|
-
const voidValues = evt.voidValues;
|
|
351
|
-
console.log(`[stream] styles @ ${elapsed()}ms (${styleIds.length} styled, ${voidKeys.length} void hosts), draining ${queuedChunks.length} queued chunks`);
|
|
352
|
-
for (const w of workers) {
|
|
353
|
-
// Slice each typed array per-worker so each can be in its own
|
|
354
|
-
// transfer list without conflict. The slice cost is bounded by
|
|
355
|
-
// `styleIds.length * 4` bytes — under 1 MB for ~250K styles.
|
|
356
|
-
try {
|
|
357
|
-
const sIds = styleIds.slice();
|
|
358
|
-
const sColors = styleColors.slice();
|
|
359
|
-
const vKeys = voidKeys.slice();
|
|
360
|
-
const vCounts = voidCounts.slice();
|
|
361
|
-
const vValues = voidValues.slice();
|
|
362
|
-
w.postMessage({
|
|
363
|
-
type: 'set-styles',
|
|
364
|
-
styleIds: sIds,
|
|
365
|
-
styleColors: sColors,
|
|
366
|
-
voidKeys: vKeys,
|
|
367
|
-
voidCounts: vCounts,
|
|
368
|
-
voidValues: vValues,
|
|
369
|
-
}, [sIds.buffer, sColors.buffer, vKeys.buffer, vCounts.buffer, vValues.buffer]);
|
|
348
|
+
else if (evt.type === 'jobs') {
|
|
349
|
+
const jobsArr = evt.jobs;
|
|
350
|
+
const jobCount = Math.floor(jobsArr.length / 3);
|
|
351
|
+
chunkArrivals++;
|
|
352
|
+
totalDispatchedJobs += jobCount;
|
|
353
|
+
if (firstChunkAt < 0) {
|
|
354
|
+
firstChunkAt = elapsed();
|
|
355
|
+
console.log(`[stream] first jobs chunk @ ${firstChunkAt}ms (${jobCount} jobs)`);
|
|
370
356
|
}
|
|
371
|
-
|
|
372
|
-
console.
|
|
357
|
+
if (chunkArrivals % 10 === 1 || jobCount < 1000) {
|
|
358
|
+
console.log(`[stream] chunk #${chunkArrivals} @ ${elapsed()}ms (+${jobCount} jobs, total ${totalDispatchedJobs})`);
|
|
373
359
|
}
|
|
360
|
+
dispatchJobsChunk(jobsArr);
|
|
374
361
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const ids = evt.ids;
|
|
387
|
-
const starts = evt.starts;
|
|
388
|
-
const lengths = evt.lengths;
|
|
389
|
-
console.log(`[stream] entity-index @ ${elapsed()}ms (${ids.length} entries)`);
|
|
390
|
-
if (typeof SharedArrayBuffer !== 'undefined') {
|
|
391
|
-
// Allocate one SAB triple, copy data once, share across all
|
|
392
|
-
// workers without postMessage clone cost.
|
|
393
|
-
const idsBytes = ids.byteLength;
|
|
394
|
-
const startsBytes = starts.byteLength;
|
|
395
|
-
const lengthsBytes = lengths.byteLength;
|
|
396
|
-
const sabIds = new SharedArrayBuffer(idsBytes);
|
|
397
|
-
const sabStarts = new SharedArrayBuffer(startsBytes);
|
|
398
|
-
const sabLengths = new SharedArrayBuffer(lengthsBytes);
|
|
399
|
-
new Uint32Array(sabIds).set(ids);
|
|
400
|
-
new Uint32Array(sabStarts).set(starts);
|
|
401
|
-
new Uint32Array(sabLengths).set(lengths);
|
|
362
|
+
else if (evt.type === 'styles') {
|
|
363
|
+
// Streaming pre-pass resolved styles + voids after its main scan.
|
|
364
|
+
// Push them into every worker, then drain any chunks that were
|
|
365
|
+
// held waiting for styles. Workers will process every chunk with
|
|
366
|
+
// resolved colors — uniform shading across the whole stream.
|
|
367
|
+
const styleIds = evt.styleIds;
|
|
368
|
+
const styleColors = evt.styleColors;
|
|
369
|
+
const voidKeys = evt.voidKeys;
|
|
370
|
+
const voidCounts = evt.voidCounts;
|
|
371
|
+
const voidValues = evt.voidValues;
|
|
372
|
+
console.log(`[stream] styles @ ${elapsed()}ms (${styleIds.length} styled, ${voidKeys.length} void hosts), draining ${queuedChunks.length} queued chunks`);
|
|
402
373
|
for (const w of workers) {
|
|
374
|
+
// Slice each typed array per-worker so each can be in its own
|
|
375
|
+
// transfer list without conflict. The slice cost is bounded by
|
|
376
|
+
// `styleIds.length * 4` bytes — under 1 MB for ~250K styles.
|
|
403
377
|
try {
|
|
378
|
+
const sIds = styleIds.slice();
|
|
379
|
+
const sColors = styleColors.slice();
|
|
380
|
+
const vKeys = voidKeys.slice();
|
|
381
|
+
const vCounts = voidCounts.slice();
|
|
382
|
+
const vValues = voidValues.slice();
|
|
404
383
|
w.postMessage({
|
|
405
|
-
type: 'set-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
console.warn('[stream] set-entity-index dispatch failed:', err);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
// Hand the same SAB triple to the parser worker (or any other
|
|
416
|
-
// listener) so it can skip its own `scanEntitiesFastBytes` call.
|
|
417
|
-
// Each consumer gets its own Uint32Array view over the shared
|
|
418
|
-
// buffers — no extra copy.
|
|
419
|
-
if (options?.onEntityIndex) {
|
|
420
|
-
try {
|
|
421
|
-
options.onEntityIndex(new Uint32Array(sabIds), new Uint32Array(sabStarts), new Uint32Array(sabLengths));
|
|
384
|
+
type: 'set-styles',
|
|
385
|
+
styleIds: sIds,
|
|
386
|
+
styleColors: sColors,
|
|
387
|
+
voidKeys: vKeys,
|
|
388
|
+
voidCounts: vCounts,
|
|
389
|
+
voidValues: vValues,
|
|
390
|
+
}, [sIds.buffer, sColors.buffer, vKeys.buffer, vCounts.buffer, vValues.buffer]);
|
|
422
391
|
}
|
|
423
392
|
catch (err) {
|
|
424
|
-
console.warn('[stream]
|
|
393
|
+
console.warn('[stream] set-styles dispatch failed:', err);
|
|
425
394
|
}
|
|
426
395
|
}
|
|
396
|
+
stylesReceived = true;
|
|
397
|
+
// Drain only when ALL gates are open (entity-index too). The
|
|
398
|
+
// worker's tail-promise serialiser ensures any set-* runs
|
|
399
|
+
// before any subsequent stream-chunk.
|
|
400
|
+
drainQueuedChunksIfReady();
|
|
427
401
|
}
|
|
428
|
-
else {
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
402
|
+
else if (evt.type === 'entity-index') {
|
|
403
|
+
// Pre-pass exported its built entity_index. Forward to every
|
|
404
|
+
// worker so they skip the ~5 s file re-scan in Rust's lazy
|
|
405
|
+
// build path. SAB sharing for zero-copy distribution to N
|
|
406
|
+
// workers — each gets a Uint32Array view over the same buffer.
|
|
407
|
+
const ids = evt.ids;
|
|
408
|
+
const starts = evt.starts;
|
|
409
|
+
const lengths = evt.lengths;
|
|
410
|
+
console.log(`[stream] entity-index @ ${elapsed()}ms (${ids.length} entries)`);
|
|
411
|
+
if (typeof SharedArrayBuffer !== 'undefined') {
|
|
412
|
+
// Allocate one SAB triple, copy data once, share across all
|
|
413
|
+
// workers without postMessage clone cost.
|
|
414
|
+
const idsBytes = ids.byteLength;
|
|
415
|
+
const startsBytes = starts.byteLength;
|
|
416
|
+
const lengthsBytes = lengths.byteLength;
|
|
417
|
+
const sabIds = new SharedArrayBuffer(idsBytes);
|
|
418
|
+
const sabStarts = new SharedArrayBuffer(startsBytes);
|
|
419
|
+
const sabLengths = new SharedArrayBuffer(lengthsBytes);
|
|
420
|
+
new Uint32Array(sabIds).set(ids);
|
|
421
|
+
new Uint32Array(sabStarts).set(starts);
|
|
422
|
+
new Uint32Array(sabLengths).set(lengths);
|
|
423
|
+
for (const w of workers) {
|
|
424
|
+
try {
|
|
425
|
+
w.postMessage({
|
|
426
|
+
type: 'set-entity-index',
|
|
427
|
+
ids: new Uint32Array(sabIds),
|
|
428
|
+
starts: new Uint32Array(sabStarts),
|
|
429
|
+
lengths: new Uint32Array(sabLengths),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
console.warn('[stream] set-entity-index dispatch failed:', err);
|
|
434
|
+
}
|
|
438
435
|
}
|
|
439
|
-
|
|
440
|
-
|
|
436
|
+
// Hand the same SAB triple to the parser worker (or any other
|
|
437
|
+
// listener) so it can skip its own `scanEntitiesFastBytes` call.
|
|
438
|
+
// Each consumer gets its own Uint32Array view over the shared
|
|
439
|
+
// buffers — no extra copy.
|
|
440
|
+
if (options?.onEntityIndex) {
|
|
441
|
+
try {
|
|
442
|
+
options.onEntityIndex(new Uint32Array(sabIds), new Uint32Array(sabStarts), new Uint32Array(sabLengths));
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
console.warn('[stream] onEntityIndex callback failed:', err);
|
|
446
|
+
}
|
|
441
447
|
}
|
|
442
448
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
449
|
+
else {
|
|
450
|
+
// SAB unavailable — clone per worker via structured clone.
|
|
451
|
+
for (const w of workers) {
|
|
452
|
+
try {
|
|
453
|
+
w.postMessage({
|
|
454
|
+
type: 'set-entity-index',
|
|
455
|
+
ids: ids.slice(),
|
|
456
|
+
starts: starts.slice(),
|
|
457
|
+
lengths: lengths.slice(),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
console.warn('[stream] set-entity-index dispatch failed:', err);
|
|
462
|
+
}
|
|
446
463
|
}
|
|
447
|
-
|
|
448
|
-
|
|
464
|
+
if (options?.onEntityIndex) {
|
|
465
|
+
try {
|
|
466
|
+
options.onEntityIndex(ids.slice(), starts.slice(), lengths.slice());
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
console.warn('[stream] onEntityIndex callback failed:', err);
|
|
470
|
+
}
|
|
449
471
|
}
|
|
450
472
|
}
|
|
473
|
+
entityIndexReceived = true;
|
|
474
|
+
drainQueuedChunksIfReady();
|
|
451
475
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
prepassCompleteSeen = true;
|
|
465
|
-
onPrepassComplete();
|
|
476
|
+
else if (evt.type === 'complete') {
|
|
477
|
+
prepassJobsTotal = evt.totalJobs;
|
|
478
|
+
console.log(`[stream] prepass complete @ ${elapsed()}ms totalJobs=${prepassJobsTotal} chunks=${chunkArrivals}`);
|
|
479
|
+
// Unconditionally drive the prepass-complete handler here.
|
|
480
|
+
// The outer loop's `prepassJobsTotal > 0` gate would skip
|
|
481
|
+
// zero-geometry files (no IFC geometry entities), causing
|
|
482
|
+
// the generator to wait forever. Calling here ensures
|
|
483
|
+
// prepassDone flips even when totalJobs === 0.
|
|
484
|
+
if (!prepassCompleteSeen) {
|
|
485
|
+
prepassCompleteSeen = true;
|
|
486
|
+
onPrepassComplete();
|
|
487
|
+
}
|
|
466
488
|
}
|
|
489
|
+
return;
|
|
467
490
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
491
|
+
if (data.type === 'error') {
|
|
492
|
+
prepassError = new Error(data.message);
|
|
493
|
+
prepassDone = true;
|
|
494
|
+
prepassWorker.terminate();
|
|
495
|
+
wake();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
// The streaming variant doesn't emit `prepass-result` — the streaming
|
|
499
|
+
// worker exits naturally after the JS callback returns from
|
|
500
|
+
// `buildPrePassStreaming`. We treat unknown messages as no-ops.
|
|
501
|
+
};
|
|
502
|
+
prepassWorker.onerror = (e) => {
|
|
503
|
+
prepassError = new Error(`Pre-pass worker failed: ${e.message}`);
|
|
472
504
|
prepassDone = true;
|
|
473
505
|
prepassWorker.terminate();
|
|
474
506
|
wake();
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
while (true) {
|
|
524
|
-
while (eventQueue.length > 0) {
|
|
525
|
-
yield eventQueue.shift();
|
|
526
|
-
}
|
|
527
|
-
if (workerError) {
|
|
528
|
-
for (const w of workers) {
|
|
507
|
+
};
|
|
508
|
+
// Track when the pre-pass worker finishes by listening for either a
|
|
509
|
+
// synthesized "complete" event from the Rust side OR a worker exit. The
|
|
510
|
+
// Rust side currently doesn't post anything after `complete` (it returns
|
|
511
|
+
// from JS), so we close the worker via terminate-on-complete in the host.
|
|
512
|
+
// After we see the Rust `complete` event we can sendStreamEnd.
|
|
513
|
+
const onPrepassComplete = () => {
|
|
514
|
+
prepassDone = true;
|
|
515
|
+
// Only signal stream-end to workers if they actually got
|
|
516
|
+
// stream-start (which gates on `meta`). Zero-geometry files
|
|
517
|
+
// never trigger meta → workers never start → no stream-end
|
|
518
|
+
// needed. The dedicated zero-jobs branch in the outer loop
|
|
519
|
+
// handles their teardown.
|
|
520
|
+
if (streamStartSentToWorkers) {
|
|
521
|
+
sendStreamEnd();
|
|
522
|
+
}
|
|
523
|
+
prepassWorker.terminate();
|
|
524
|
+
wake();
|
|
525
|
+
};
|
|
526
|
+
// Dispatch the streaming pre-pass.
|
|
527
|
+
// chunk_size = 50K is a deliberate compromise:
|
|
528
|
+
// • small enough that the FIRST chunk (always a tiny one — bounded by
|
|
529
|
+
// RTC_SAMPLE_THRESHOLD ≈ 50 jobs from the Rust side) reaches workers
|
|
530
|
+
// within ~1.5 s for fast TTFG;
|
|
531
|
+
// • large enough that subsequent chunks make few Rust→JS callbacks
|
|
532
|
+
// and few worker postMessages — each call into processGeometryBatch
|
|
533
|
+
// has fixed setup cost that compounds badly when invoked 30+ times.
|
|
534
|
+
// Per-chunk fan-out (see `dispatchJobsChunkInternal`) splits each chunk
|
|
535
|
+
// evenly across all workers so parallelism is preserved at every chunk.
|
|
536
|
+
prepassWorker.postMessage({ type: 'prepass-streaming', sharedBuffer, chunkSize: 50_000 });
|
|
537
|
+
// Drain the event queue until the pre-pass and all process workers complete.
|
|
538
|
+
// The pre-pass `complete` event is captured inside the message handler
|
|
539
|
+
// (we set prepassJobsTotal there) but the worker stays alive briefly
|
|
540
|
+
// while the JS callback returns. Detect end-of-stream by:
|
|
541
|
+
// a) `prepassJobsTotal > 0` (or zero-jobs file): pre-pass emitted complete
|
|
542
|
+
// b) all workers reported `complete`
|
|
543
|
+
let prepassCompleteSeen = false;
|
|
544
|
+
while (true) {
|
|
545
|
+
while (eventQueue.length > 0) {
|
|
546
|
+
yield eventQueue.shift();
|
|
547
|
+
}
|
|
548
|
+
if (workerError) {
|
|
549
|
+
for (const w of workers) {
|
|
550
|
+
try {
|
|
551
|
+
w.terminate();
|
|
552
|
+
}
|
|
553
|
+
catch { /* cleanup — safe to ignore */ }
|
|
554
|
+
}
|
|
529
555
|
try {
|
|
530
|
-
|
|
556
|
+
prepassWorker.terminate();
|
|
531
557
|
}
|
|
532
558
|
catch { /* cleanup — safe to ignore */ }
|
|
559
|
+
throw workerError;
|
|
533
560
|
}
|
|
534
|
-
|
|
535
|
-
|
|
561
|
+
if (prepassError) {
|
|
562
|
+
for (const w of workers) {
|
|
563
|
+
try {
|
|
564
|
+
w.terminate();
|
|
565
|
+
}
|
|
566
|
+
catch { /* cleanup — safe to ignore */ }
|
|
567
|
+
}
|
|
568
|
+
throw prepassError;
|
|
536
569
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
570
|
+
// Edge case: pre-pass for a file with zero geometry. The Rust side
|
|
571
|
+
// emits `complete { totalJobs: 0 }`; meta never fired so workers
|
|
572
|
+
// never received stream-start. Tear them down explicitly and yield
|
|
573
|
+
// `complete`. Workers were pre-spawned with `init` so they need an
|
|
574
|
+
// explicit terminate to exit.
|
|
575
|
+
if (prepassDone && !streamStartSentToWorkers && prepassJobsTotal === 0) {
|
|
576
|
+
for (const w of workers) {
|
|
577
|
+
try {
|
|
578
|
+
w.terminate();
|
|
579
|
+
}
|
|
580
|
+
catch { /* cleanup — safe to ignore */ }
|
|
544
581
|
}
|
|
545
|
-
|
|
582
|
+
const coordinateInfo = coordinator.getFinalCoordinateInfo();
|
|
583
|
+
yield { type: 'complete', totalMeshes: 0, coordinateInfo };
|
|
584
|
+
return;
|
|
546
585
|
}
|
|
547
|
-
|
|
586
|
+
if (prepassDone
|
|
587
|
+
&& streamStartSentToWorkers
|
|
588
|
+
&& workersCompleted >= workers.length
|
|
589
|
+
&& eventQueue.length === 0) {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
await new Promise((resolve) => { resolveWaiting = resolve; });
|
|
548
593
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
w.terminate();
|
|
558
|
-
}
|
|
559
|
-
catch { /* cleanup — safe to ignore */ }
|
|
594
|
+
const coordinateInfo = coordinator.getFinalCoordinateInfo();
|
|
595
|
+
yield { type: 'complete', totalMeshes, coordinateInfo };
|
|
596
|
+
}
|
|
597
|
+
finally {
|
|
598
|
+
for (const w of workers) {
|
|
599
|
+
try {
|
|
600
|
+
w.terminate();
|
|
560
601
|
}
|
|
561
|
-
|
|
562
|
-
yield { type: 'complete', totalMeshes: 0, coordinateInfo };
|
|
563
|
-
return;
|
|
602
|
+
catch { /* cleanup — safe to ignore */ }
|
|
564
603
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
&& workersCompleted >= workers.length
|
|
568
|
-
&& eventQueue.length === 0) {
|
|
569
|
-
break;
|
|
604
|
+
try {
|
|
605
|
+
prepassWorker.terminate();
|
|
570
606
|
}
|
|
571
|
-
|
|
607
|
+
catch { /* cleanup — safe to ignore */ }
|
|
572
608
|
}
|
|
573
|
-
const coordinateInfo = coordinator.getFinalCoordinateInfo();
|
|
574
|
-
yield { type: 'complete', totalMeshes, coordinateInfo };
|
|
575
609
|
}
|
|
576
610
|
//# sourceMappingURL=geometry-parallel.js.map
|