@ifc-lite/renderer 1.14.4 → 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 +59 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +382 -51
- package/dist/index.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/scene.d.ts +34 -9
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +242 -120
- package/dist/scene.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,20 +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
|
-
meshDataBatchIdx = new Map(); // Reverse lookup: MeshData -> index in bucket array (O(1) removal)
|
|
18
15
|
boundingBoxes = new Map(); // Map expressId -> bounding box (computed lazily)
|
|
19
16
|
// Buffer-size-aware bucket splitting: when a single color group's geometry
|
|
20
17
|
// would exceed the GPU maxBufferSize, overflow is directed to a new
|
|
21
18
|
// sub-bucket with a suffixed key (e.g. "500|500|500|1000#1"). This keeps
|
|
22
19
|
// all downstream maps single-valued and the rendering code unchanged.
|
|
23
20
|
activeBucketKey = new Map(); // base colorKey -> current active bucket key
|
|
24
|
-
bucketVertexBytes = new Map(); // bucket key -> accumulated vertex buffer bytes
|
|
25
21
|
nextSplitId = 0; // Monotonic counter for sub-bucket keys
|
|
26
22
|
cachedMaxBufferSize = 0; // device.limits.maxBufferSize * safety factor (set on first use)
|
|
27
23
|
// Sub-batch cache for partially visible batches (PERFORMANCE FIX)
|
|
@@ -39,6 +35,11 @@ export class Scene {
|
|
|
39
35
|
// Temporary fragment batches created during streaming for immediate rendering.
|
|
40
36
|
// Destroyed and replaced by proper merged batches in finalizeStreaming().
|
|
41
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 = [];
|
|
42
43
|
// ─── GPU-resident mode ──────────────────────────────────────────────
|
|
43
44
|
// After releaseGeometryData(), JS-side typed arrays are freed.
|
|
44
45
|
// Only lightweight metadata is retained for operations that don't need
|
|
@@ -222,15 +223,14 @@ export class Scene {
|
|
|
222
223
|
const baseKey = this.colorKey(meshData.color);
|
|
223
224
|
const bucketKey = this.resolveActiveBucket(baseKey, meshData);
|
|
224
225
|
// Accumulate mesh data in the bucket (always — needed for final merge)
|
|
225
|
-
let bucket = this.
|
|
226
|
+
let bucket = this.buckets.get(bucketKey);
|
|
226
227
|
if (!bucket) {
|
|
227
|
-
bucket = [];
|
|
228
|
-
this.
|
|
228
|
+
bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
229
|
+
this.buckets.set(bucketKey, bucket);
|
|
229
230
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.meshDataBatchKey.set(meshData, bucketKey);
|
|
231
|
+
bucket.meshData.push(meshData);
|
|
232
|
+
// Track reverse mapping for O(1) bucket lookup in updateMeshColors
|
|
233
|
+
this.meshDataBucket.set(meshData, bucket);
|
|
234
234
|
// Also store individual mesh data for visibility filtering
|
|
235
235
|
this.addMeshData(meshData);
|
|
236
236
|
// Track pending keys for non-streaming rebuild only
|
|
@@ -259,48 +259,31 @@ export class Scene {
|
|
|
259
259
|
if (this.pendingBatchKeys.size === 0)
|
|
260
260
|
return;
|
|
261
261
|
for (const key of this.pendingBatchKeys) {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
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();
|
|
270
269
|
}
|
|
270
|
+
bucket.batchedMesh = null;
|
|
271
271
|
}
|
|
272
|
-
if (!
|
|
272
|
+
if (!bucket || bucket.meshData.length === 0) {
|
|
273
273
|
// Bucket is empty — clean up
|
|
274
|
-
this.
|
|
275
|
-
this.batchedMeshData.delete(key);
|
|
276
|
-
this.bucketVertexBytes.delete(key);
|
|
277
|
-
// Swap-remove from flat array using O(1) index lookup
|
|
278
|
-
const arrayIdx = this.batchedMeshIndex.get(key);
|
|
279
|
-
if (arrayIdx !== undefined) {
|
|
280
|
-
const lastIdx = this.batchedMeshes.length - 1;
|
|
281
|
-
if (arrayIdx !== lastIdx) {
|
|
282
|
-
const lastBatch = this.batchedMeshes[lastIdx];
|
|
283
|
-
this.batchedMeshes[arrayIdx] = lastBatch;
|
|
284
|
-
this.batchedMeshIndex.set(lastBatch.colorKey, arrayIdx);
|
|
285
|
-
}
|
|
286
|
-
this.batchedMeshes.pop();
|
|
287
|
-
this.batchedMeshIndex.delete(key);
|
|
288
|
-
}
|
|
274
|
+
this.buckets.delete(key);
|
|
289
275
|
continue;
|
|
290
276
|
}
|
|
291
277
|
// Create new batch with all accumulated meshes for this bucket
|
|
292
|
-
const color =
|
|
293
|
-
const batchedMesh = this.createBatchedMesh(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const newIndex = this.batchedMeshes.length;
|
|
302
|
-
this.batchedMeshes.push(batchedMesh);
|
|
303
|
-
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);
|
|
304
287
|
}
|
|
305
288
|
}
|
|
306
289
|
this.pendingBatchKeys.clear();
|
|
@@ -311,6 +294,39 @@ export class Scene {
|
|
|
311
294
|
hasPendingBatches() {
|
|
312
295
|
return this.pendingBatchKeys.size > 0;
|
|
313
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
|
+
}
|
|
314
330
|
/**
|
|
315
331
|
* Create lightweight fragment batches from a single streaming batch.
|
|
316
332
|
* Fragments are grouped by color and added to batchedMeshes for immediate
|
|
@@ -355,18 +371,63 @@ export class Scene {
|
|
|
355
371
|
finalizeStreaming(device, pipeline) {
|
|
356
372
|
if (this.streamingFragments.length === 0)
|
|
357
373
|
return;
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
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) {
|
|
361
424
|
fragment.vertexBuffer.destroy();
|
|
362
425
|
fragment.indexBuffer.destroy();
|
|
363
426
|
if (fragment.uniformBuffer) {
|
|
364
427
|
fragment.uniformBuffer.destroy();
|
|
365
428
|
}
|
|
366
429
|
}
|
|
367
|
-
|
|
368
|
-
// 2. Destroy any pre-existing proper batches (non-fragments)
|
|
369
|
-
for (const batch of this.batchedMeshes) {
|
|
430
|
+
for (const batch of oldBatches) {
|
|
370
431
|
if (!fragmentSet.has(batch)) {
|
|
371
432
|
batch.vertexBuffer.destroy();
|
|
372
433
|
batch.indexBuffer.destroy();
|
|
@@ -375,51 +436,108 @@ export class Scene {
|
|
|
375
436
|
}
|
|
376
437
|
}
|
|
377
438
|
}
|
|
378
|
-
|
|
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
|
|
379
460
|
const allMeshData = [];
|
|
380
|
-
for (const
|
|
381
|
-
for (const md of
|
|
461
|
+
for (const bucket of this.buckets.values()) {
|
|
462
|
+
for (const md of bucket.meshData)
|
|
382
463
|
allMeshData.push(md);
|
|
383
464
|
}
|
|
384
|
-
//
|
|
385
|
-
this.
|
|
386
|
-
this.
|
|
387
|
-
this.batchedMeshIndex.clear();
|
|
388
|
-
this.batchedMeshData.clear();
|
|
389
|
-
this.meshDataBatchKey.clear();
|
|
390
|
-
this.meshDataBatchIdx.clear();
|
|
465
|
+
// 2. Clear bucket/batch state
|
|
466
|
+
this.buckets.clear();
|
|
467
|
+
this.meshDataBucket = new Map();
|
|
391
468
|
this.activeBucketKey.clear();
|
|
392
|
-
this.bucketVertexBytes.clear();
|
|
393
469
|
this.pendingBatchKeys.clear();
|
|
394
|
-
// Destroy cached partial batches — their colorKeys are now stale
|
|
395
470
|
for (const batch of this.partialBatchCache.values()) {
|
|
396
471
|
batch.vertexBuffer.destroy();
|
|
397
472
|
batch.indexBuffer.destroy();
|
|
398
|
-
if (batch.uniformBuffer)
|
|
473
|
+
if (batch.uniformBuffer)
|
|
399
474
|
batch.uniformBuffer.destroy();
|
|
400
|
-
}
|
|
401
475
|
}
|
|
402
476
|
this.partialBatchCache.clear();
|
|
403
477
|
this.partialBatchCacheKeys.clear();
|
|
404
|
-
//
|
|
405
|
-
// meshData.color may have been mutated in-place since the mesh was
|
|
406
|
-
// first bucketed, so the original bucket key is stale. Re-grouping
|
|
407
|
-
// by current color ensures batches render with correct colors.
|
|
478
|
+
// 3. Re-group meshData by current color (fast)
|
|
408
479
|
for (const meshData of allMeshData) {
|
|
409
480
|
const baseKey = this.colorKey(meshData.color);
|
|
410
481
|
const bucketKey = this.resolveActiveBucket(baseKey, meshData);
|
|
411
|
-
let bucket = this.
|
|
482
|
+
let bucket = this.buckets.get(bucketKey);
|
|
412
483
|
if (!bucket) {
|
|
413
|
-
bucket = [];
|
|
414
|
-
this.
|
|
484
|
+
bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
485
|
+
this.buckets.set(bucketKey, bucket);
|
|
415
486
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
this.meshDataBatchKey.set(meshData, bucketKey);
|
|
487
|
+
bucket.meshData.push(meshData);
|
|
488
|
+
this.meshDataBucket.set(meshData, bucket);
|
|
419
489
|
this.pendingBatchKeys.add(bucketKey);
|
|
420
490
|
}
|
|
421
|
-
//
|
|
422
|
-
|
|
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
|
+
});
|
|
423
541
|
}
|
|
424
542
|
/**
|
|
425
543
|
* Release JS-side mesh geometry data (positions, normals, indices) after
|
|
@@ -428,7 +546,7 @@ export class Scene {
|
|
|
428
546
|
*
|
|
429
547
|
* After calling this method:
|
|
430
548
|
* - Bounding boxes are precomputed and cached for all entities
|
|
431
|
-
* - meshDataMap and
|
|
549
|
+
* - meshDataMap and bucket meshData arrays are cleared (typed arrays become GC-eligible)
|
|
432
550
|
* - Color updates (updateMeshColors) are no longer available
|
|
433
551
|
* - Partial batch creation and color overlays are no longer available
|
|
434
552
|
* - CPU raycasting falls back to bounding-box-only (no triangle intersection)
|
|
@@ -479,10 +597,13 @@ export class Scene {
|
|
|
479
597
|
}
|
|
480
598
|
// 2. Clear the heavy data structures — typed arrays become GC-eligible
|
|
481
599
|
this.meshDataMap.clear();
|
|
482
|
-
|
|
483
|
-
|
|
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();
|
|
484
606
|
this.activeBucketKey.clear();
|
|
485
|
-
this.bucketVertexBytes.clear();
|
|
486
607
|
// 3. Clear partial batch cache (would need mesh data to rebuild)
|
|
487
608
|
for (const batch of this.partialBatchCache.values()) {
|
|
488
609
|
batch.vertexBuffer.destroy();
|
|
@@ -506,7 +627,7 @@ export class Scene {
|
|
|
506
627
|
* Update colors for existing meshes and rebuild affected batches
|
|
507
628
|
* Call this when deferred color parsing completes
|
|
508
629
|
*
|
|
509
|
-
* OPTIMIZATION: Uses
|
|
630
|
+
* OPTIMIZATION: Uses meshDataBucket reverse-map for O(1) batch lookup
|
|
510
631
|
* instead of O(N) indexOf scan per mesh. Critical for bulk IDS validation updates.
|
|
511
632
|
*/
|
|
512
633
|
updateMeshColors(updates, device, pipeline) {
|
|
@@ -529,8 +650,9 @@ export class Scene {
|
|
|
529
650
|
continue;
|
|
530
651
|
const newBaseKey = this.colorKey(newColor);
|
|
531
652
|
for (const meshData of meshDataList) {
|
|
532
|
-
// Use reverse-map for O(1) old bucket
|
|
533
|
-
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);
|
|
534
656
|
// Derive old color from bucket key, NOT meshData.color.
|
|
535
657
|
// meshData.color may have been mutated in-place by external code
|
|
536
658
|
// (applyColorUpdatesToMeshes), making it unreliable for change detection.
|
|
@@ -540,40 +662,37 @@ export class Scene {
|
|
|
540
662
|
const newBucketKey = this.resolveActiveBucket(newBaseKey, meshData);
|
|
541
663
|
affectedOldKeys.add(oldBucketKey);
|
|
542
664
|
affectedNewKeys.add(newBucketKey);
|
|
543
|
-
// Remove from old bucket data using
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const last =
|
|
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);
|
|
668
|
+
if (idx >= 0) {
|
|
669
|
+
// Swap-remove for O(1)
|
|
670
|
+
const last = oldBucket.meshData.length - 1;
|
|
549
671
|
if (idx !== last) {
|
|
550
|
-
|
|
551
|
-
oldBatchData[idx] = swapped;
|
|
552
|
-
this.meshDataBatchIdx.set(swapped, idx);
|
|
672
|
+
oldBucket.meshData[idx] = oldBucket.meshData[last];
|
|
553
673
|
}
|
|
554
|
-
|
|
555
|
-
this.meshDataBatchIdx.delete(meshData);
|
|
674
|
+
oldBucket.meshData.pop();
|
|
556
675
|
}
|
|
557
|
-
if (
|
|
558
|
-
this.
|
|
676
|
+
if (oldBucket.meshData.length === 0) {
|
|
677
|
+
this.buckets.delete(oldBucketKey);
|
|
559
678
|
}
|
|
560
679
|
}
|
|
561
680
|
// Decrease old bucket size tracking
|
|
562
681
|
const meshBytes = (meshData.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
|
|
563
|
-
|
|
564
|
-
|
|
682
|
+
if (oldBucket) {
|
|
683
|
+
oldBucket.vertexBytes = Math.max(0, oldBucket.vertexBytes - meshBytes);
|
|
684
|
+
}
|
|
565
685
|
// Update mesh color
|
|
566
686
|
meshData.color = newColor;
|
|
567
687
|
// Add to new bucket data (resolveActiveBucket already updated size tracking)
|
|
568
|
-
let newBucket = this.
|
|
688
|
+
let newBucket = this.buckets.get(newBucketKey);
|
|
569
689
|
if (!newBucket) {
|
|
570
|
-
newBucket = [];
|
|
571
|
-
this.
|
|
690
|
+
newBucket = { key: newBucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
|
|
691
|
+
this.buckets.set(newBucketKey, newBucket);
|
|
572
692
|
}
|
|
573
|
-
|
|
574
|
-
newBucket.push(meshData);
|
|
693
|
+
newBucket.meshData.push(meshData);
|
|
575
694
|
// Update reverse mapping
|
|
576
|
-
this.
|
|
695
|
+
this.meshDataBucket.set(meshData, newBucket);
|
|
577
696
|
}
|
|
578
697
|
}
|
|
579
698
|
}
|
|
@@ -594,7 +713,7 @@ export class Scene {
|
|
|
594
713
|
* Create a new batched mesh from mesh data array.
|
|
595
714
|
* @param bucketKey - Optional unique key for this batch. When omitted the
|
|
596
715
|
* base color key is used (fine for overlay / partial batches that don't
|
|
597
|
-
* participate in the main
|
|
716
|
+
* participate in the main buckets map).
|
|
598
717
|
*/
|
|
599
718
|
createBatchedMesh(meshDataArray, color, device, pipeline, bucketKey) {
|
|
600
719
|
const merged = this.mergeGeometry(meshDataArray);
|
|
@@ -782,15 +901,21 @@ export class Scene {
|
|
|
782
901
|
*/
|
|
783
902
|
resolveActiveBucket(baseColorKey, meshData) {
|
|
784
903
|
let bucketKey = this.activeBucketKey.get(baseColorKey) ?? baseColorKey;
|
|
785
|
-
const
|
|
904
|
+
const bucket = this.buckets.get(bucketKey);
|
|
905
|
+
const currentBytes = bucket?.vertexBytes ?? 0;
|
|
786
906
|
const meshBytes = (meshData.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
|
|
787
907
|
if (currentBytes > 0 && currentBytes + meshBytes > this.cachedMaxBufferSize) {
|
|
788
908
|
// Overflow — create a new sub-bucket
|
|
789
909
|
bucketKey = `${baseColorKey}#${this.nextSplitId++}`;
|
|
790
910
|
this.activeBucketKey.set(baseColorKey, bucketKey);
|
|
791
911
|
}
|
|
792
|
-
// Update size tracking
|
|
793
|
-
|
|
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;
|
|
794
919
|
return bucketKey;
|
|
795
920
|
}
|
|
796
921
|
/**
|
|
@@ -1009,19 +1134,16 @@ export class Scene {
|
|
|
1009
1134
|
this.meshes = [];
|
|
1010
1135
|
this.instancedMeshes = [];
|
|
1011
1136
|
this.batchedMeshes = [];
|
|
1012
|
-
this.
|
|
1013
|
-
this.
|
|
1014
|
-
this.batchedMeshIndex.clear();
|
|
1137
|
+
this.buckets.clear();
|
|
1138
|
+
this.meshDataBucket = new Map();
|
|
1015
1139
|
this.meshDataMap.clear();
|
|
1016
|
-
this.meshDataBatchKey.clear();
|
|
1017
|
-
this.meshDataBatchIdx.clear();
|
|
1018
1140
|
this.boundingBoxes.clear();
|
|
1019
1141
|
this.activeBucketKey.clear();
|
|
1020
|
-
this.bucketVertexBytes.clear();
|
|
1021
1142
|
this.cachedMaxBufferSize = 0;
|
|
1022
1143
|
this.pendingBatchKeys.clear();
|
|
1023
1144
|
this.partialBatchCache.clear();
|
|
1024
1145
|
this.partialBatchCacheKeys.clear();
|
|
1146
|
+
this.meshQueue = [];
|
|
1025
1147
|
this.geometryReleased = false;
|
|
1026
1148
|
}
|
|
1027
1149
|
/**
|