@ifc-lite/renderer 1.14.3 → 1.14.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/camera-animation.d.ts +10 -4
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +79 -39
- package/dist/camera-animation.js.map +1 -1
- package/dist/camera-controls.d.ts +63 -25
- package/dist/camera-controls.d.ts.map +1 -1
- package/dist/camera-controls.js +284 -186
- package/dist/camera-controls.js.map +1 -1
- package/dist/camera.d.ts +32 -14
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +74 -25
- package/dist/camera.js.map +1 -1
- package/dist/index.d.ts +68 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +412 -51
- package/dist/index.js.map +1 -1
- package/dist/picker.d.ts +7 -0
- package/dist/picker.d.ts.map +1 -1
- package/dist/picker.js +15 -0
- package/dist/picker.js.map +1 -1
- package/dist/picking-manager.d.ts +3 -3
- package/dist/picking-manager.d.ts.map +1 -1
- package/dist/picking-manager.js +4 -4
- package/dist/picking-manager.js.map +1 -1
- package/dist/pipeline.d.ts +14 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +32 -230
- package/dist/pipeline.js.map +1 -1
- package/dist/post-processor.d.ts +7 -0
- package/dist/post-processor.d.ts.map +1 -1
- package/dist/post-processor.js +15 -0
- package/dist/post-processor.js.map +1 -1
- package/dist/scene.d.ts +65 -9
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +482 -127
- package/dist/scene.js.map +1 -1
- package/dist/section-plane.d.ts +6 -0
- package/dist/section-plane.d.ts.map +1 -1
- package/dist/section-plane.js +12 -0
- package/dist/section-plane.js.map +1 -1
- package/dist/shaders/main.wgsl.d.ts +7 -0
- package/dist/shaders/main.wgsl.d.ts.map +1 -0
- package/dist/shaders/main.wgsl.js +239 -0
- package/dist/shaders/main.wgsl.js.map +1 -0
- package/dist/snap-detector.d.ts.map +1 -1
- package/dist/snap-detector.js +20 -6
- package/dist/snap-detector.js.map +1 -1
- package/package.json +4 -4
- package/dist/geometry-manager.d.ts +0 -99
- package/dist/geometry-manager.d.ts.map +0 -1
- package/dist/geometry-manager.js +0 -400
- package/dist/geometry-manager.js.map +0 -1
package/dist/scene.js
CHANGED
|
@@ -8,19 +8,16 @@ let warnedEntityIdRange = false;
|
|
|
8
8
|
export class Scene {
|
|
9
9
|
meshes = [];
|
|
10
10
|
instancedMeshes = [];
|
|
11
|
-
batchedMeshes = [];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
batchedMeshIndex = new Map(); // Map bucketKey -> index in batchedMeshes array (O(1) lookup)
|
|
11
|
+
batchedMeshes = []; // flat render array (rebuilt from buckets)
|
|
12
|
+
buckets = new Map(); // bucketKey -> consolidated bucket state
|
|
13
|
+
meshDataBucket = new Map(); // reverse lookup: MeshData -> owning bucket
|
|
15
14
|
meshDataMap = new Map(); // Map expressId -> MeshData[] (for lazy buffer creation, accumulates multiple pieces)
|
|
16
|
-
meshDataBatchKey = new Map(); // Reverse lookup: MeshData -> bucketKey (O(1) removal in updateMeshColors)
|
|
17
15
|
boundingBoxes = new Map(); // Map expressId -> bounding box (computed lazily)
|
|
18
16
|
// Buffer-size-aware bucket splitting: when a single color group's geometry
|
|
19
17
|
// would exceed the GPU maxBufferSize, overflow is directed to a new
|
|
20
18
|
// sub-bucket with a suffixed key (e.g. "500|500|500|1000#1"). This keeps
|
|
21
19
|
// all downstream maps single-valued and the rendering code unchanged.
|
|
22
20
|
activeBucketKey = new Map(); // base colorKey -> current active bucket key
|
|
23
|
-
bucketVertexBytes = new Map(); // bucket key -> accumulated vertex buffer bytes
|
|
24
21
|
nextSplitId = 0; // Monotonic counter for sub-bucket keys
|
|
25
22
|
cachedMaxBufferSize = 0; // device.limits.maxBufferSize * safety factor (set on first use)
|
|
26
23
|
// Sub-batch cache for partially visible batches (PERFORMANCE FIX)
|
|
@@ -38,6 +35,16 @@ export class Scene {
|
|
|
38
35
|
// Temporary fragment batches created during streaming for immediate rendering.
|
|
39
36
|
// Destroyed and replaced by proper merged batches in finalizeStreaming().
|
|
40
37
|
streamingFragments = [];
|
|
38
|
+
// ─── Mesh command queue ────────────────────────────────────────────
|
|
39
|
+
// Decouples React state updates from GPU work. Callers push meshes
|
|
40
|
+
// via queueMeshes() (instant, no GPU), and the animation loop drains
|
|
41
|
+
// the queue via flushPending() with a per-frame time budget.
|
|
42
|
+
meshQueue = [];
|
|
43
|
+
// ─── GPU-resident mode ──────────────────────────────────────────────
|
|
44
|
+
// After releaseGeometryData(), JS-side typed arrays are freed.
|
|
45
|
+
// Only lightweight metadata is retained for operations that don't need
|
|
46
|
+
// raw vertex data (bounding boxes, color key lookups, expressId sets).
|
|
47
|
+
geometryReleased = false;
|
|
41
48
|
/**
|
|
42
49
|
* Add mesh to scene
|
|
43
50
|
*/
|
|
@@ -216,14 +223,14 @@ export class Scene {
|
|
|
216
223
|
const baseKey = this.colorKey(meshData.color);
|
|
217
224
|
const bucketKey = this.resolveActiveBucket(baseKey, meshData);
|
|
218
225
|
// Accumulate mesh data in the bucket (always — needed for final merge)
|
|
219
|
-
let bucket = this.
|
|
226
|
+
let bucket = this.buckets.get(bucketKey);
|
|
220
227
|
if (!bucket) {
|
|
221
|
-
bucket = [];
|
|
222
|
-
this.
|
|
228
|
+
bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
229
|
+
this.buckets.set(bucketKey, bucket);
|
|
223
230
|
}
|
|
224
|
-
bucket.push(meshData);
|
|
225
|
-
// Track reverse mapping for O(1)
|
|
226
|
-
this.
|
|
231
|
+
bucket.meshData.push(meshData);
|
|
232
|
+
// Track reverse mapping for O(1) bucket lookup in updateMeshColors
|
|
233
|
+
this.meshDataBucket.set(meshData, bucket);
|
|
227
234
|
// Also store individual mesh data for visibility filtering
|
|
228
235
|
this.addMeshData(meshData);
|
|
229
236
|
// Track pending keys for non-streaming rebuild only
|
|
@@ -252,48 +259,31 @@ export class Scene {
|
|
|
252
259
|
if (this.pendingBatchKeys.size === 0)
|
|
253
260
|
return;
|
|
254
261
|
for (const key of this.pendingBatchKeys) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
existingBatch.uniformBuffer.destroy();
|
|
262
|
+
const bucket = this.buckets.get(key);
|
|
263
|
+
// Destroy old GPU batch if it exists
|
|
264
|
+
if (bucket?.batchedMesh) {
|
|
265
|
+
bucket.batchedMesh.vertexBuffer.destroy();
|
|
266
|
+
bucket.batchedMesh.indexBuffer.destroy();
|
|
267
|
+
if (bucket.batchedMesh.uniformBuffer) {
|
|
268
|
+
bucket.batchedMesh.uniformBuffer.destroy();
|
|
263
269
|
}
|
|
270
|
+
bucket.batchedMesh = null;
|
|
264
271
|
}
|
|
265
|
-
if (!
|
|
272
|
+
if (!bucket || bucket.meshData.length === 0) {
|
|
266
273
|
// Bucket is empty — clean up
|
|
267
|
-
this.
|
|
268
|
-
this.batchedMeshData.delete(key);
|
|
269
|
-
this.bucketVertexBytes.delete(key);
|
|
270
|
-
// Swap-remove from flat array using O(1) index lookup
|
|
271
|
-
const arrayIdx = this.batchedMeshIndex.get(key);
|
|
272
|
-
if (arrayIdx !== undefined) {
|
|
273
|
-
const lastIdx = this.batchedMeshes.length - 1;
|
|
274
|
-
if (arrayIdx !== lastIdx) {
|
|
275
|
-
const lastBatch = this.batchedMeshes[lastIdx];
|
|
276
|
-
this.batchedMeshes[arrayIdx] = lastBatch;
|
|
277
|
-
this.batchedMeshIndex.set(lastBatch.colorKey, arrayIdx);
|
|
278
|
-
}
|
|
279
|
-
this.batchedMeshes.pop();
|
|
280
|
-
this.batchedMeshIndex.delete(key);
|
|
281
|
-
}
|
|
274
|
+
this.buckets.delete(key);
|
|
282
275
|
continue;
|
|
283
276
|
}
|
|
284
277
|
// Create new batch with all accumulated meshes for this bucket
|
|
285
|
-
const color =
|
|
286
|
-
const batchedMesh = this.createBatchedMesh(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const newIndex = this.batchedMeshes.length;
|
|
295
|
-
this.batchedMeshes.push(batchedMesh);
|
|
296
|
-
this.batchedMeshIndex.set(key, newIndex);
|
|
278
|
+
const color = bucket.meshData[0].color;
|
|
279
|
+
const batchedMesh = this.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
|
|
280
|
+
bucket.batchedMesh = batchedMesh;
|
|
281
|
+
}
|
|
282
|
+
// Rebuild the flat render array from all buckets (148 max batches — not perf critical)
|
|
283
|
+
this.batchedMeshes = [];
|
|
284
|
+
for (const bucket of this.buckets.values()) {
|
|
285
|
+
if (bucket.batchedMesh) {
|
|
286
|
+
this.batchedMeshes.push(bucket.batchedMesh);
|
|
297
287
|
}
|
|
298
288
|
}
|
|
299
289
|
this.pendingBatchKeys.clear();
|
|
@@ -304,6 +294,39 @@ export class Scene {
|
|
|
304
294
|
hasPendingBatches() {
|
|
305
295
|
return this.pendingBatchKeys.size > 0;
|
|
306
296
|
}
|
|
297
|
+
// ─── Mesh command queue ──────────────────────────────────────────────
|
|
298
|
+
/**
|
|
299
|
+
* Queue meshes for deferred GPU upload.
|
|
300
|
+
* Instant (no GPU work) — safe to call from React effects.
|
|
301
|
+
* The animation loop calls flushPending() each frame to drain the queue.
|
|
302
|
+
*/
|
|
303
|
+
queueMeshes(meshes) {
|
|
304
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
305
|
+
this.meshQueue.push(meshes[i]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** True if the mesh queue has pending work. */
|
|
309
|
+
hasQueuedMeshes() {
|
|
310
|
+
return this.meshQueue.length > 0;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Drain the mesh queue with a per-frame time budget.
|
|
314
|
+
* Processes queued meshes through appendToBatches in streaming mode
|
|
315
|
+
* (creates lightweight fragment batches for immediate rendering).
|
|
316
|
+
*
|
|
317
|
+
* @returns true if any meshes were processed (caller should render)
|
|
318
|
+
*/
|
|
319
|
+
flushPending(device, pipeline) {
|
|
320
|
+
if (this.meshQueue.length === 0)
|
|
321
|
+
return false;
|
|
322
|
+
// Drain the entire queue in one appendToBatches call.
|
|
323
|
+
// The queue coalesces multiple React batches into a single GPU upload,
|
|
324
|
+
// which is already bounded by the WASM→JS batch interval (~50-200ms).
|
|
325
|
+
const meshes = this.meshQueue;
|
|
326
|
+
this.meshQueue = [];
|
|
327
|
+
this.appendToBatches(meshes, device, pipeline, true);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
307
330
|
/**
|
|
308
331
|
* Create lightweight fragment batches from a single streaming batch.
|
|
309
332
|
* Fragments are grouped by color and added to batchedMeshes for immediate
|
|
@@ -348,18 +371,63 @@ export class Scene {
|
|
|
348
371
|
finalizeStreaming(device, pipeline) {
|
|
349
372
|
if (this.streamingFragments.length === 0)
|
|
350
373
|
return;
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
374
|
+
// Save references to old fragments/batches — keep them rendering
|
|
375
|
+
// until the new proper batches are fully built (no visual gap).
|
|
376
|
+
const oldFragments = this.streamingFragments;
|
|
377
|
+
const oldBatches = this.batchedMeshes;
|
|
378
|
+
const fragmentSet = new Set(oldFragments);
|
|
379
|
+
this.streamingFragments = [];
|
|
380
|
+
// 1. Collect ALL accumulated meshData before clearing state
|
|
381
|
+
const allMeshData = [];
|
|
382
|
+
for (const bucket of this.buckets.values()) {
|
|
383
|
+
for (const md of bucket.meshData)
|
|
384
|
+
allMeshData.push(md);
|
|
385
|
+
}
|
|
386
|
+
// 2. Clear all bucket/batch state for a clean rebuild
|
|
387
|
+
// NOTE: batchedMeshes keeps the OLD array reference — the renderer
|
|
388
|
+
// continues to draw from it until we swap in the new array below.
|
|
389
|
+
this.buckets.clear();
|
|
390
|
+
this.meshDataBucket = new Map();
|
|
391
|
+
this.activeBucketKey.clear();
|
|
392
|
+
this.pendingBatchKeys.clear();
|
|
393
|
+
// Destroy cached partial batches — their colorKeys are now stale
|
|
394
|
+
for (const batch of this.partialBatchCache.values()) {
|
|
395
|
+
batch.vertexBuffer.destroy();
|
|
396
|
+
batch.indexBuffer.destroy();
|
|
397
|
+
if (batch.uniformBuffer) {
|
|
398
|
+
batch.uniformBuffer.destroy();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
this.partialBatchCache.clear();
|
|
402
|
+
this.partialBatchCacheKeys.clear();
|
|
403
|
+
// 3. Re-group ALL meshData by their CURRENT color.
|
|
404
|
+
// meshData.color may have been mutated in-place since the mesh was
|
|
405
|
+
// first bucketed, so the original bucket key is stale. Re-grouping
|
|
406
|
+
// by current color ensures batches render with correct colors.
|
|
407
|
+
for (const meshData of allMeshData) {
|
|
408
|
+
const baseKey = this.colorKey(meshData.color);
|
|
409
|
+
const bucketKey = this.resolveActiveBucket(baseKey, meshData);
|
|
410
|
+
let bucket = this.buckets.get(bucketKey);
|
|
411
|
+
if (!bucket) {
|
|
412
|
+
bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
413
|
+
this.buckets.set(bucketKey, bucket);
|
|
414
|
+
}
|
|
415
|
+
bucket.meshData.push(meshData);
|
|
416
|
+
this.meshDataBucket.set(meshData, bucket);
|
|
417
|
+
this.pendingBatchKeys.add(bucketKey);
|
|
418
|
+
}
|
|
419
|
+
// 4. Build new proper batches into a fresh array
|
|
420
|
+
this.batchedMeshes = [];
|
|
421
|
+
this.rebuildPendingBatches(device, pipeline);
|
|
422
|
+
// 5. NOW destroy old fragment/batch GPU resources (new batches are live)
|
|
423
|
+
for (const fragment of oldFragments) {
|
|
354
424
|
fragment.vertexBuffer.destroy();
|
|
355
425
|
fragment.indexBuffer.destroy();
|
|
356
426
|
if (fragment.uniformBuffer) {
|
|
357
427
|
fragment.uniformBuffer.destroy();
|
|
358
428
|
}
|
|
359
429
|
}
|
|
360
|
-
|
|
361
|
-
// 2. Destroy any pre-existing proper batches (non-fragments)
|
|
362
|
-
for (const batch of this.batchedMeshes) {
|
|
430
|
+
for (const batch of oldBatches) {
|
|
363
431
|
if (!fragmentSet.has(batch)) {
|
|
364
432
|
batch.vertexBuffer.destroy();
|
|
365
433
|
batch.indexBuffer.destroy();
|
|
@@ -368,60 +436,207 @@ export class Scene {
|
|
|
368
436
|
}
|
|
369
437
|
}
|
|
370
438
|
}
|
|
371
|
-
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Time-sliced version of finalizeStreaming.
|
|
442
|
+
* Re-groups mesh data and rebuilds GPU batches in small chunks,
|
|
443
|
+
* yielding to the event loop between chunks so orbit/pan stays responsive.
|
|
444
|
+
* Streaming fragments continue rendering until each new batch replaces them.
|
|
445
|
+
*
|
|
446
|
+
* @param device GPU device
|
|
447
|
+
* @param pipeline Render pipeline
|
|
448
|
+
* @param budgetMs Max milliseconds per chunk (default 8 — half a 60fps frame)
|
|
449
|
+
* @returns Promise that resolves when all batches are rebuilt
|
|
450
|
+
*/
|
|
451
|
+
finalizeStreamingAsync(device, pipeline, budgetMs = 8) {
|
|
452
|
+
if (this.streamingFragments.length === 0)
|
|
453
|
+
return Promise.resolve();
|
|
454
|
+
// --- Synchronous preamble (fast O(N) bookkeeping) ---
|
|
455
|
+
const oldFragments = this.streamingFragments;
|
|
456
|
+
const oldBatches = this.batchedMeshes;
|
|
457
|
+
const fragmentSet = new Set(oldFragments);
|
|
458
|
+
this.streamingFragments = [];
|
|
459
|
+
// 1. Collect ALL accumulated meshData
|
|
372
460
|
const allMeshData = [];
|
|
373
|
-
for (const
|
|
374
|
-
for (const md of
|
|
461
|
+
for (const bucket of this.buckets.values()) {
|
|
462
|
+
for (const md of bucket.meshData)
|
|
375
463
|
allMeshData.push(md);
|
|
376
464
|
}
|
|
377
|
-
//
|
|
378
|
-
this.
|
|
379
|
-
this.
|
|
380
|
-
this.batchedMeshIndex.clear();
|
|
381
|
-
this.batchedMeshData.clear();
|
|
382
|
-
this.meshDataBatchKey.clear();
|
|
465
|
+
// 2. Clear bucket/batch state
|
|
466
|
+
this.buckets.clear();
|
|
467
|
+
this.meshDataBucket = new Map();
|
|
383
468
|
this.activeBucketKey.clear();
|
|
384
|
-
this.bucketVertexBytes.clear();
|
|
385
469
|
this.pendingBatchKeys.clear();
|
|
386
|
-
// Destroy cached partial batches — their colorKeys are now stale
|
|
387
470
|
for (const batch of this.partialBatchCache.values()) {
|
|
388
471
|
batch.vertexBuffer.destroy();
|
|
389
472
|
batch.indexBuffer.destroy();
|
|
390
|
-
if (batch.uniformBuffer)
|
|
473
|
+
if (batch.uniformBuffer)
|
|
391
474
|
batch.uniformBuffer.destroy();
|
|
392
|
-
}
|
|
393
475
|
}
|
|
394
476
|
this.partialBatchCache.clear();
|
|
395
477
|
this.partialBatchCacheKeys.clear();
|
|
396
|
-
//
|
|
397
|
-
// meshData.color may have been mutated in-place since the mesh was
|
|
398
|
-
// first bucketed, so the original bucket key is stale. Re-grouping
|
|
399
|
-
// by current color ensures batches render with correct colors.
|
|
478
|
+
// 3. Re-group meshData by current color (fast)
|
|
400
479
|
for (const meshData of allMeshData) {
|
|
401
480
|
const baseKey = this.colorKey(meshData.color);
|
|
402
481
|
const bucketKey = this.resolveActiveBucket(baseKey, meshData);
|
|
403
|
-
let bucket = this.
|
|
482
|
+
let bucket = this.buckets.get(bucketKey);
|
|
404
483
|
if (!bucket) {
|
|
405
|
-
bucket = [];
|
|
406
|
-
this.
|
|
484
|
+
bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
485
|
+
this.buckets.set(bucketKey, bucket);
|
|
407
486
|
}
|
|
408
|
-
bucket.push(meshData);
|
|
409
|
-
this.
|
|
487
|
+
bucket.meshData.push(meshData);
|
|
488
|
+
this.meshDataBucket.set(meshData, bucket);
|
|
410
489
|
this.pendingBatchKeys.add(bucketKey);
|
|
411
490
|
}
|
|
412
|
-
//
|
|
413
|
-
|
|
491
|
+
// Build new batches into a temporary array so the old batchedMeshes
|
|
492
|
+
// (streaming fragments) keep rendering until the swap is complete.
|
|
493
|
+
const newBatches = [];
|
|
494
|
+
const pendingKeys = Array.from(this.pendingBatchKeys);
|
|
495
|
+
this.pendingBatchKeys.clear();
|
|
496
|
+
// --- Async: rebuild batches in time-sliced chunks ---
|
|
497
|
+
let keyIdx = 0;
|
|
498
|
+
const scene = this;
|
|
499
|
+
return new Promise((resolve) => {
|
|
500
|
+
function processChunk() {
|
|
501
|
+
const chunkStart = performance.now();
|
|
502
|
+
while (keyIdx < pendingKeys.length) {
|
|
503
|
+
const key = pendingKeys[keyIdx++];
|
|
504
|
+
const bucket = scene.buckets.get(key);
|
|
505
|
+
if (!bucket || bucket.meshData.length === 0) {
|
|
506
|
+
scene.buckets.delete(key);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const color = bucket.meshData[0].color;
|
|
510
|
+
const batchedMesh = scene.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
|
|
511
|
+
bucket.batchedMesh = batchedMesh;
|
|
512
|
+
newBatches.push(batchedMesh);
|
|
513
|
+
// Check time budget — yield if exceeded
|
|
514
|
+
if (performance.now() - chunkStart >= budgetMs) {
|
|
515
|
+
setTimeout(processChunk, 0);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// All batches built — atomic swap so renderer never sees an empty array
|
|
520
|
+
scene.batchedMeshes = newBatches;
|
|
521
|
+
// Destroy old fragment/batch GPU resources
|
|
522
|
+
for (const fragment of oldFragments) {
|
|
523
|
+
fragment.vertexBuffer.destroy();
|
|
524
|
+
fragment.indexBuffer.destroy();
|
|
525
|
+
if (fragment.uniformBuffer)
|
|
526
|
+
fragment.uniformBuffer.destroy();
|
|
527
|
+
}
|
|
528
|
+
for (const batch of oldBatches) {
|
|
529
|
+
if (!fragmentSet.has(batch)) {
|
|
530
|
+
batch.vertexBuffer.destroy();
|
|
531
|
+
batch.indexBuffer.destroy();
|
|
532
|
+
if (batch.uniformBuffer)
|
|
533
|
+
batch.uniformBuffer.destroy();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
resolve();
|
|
537
|
+
}
|
|
538
|
+
// Start first chunk immediately (no setTimeout delay)
|
|
539
|
+
processChunk();
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Release JS-side mesh geometry data (positions, normals, indices) after
|
|
544
|
+
* GPU batches have been built. This frees the ~1.9GB of typed arrays that
|
|
545
|
+
* duplicate data already resident in GPU vertex/index buffers.
|
|
546
|
+
*
|
|
547
|
+
* After calling this method:
|
|
548
|
+
* - Bounding boxes are precomputed and cached for all entities
|
|
549
|
+
* - meshDataMap and bucket meshData arrays are cleared (typed arrays become GC-eligible)
|
|
550
|
+
* - Color updates (updateMeshColors) are no longer available
|
|
551
|
+
* - Partial batch creation and color overlays are no longer available
|
|
552
|
+
* - CPU raycasting falls back to bounding-box-only (no triangle intersection)
|
|
553
|
+
* - Selection highlighting must use GPU picking instead of CPU mesh reconstruction
|
|
554
|
+
*
|
|
555
|
+
* Call this after finalizeStreaming() when all color updates have been applied.
|
|
556
|
+
*/
|
|
557
|
+
releaseGeometryData() {
|
|
558
|
+
if (this.geometryReleased)
|
|
559
|
+
return;
|
|
560
|
+
// Guard: releasing while async batch work is in-flight would corrupt GPU state
|
|
561
|
+
if (this.pendingBatchKeys.size > 0 || this.streamingFragments.length > 0) {
|
|
562
|
+
console.warn(`[Scene] releaseGeometryData() called with ${this.pendingBatchKeys.size} pending batches ` +
|
|
563
|
+
`and ${this.streamingFragments.length} streaming fragments still in-flight. ` +
|
|
564
|
+
`Call finalizeStreaming()/rebuildPendingBatches() first.`);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// 1. Precompute and cache ALL entity bounding boxes before releasing data
|
|
568
|
+
for (const [expressId, pieces] of this.meshDataMap) {
|
|
569
|
+
if (this.boundingBoxes.has(expressId))
|
|
570
|
+
continue;
|
|
571
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
572
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
573
|
+
for (const piece of pieces) {
|
|
574
|
+
const positions = piece.positions;
|
|
575
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
576
|
+
const x = positions[i];
|
|
577
|
+
const y = positions[i + 1];
|
|
578
|
+
const z = positions[i + 2];
|
|
579
|
+
if (x < minX)
|
|
580
|
+
minX = x;
|
|
581
|
+
if (y < minY)
|
|
582
|
+
minY = y;
|
|
583
|
+
if (z < minZ)
|
|
584
|
+
minZ = z;
|
|
585
|
+
if (x > maxX)
|
|
586
|
+
maxX = x;
|
|
587
|
+
if (y > maxY)
|
|
588
|
+
maxY = y;
|
|
589
|
+
if (z > maxZ)
|
|
590
|
+
maxZ = z;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
this.boundingBoxes.set(expressId, {
|
|
594
|
+
min: { x: minX, y: minY, z: minZ },
|
|
595
|
+
max: { x: maxX, y: maxY, z: maxZ },
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
// 2. Clear the heavy data structures — typed arrays become GC-eligible
|
|
599
|
+
this.meshDataMap.clear();
|
|
600
|
+
// Clear meshData arrays in each bucket (typed arrays become GC-eligible)
|
|
601
|
+
// but keep the bucket shells so batchedMesh references remain valid
|
|
602
|
+
for (const bucket of this.buckets.values()) {
|
|
603
|
+
bucket.meshData = [];
|
|
604
|
+
}
|
|
605
|
+
this.meshDataBucket = new Map();
|
|
606
|
+
this.activeBucketKey.clear();
|
|
607
|
+
// 3. Clear partial batch cache (would need mesh data to rebuild)
|
|
608
|
+
for (const batch of this.partialBatchCache.values()) {
|
|
609
|
+
batch.vertexBuffer.destroy();
|
|
610
|
+
batch.indexBuffer.destroy();
|
|
611
|
+
if (batch.uniformBuffer)
|
|
612
|
+
batch.uniformBuffer.destroy();
|
|
613
|
+
}
|
|
614
|
+
this.partialBatchCache.clear();
|
|
615
|
+
this.partialBatchCacheKeys.clear();
|
|
616
|
+
this.geometryReleased = true;
|
|
617
|
+
console.log(`[Scene] Released JS geometry data. ${this.boundingBoxes.size} bounding boxes cached. ` +
|
|
618
|
+
`${this.batchedMeshes.length} GPU batches retained.`);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Whether JS geometry data has been released (GPU-resident mode).
|
|
622
|
+
*/
|
|
623
|
+
isGeometryDataReleased() {
|
|
624
|
+
return this.geometryReleased;
|
|
414
625
|
}
|
|
415
626
|
/**
|
|
416
627
|
* Update colors for existing meshes and rebuild affected batches
|
|
417
628
|
* Call this when deferred color parsing completes
|
|
418
629
|
*
|
|
419
|
-
* OPTIMIZATION: Uses
|
|
630
|
+
* OPTIMIZATION: Uses meshDataBucket reverse-map for O(1) batch lookup
|
|
420
631
|
* instead of O(N) indexOf scan per mesh. Critical for bulk IDS validation updates.
|
|
421
632
|
*/
|
|
422
633
|
updateMeshColors(updates, device, pipeline) {
|
|
423
634
|
if (updates.size === 0)
|
|
424
635
|
return;
|
|
636
|
+
if (this.geometryReleased) {
|
|
637
|
+
console.warn('[Scene] updateMeshColors called after geometry data was released — skipping.');
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
425
640
|
// Cache max buffer size if not yet set
|
|
426
641
|
if (this.cachedMaxBufferSize === 0) {
|
|
427
642
|
this.cachedMaxBufferSize = this.getMaxBufferSize(device);
|
|
@@ -435,8 +650,9 @@ export class Scene {
|
|
|
435
650
|
continue;
|
|
436
651
|
const newBaseKey = this.colorKey(newColor);
|
|
437
652
|
for (const meshData of meshDataList) {
|
|
438
|
-
// Use reverse-map for O(1) old bucket
|
|
439
|
-
const
|
|
653
|
+
// Use reverse-map for O(1) old bucket lookup
|
|
654
|
+
const oldBucket = this.meshDataBucket.get(meshData);
|
|
655
|
+
const oldBucketKey = oldBucket?.key ?? this.colorKey(meshData.color);
|
|
440
656
|
// Derive old color from bucket key, NOT meshData.color.
|
|
441
657
|
// meshData.color may have been mutated in-place by external code
|
|
442
658
|
// (applyColorUpdatesToMeshes), making it unreliable for change detection.
|
|
@@ -446,37 +662,37 @@ export class Scene {
|
|
|
446
662
|
const newBucketKey = this.resolveActiveBucket(newBaseKey, meshData);
|
|
447
663
|
affectedOldKeys.add(oldBucketKey);
|
|
448
664
|
affectedNewKeys.add(newBucketKey);
|
|
449
|
-
// Remove from old bucket data using
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const idx = oldBatchData.indexOf(meshData);
|
|
665
|
+
// Remove from old bucket data using indexOf (O(N) within one color bucket, typically <100 items)
|
|
666
|
+
if (oldBucket) {
|
|
667
|
+
const idx = oldBucket.meshData.indexOf(meshData);
|
|
453
668
|
if (idx >= 0) {
|
|
454
|
-
// Swap
|
|
455
|
-
const last =
|
|
669
|
+
// Swap-remove for O(1)
|
|
670
|
+
const last = oldBucket.meshData.length - 1;
|
|
456
671
|
if (idx !== last) {
|
|
457
|
-
|
|
672
|
+
oldBucket.meshData[idx] = oldBucket.meshData[last];
|
|
458
673
|
}
|
|
459
|
-
|
|
674
|
+
oldBucket.meshData.pop();
|
|
460
675
|
}
|
|
461
|
-
if (
|
|
462
|
-
this.
|
|
676
|
+
if (oldBucket.meshData.length === 0) {
|
|
677
|
+
this.buckets.delete(oldBucketKey);
|
|
463
678
|
}
|
|
464
679
|
}
|
|
465
680
|
// Decrease old bucket size tracking
|
|
466
681
|
const meshBytes = (meshData.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
|
|
467
|
-
|
|
468
|
-
|
|
682
|
+
if (oldBucket) {
|
|
683
|
+
oldBucket.vertexBytes = Math.max(0, oldBucket.vertexBytes - meshBytes);
|
|
684
|
+
}
|
|
469
685
|
// Update mesh color
|
|
470
686
|
meshData.color = newColor;
|
|
471
687
|
// Add to new bucket data (resolveActiveBucket already updated size tracking)
|
|
472
|
-
let newBucket = this.
|
|
688
|
+
let newBucket = this.buckets.get(newBucketKey);
|
|
473
689
|
if (!newBucket) {
|
|
474
|
-
newBucket = [];
|
|
475
|
-
this.
|
|
690
|
+
newBucket = { key: newBucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
691
|
+
this.buckets.set(newBucketKey, newBucket);
|
|
476
692
|
}
|
|
477
|
-
newBucket.push(meshData);
|
|
693
|
+
newBucket.meshData.push(meshData);
|
|
478
694
|
// Update reverse mapping
|
|
479
|
-
this.
|
|
695
|
+
this.meshDataBucket.set(meshData, newBucket);
|
|
480
696
|
}
|
|
481
697
|
}
|
|
482
698
|
}
|
|
@@ -497,7 +713,7 @@ export class Scene {
|
|
|
497
713
|
* Create a new batched mesh from mesh data array.
|
|
498
714
|
* @param bucketKey - Optional unique key for this batch. When omitted the
|
|
499
715
|
* base color key is used (fine for overlay / partial batches that don't
|
|
500
|
-
* participate in the main
|
|
716
|
+
* participate in the main buckets map).
|
|
501
717
|
*/
|
|
502
718
|
createBatchedMesh(meshDataArray, color, device, pipeline, bucketKey) {
|
|
503
719
|
const merged = this.mergeGeometry(meshDataArray);
|
|
@@ -685,15 +901,21 @@ export class Scene {
|
|
|
685
901
|
*/
|
|
686
902
|
resolveActiveBucket(baseColorKey, meshData) {
|
|
687
903
|
let bucketKey = this.activeBucketKey.get(baseColorKey) ?? baseColorKey;
|
|
688
|
-
const
|
|
904
|
+
const bucket = this.buckets.get(bucketKey);
|
|
905
|
+
const currentBytes = bucket?.vertexBytes ?? 0;
|
|
689
906
|
const meshBytes = (meshData.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
|
|
690
907
|
if (currentBytes > 0 && currentBytes + meshBytes > this.cachedMaxBufferSize) {
|
|
691
908
|
// Overflow — create a new sub-bucket
|
|
692
909
|
bucketKey = `${baseColorKey}#${this.nextSplitId++}`;
|
|
693
910
|
this.activeBucketKey.set(baseColorKey, bucketKey);
|
|
694
911
|
}
|
|
695
|
-
// Update size tracking
|
|
696
|
-
|
|
912
|
+
// Update size tracking on the bucket (create if needed)
|
|
913
|
+
let targetBucket = this.buckets.get(bucketKey);
|
|
914
|
+
if (!targetBucket) {
|
|
915
|
+
targetBucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
916
|
+
this.buckets.set(bucketKey, targetBucket);
|
|
917
|
+
}
|
|
918
|
+
targetBucket.vertexBytes += meshBytes;
|
|
697
919
|
return bucketKey;
|
|
698
920
|
}
|
|
699
921
|
/**
|
|
@@ -717,6 +939,9 @@ export class Scene {
|
|
|
717
939
|
* @returns BatchedMesh containing only visible elements, or undefined if no visible elements
|
|
718
940
|
*/
|
|
719
941
|
getOrCreatePartialBatch(colorKey, visibleIds, device, pipeline) {
|
|
942
|
+
// Cannot create partial batches after geometry data has been released
|
|
943
|
+
if (this.geometryReleased)
|
|
944
|
+
return undefined;
|
|
720
945
|
// Create cache key from colorKey + deterministic hash of all visible IDs
|
|
721
946
|
// Using a proper hash over all IDs to avoid collisions when middle IDs differ
|
|
722
947
|
const sortedIds = Array.from(visibleIds).sort((a, b) => a - b);
|
|
@@ -792,6 +1017,11 @@ export class Scene {
|
|
|
792
1017
|
setColorOverrides(overrides, device, pipeline) {
|
|
793
1018
|
// Destroy previous overlay batches
|
|
794
1019
|
this.destroyOverrideBatches();
|
|
1020
|
+
if (this.geometryReleased) {
|
|
1021
|
+
console.warn('[Scene] setColorOverrides called after geometry data was released — skipping.');
|
|
1022
|
+
this.colorOverrides = null;
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
795
1025
|
if (overrides.size === 0) {
|
|
796
1026
|
this.colorOverrides = null;
|
|
797
1027
|
return;
|
|
@@ -904,23 +1134,47 @@ export class Scene {
|
|
|
904
1134
|
this.meshes = [];
|
|
905
1135
|
this.instancedMeshes = [];
|
|
906
1136
|
this.batchedMeshes = [];
|
|
907
|
-
this.
|
|
908
|
-
this.
|
|
909
|
-
this.batchedMeshIndex.clear();
|
|
1137
|
+
this.buckets.clear();
|
|
1138
|
+
this.meshDataBucket = new Map();
|
|
910
1139
|
this.meshDataMap.clear();
|
|
911
|
-
this.meshDataBatchKey.clear();
|
|
912
1140
|
this.boundingBoxes.clear();
|
|
913
1141
|
this.activeBucketKey.clear();
|
|
914
|
-
this.bucketVertexBytes.clear();
|
|
915
1142
|
this.cachedMaxBufferSize = 0;
|
|
916
1143
|
this.pendingBatchKeys.clear();
|
|
917
1144
|
this.partialBatchCache.clear();
|
|
918
1145
|
this.partialBatchCacheKeys.clear();
|
|
1146
|
+
this.meshQueue = [];
|
|
1147
|
+
this.geometryReleased = false;
|
|
919
1148
|
}
|
|
920
1149
|
/**
|
|
921
1150
|
* Calculate bounding box from actual mesh vertex data
|
|
922
1151
|
*/
|
|
923
1152
|
getBounds() {
|
|
1153
|
+
// When geometry data is released, compute bounds from cached bounding boxes
|
|
1154
|
+
if (this.geometryReleased) {
|
|
1155
|
+
if (this.boundingBoxes.size === 0)
|
|
1156
|
+
return null;
|
|
1157
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
1158
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
1159
|
+
for (const bbox of this.boundingBoxes.values()) {
|
|
1160
|
+
if (bbox.min.x < minX)
|
|
1161
|
+
minX = bbox.min.x;
|
|
1162
|
+
if (bbox.min.y < minY)
|
|
1163
|
+
minY = bbox.min.y;
|
|
1164
|
+
if (bbox.min.z < minZ)
|
|
1165
|
+
minZ = bbox.min.z;
|
|
1166
|
+
if (bbox.max.x > maxX)
|
|
1167
|
+
maxX = bbox.max.x;
|
|
1168
|
+
if (bbox.max.y > maxY)
|
|
1169
|
+
maxY = bbox.max.y;
|
|
1170
|
+
if (bbox.max.z > maxZ)
|
|
1171
|
+
maxZ = bbox.max.z;
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
min: { x: minX, y: minY, z: minZ },
|
|
1175
|
+
max: { x: maxX, y: maxY, z: maxZ },
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
924
1178
|
if (this.meshDataMap.size === 0)
|
|
925
1179
|
return null;
|
|
926
1180
|
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
@@ -960,9 +1214,13 @@ export class Scene {
|
|
|
960
1214
|
};
|
|
961
1215
|
}
|
|
962
1216
|
/**
|
|
963
|
-
* Get all expressIds that have mesh data (for CPU raycasting)
|
|
1217
|
+
* Get all expressIds that have mesh data (for CPU raycasting).
|
|
1218
|
+
* After geometry release, returns expressIds from the cached bounding boxes.
|
|
964
1219
|
*/
|
|
965
1220
|
getAllMeshDataExpressIds() {
|
|
1221
|
+
if (this.geometryReleased) {
|
|
1222
|
+
return Array.from(this.boundingBoxes.keys());
|
|
1223
|
+
}
|
|
966
1224
|
return Array.from(this.meshDataMap.keys());
|
|
967
1225
|
}
|
|
968
1226
|
/**
|
|
@@ -1010,31 +1268,111 @@ export class Scene {
|
|
|
1010
1268
|
return bbox;
|
|
1011
1269
|
}
|
|
1012
1270
|
/**
|
|
1013
|
-
* Ray-box intersection test (slab method)
|
|
1271
|
+
* Ray-box intersection test (slab method).
|
|
1272
|
+
* Handles zero ray direction components (axis-aligned rays) safely.
|
|
1014
1273
|
*/
|
|
1015
1274
|
rayIntersectsBox(rayOrigin, rayDirInv, // 1/rayDir for efficiency
|
|
1016
1275
|
rayDirSign, box) {
|
|
1017
1276
|
const bounds = [box.min, box.max];
|
|
1018
|
-
let tmin =
|
|
1019
|
-
let tmax =
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
tmax =
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
if (
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1277
|
+
let tmin = -Infinity;
|
|
1278
|
+
let tmax = Infinity;
|
|
1279
|
+
// X axis
|
|
1280
|
+
if (!isFinite(rayDirInv.x)) {
|
|
1281
|
+
if (rayOrigin.x < box.min.x || rayOrigin.x > box.max.x)
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
tmin = (bounds[rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
|
|
1286
|
+
tmax = (bounds[1 - rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
|
|
1287
|
+
}
|
|
1288
|
+
// Y axis
|
|
1289
|
+
if (!isFinite(rayDirInv.y)) {
|
|
1290
|
+
if (rayOrigin.y < box.min.y || rayOrigin.y > box.max.y)
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
const tymin = (bounds[rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
|
|
1295
|
+
const tymax = (bounds[1 - rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
|
|
1296
|
+
if (tmin > tymax || tymin > tmax)
|
|
1297
|
+
return false;
|
|
1298
|
+
if (tymin > tmin)
|
|
1299
|
+
tmin = tymin;
|
|
1300
|
+
if (tymax < tmax)
|
|
1301
|
+
tmax = tymax;
|
|
1302
|
+
}
|
|
1303
|
+
// Z axis
|
|
1304
|
+
if (!isFinite(rayDirInv.z)) {
|
|
1305
|
+
if (rayOrigin.z < box.min.z || rayOrigin.z > box.max.z)
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
const tzmin = (bounds[rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
|
|
1310
|
+
const tzmax = (bounds[1 - rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
|
|
1311
|
+
if (tmin > tzmax || tzmin > tmax)
|
|
1312
|
+
return false;
|
|
1313
|
+
if (tzmin > tmin)
|
|
1314
|
+
tmin = tzmin;
|
|
1315
|
+
if (tzmax < tmax)
|
|
1316
|
+
tmax = tzmax;
|
|
1317
|
+
}
|
|
1036
1318
|
return tmax >= 0;
|
|
1037
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Ray-box intersection returning entry distance (tNear).
|
|
1322
|
+
* Returns null if no intersection, otherwise the distance along the ray
|
|
1323
|
+
* to the entry point (clamped to 0 if the ray originates inside the box).
|
|
1324
|
+
* Handles zero ray direction components (axis-aligned rays) safely.
|
|
1325
|
+
*/
|
|
1326
|
+
rayBoxDistance(rayOrigin, rayDirInv, rayDirSign, box) {
|
|
1327
|
+
const bounds = [box.min, box.max];
|
|
1328
|
+
let tmin = -Infinity;
|
|
1329
|
+
let tmax = Infinity;
|
|
1330
|
+
// X axis
|
|
1331
|
+
if (!isFinite(rayDirInv.x)) {
|
|
1332
|
+
// Ray parallel to X: miss if origin outside X slab
|
|
1333
|
+
if (rayOrigin.x < box.min.x || rayOrigin.x > box.max.x)
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
const t1 = (bounds[rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
|
|
1338
|
+
const t2 = (bounds[1 - rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
|
|
1339
|
+
tmin = t1;
|
|
1340
|
+
tmax = t2;
|
|
1341
|
+
}
|
|
1342
|
+
// Y axis
|
|
1343
|
+
if (!isFinite(rayDirInv.y)) {
|
|
1344
|
+
if (rayOrigin.y < box.min.y || rayOrigin.y > box.max.y)
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
const tymin = (bounds[rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
|
|
1349
|
+
const tymax = (bounds[1 - rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
|
|
1350
|
+
if (tmin > tymax || tymin > tmax)
|
|
1351
|
+
return null;
|
|
1352
|
+
if (tymin > tmin)
|
|
1353
|
+
tmin = tymin;
|
|
1354
|
+
if (tymax < tmax)
|
|
1355
|
+
tmax = tymax;
|
|
1356
|
+
}
|
|
1357
|
+
// Z axis
|
|
1358
|
+
if (!isFinite(rayDirInv.z)) {
|
|
1359
|
+
if (rayOrigin.z < box.min.z || rayOrigin.z > box.max.z)
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
const tzmin = (bounds[rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
|
|
1364
|
+
const tzmax = (bounds[1 - rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
|
|
1365
|
+
if (tmin > tzmax || tzmin > tmax)
|
|
1366
|
+
return null;
|
|
1367
|
+
if (tzmin > tmin)
|
|
1368
|
+
tmin = tzmin;
|
|
1369
|
+
if (tzmax < tmax)
|
|
1370
|
+
tmax = tzmax;
|
|
1371
|
+
}
|
|
1372
|
+
if (tmax < 0)
|
|
1373
|
+
return null;
|
|
1374
|
+
return tmin < 0 ? 0 : tmin;
|
|
1375
|
+
}
|
|
1038
1376
|
/**
|
|
1039
1377
|
* Möller–Trumbore ray-triangle intersection
|
|
1040
1378
|
* Returns distance to intersection or null if no hit
|
|
@@ -1080,6 +1418,23 @@ export class Scene {
|
|
|
1080
1418
|
];
|
|
1081
1419
|
let closestHit = null;
|
|
1082
1420
|
let closestDistance = Infinity;
|
|
1421
|
+
// When geometry data has been released, use bounding-box-only raycast.
|
|
1422
|
+
// This is less accurate but uses only the precomputed bounding boxes.
|
|
1423
|
+
// For precise picking, callers should use GPU picking instead.
|
|
1424
|
+
if (this.geometryReleased) {
|
|
1425
|
+
for (const [expressId, bbox] of this.boundingBoxes) {
|
|
1426
|
+
if (hiddenIds?.has(expressId))
|
|
1427
|
+
continue;
|
|
1428
|
+
if (isolatedIds !== null && isolatedIds !== undefined && !isolatedIds.has(expressId))
|
|
1429
|
+
continue;
|
|
1430
|
+
const tNear = this.rayBoxDistance(rayOrigin, rayDirInv, rayDirSign, bbox);
|
|
1431
|
+
if (tNear !== null && tNear < closestDistance) {
|
|
1432
|
+
closestDistance = tNear;
|
|
1433
|
+
closestHit = { expressId, distance: tNear };
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return closestHit;
|
|
1437
|
+
}
|
|
1083
1438
|
// First pass: filter by bounding box (fast)
|
|
1084
1439
|
const candidates = [];
|
|
1085
1440
|
for (const expressId of this.meshDataMap.keys()) {
|