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