@ifc-lite/renderer 3.0.1 → 3.1.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/scene.js CHANGED
@@ -10,6 +10,7 @@ import { prepareSceneAuthoredOwner } from './scene-authored-owner.js';
10
10
  import { interleaveTexturedVertices } from './textured-vertices.js';
11
11
  import { RgbaTexturePool } from './rgba-texture-pool.js';
12
12
  import { splitMeshForStreaming } from './scene-stream-split.js';
13
+ import { hostsOtherEntities } from './mesh-entity-hosting.js';
13
14
  import { BATCH_CONSTANTS } from './constants.js';
14
15
  import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, rayIntersectsBox, } from './scene-raycaster.js';
15
16
  import { selectBoundingBoxesInRect } from './scene-rect-select.js';
@@ -17,6 +18,7 @@ import { splitMeshDataForBufferLimit, cachedWorldAabb, worldAabbFromPieces, dest
17
18
  import { sumResidentGpuBytes } from './render-stats.js';
18
19
  import { composeInstancedOverrideColor } from './instanced-override-color.js';
19
20
  import { bucketBaseKeyFor } from './chunk-grid.js';
21
+ import { inheritedQuantization, groupOverridePieces, cloneOverrides, overlaysInvalidatedBy } from './scene-derived-batches.js';
20
22
  import { VisibilityEpochTracker } from './visibility-epoch.js';
21
23
  import { isEntityVisible } from './entity-visibility.js';
22
24
  import { planInstancedGhosting } from './instanced-ghost-plan.js';
@@ -279,6 +281,7 @@ export class Scene {
279
281
  // Overlay batches render on top using depthCompare 'equal', so they only
280
282
  // paint where original geometry already wrote depth. Clearing is instant.
281
283
  overrideBatches = [];
284
+ overlaySources = new Map(); // overridden piece -> bucket key its overlay inherited from (#4832)
282
285
  // Defensively-typed: the renderer is the sole writer (via setColorOverrides),
283
286
  // external readers go through getColorOverrides() and get a ReadonlyMap.
284
287
  colorOverrides = null;
@@ -458,28 +461,20 @@ export class Scene {
458
461
  * Enable 12-byte lattice-quantized batch vertices (issue #1682 phase 6).
459
462
  * ONLY call after the renderer verified its quantized pipeline variants
460
463
  * exist (see Renderer.enableQuantizedBatches) — quantized buffers are
461
- * undrawable without them. Applies to batches built from now on; every
462
- * createBatchedMesh output (buckets, fragments, partial + override
463
- * batches) quantizes onto the SAME 2^-10 lattice, so depth-equal overlay
464
- * matching and cross-batch coincidence are preserved bit-exactly. Batches
465
- * whose extent exceeds the u16 lattice range fall back to f32 silently.
464
+ * undrawable without them. Applies to batches built from now on: bucket
465
+ * batches and fragments quantize onto the SAME 2^-10 lattice when their
466
+ * extent fits the u16 range (else f32); partial + override batches INHERIT
467
+ * their source batch's decision (#4832, `scene-derived-batches.ts`), so
468
+ * depth-equal overlay matching and cross-batch coincidence stay bit-exact.
466
469
  */
467
470
  setQuantizedBatches(enabled) {
468
471
  this.quantizedBatchesEnabled = enabled;
469
472
  }
470
- /**
471
- * Whether THIS mesh's source batch renders quantized — drives the
472
- * hydrated-mesh lattice snap in createMeshFromData (a mesh whose batch
473
- * fell back to f32, e.g. >64m extent, must NOT snap). Falls back to the
474
- * global flag when the mesh isn't bucketed (mid-stream hydration).
475
- */
473
+ /** Whether THIS mesh's source batch renders quantized — drives the hydrated-mesh
474
+ * lattice snap in createMeshFromData (an f32 batch must NOT snap). Same rule
475
+ * as overlay/partial batches; global flag while unbucketed. */
476
476
  isMeshQuantized(meshData) {
477
- if (!this.quantizedBatchesEnabled)
478
- return false;
479
- const bucket = this.meshDataBucket.get(meshData);
480
- if (bucket?.batchedMesh)
481
- return bucket.batchedMesh.quantized !== undefined;
482
- return true;
477
+ return inheritedQuantization(this.quantizedBatchesEnabled, this.meshDataBucket.get(meshData)?.batchedMesh) !== 'off';
483
478
  }
484
479
  /** Set (or clear) the HOST budget in bytes for bucket CPU geometry. */
485
480
  setHostResidencyBudget(bytes) {
@@ -1127,8 +1122,10 @@ export class Scene {
1127
1122
  rebuildPendingBatches(device, pipeline) {
1128
1123
  if (this.pendingBatchKeys.size === 0)
1129
1124
  return;
1125
+ const rebuilt = [];
1130
1126
  for (const key of this.pendingBatchKeys) {
1131
1127
  const bucket = this.buckets.get(key);
1128
+ const previousQuantized = bucket?.batchedMesh?.quantized !== undefined;
1132
1129
  // Destroy old GPU batch if it exists
1133
1130
  if (bucket?.batchedMesh) {
1134
1131
  // Slot keys embed the batch id and the replacement gets a fresh one, so
@@ -1148,6 +1145,7 @@ export class Scene {
1148
1145
  const color = bucket.meshData[0].color;
1149
1146
  const batchedMesh = this.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
1150
1147
  bucket.batchedMesh = batchedMesh;
1148
+ rebuilt.push({ bucket, previousQuantized });
1151
1149
  }
1152
1150
  // Rebuild the flat render array from all buckets (148 max batches — not perf critical)
1153
1151
  this.batchedMeshes = [];
@@ -1157,6 +1155,10 @@ export class Scene {
1157
1155
  }
1158
1156
  }
1159
1157
  this.pendingBatchKeys.clear();
1158
+ // Overlays follow their depth writers only when one actually changed under
1159
+ // them (flip / bucket move, #4832); finalize re-applies once itself.
1160
+ if (!this.finalizeInProgress && overlaysInvalidatedBy(rebuilt, this.overlaySources))
1161
+ this.reapplyColorOverrides(device, pipeline);
1160
1162
  }
1161
1163
  /**
1162
1164
  * Check if there are pending batch rebuilds
@@ -1177,8 +1179,8 @@ export class Scene {
1177
1179
  * raycast bounds. This is the proper removal path.
1178
1180
  *
1179
1181
  * Notes:
1180
- * - For color-merged meshes (the `entityIds` per-vertex case)
1181
- * a single MeshData often hosts many entities. We do NOT
1182
+ * - For color-merged meshes (`entityIds` naming other entities,
1183
+ * `hostsOtherEntities`) a single MeshData hosts many entities. We do NOT
1182
1184
  * drop the whole mesh in that case — that would also remove
1183
1185
  * the other entities — but we DO clear the bbox + meshDataMap
1184
1186
  * for the requested expressId, so picking and selection stop
@@ -1207,10 +1209,9 @@ export class Scene {
1207
1209
  // caller's bulk-count contract.
1208
1210
  let removedDedicated = false;
1209
1211
  for (const meshData of meshDataList) {
1210
- // Color-merged path: shared mesh, keep it but drop our entry.
1211
- if (meshData.entityIds && meshData.entityIds.length > 0) {
1212
+ // Color-merged path (entityIds naming OTHER entities): keep the shared mesh, drop our entry.
1213
+ if (hostsOtherEntities(meshData))
1212
1214
  continue;
1213
- }
1214
1215
  removedDedicated = true;
1215
1216
  // Dedicated mesh — drop from its bucket and decrement the
1216
1217
  // bucket's vertexBytes counter so subsequent
@@ -1943,6 +1944,7 @@ export class Scene {
1943
1944
  if (!fragmentSet.has(batch))
1944
1945
  destroyGpuResources(batch);
1945
1946
  }
1947
+ this.reapplyColorOverrides(device, pipeline);
1946
1948
  }
1947
1949
  /**
1948
1950
  * Time-sliced version of finalizeStreaming.
@@ -2062,6 +2064,7 @@ export class Scene {
2062
2064
  destroyGpuResources(batch);
2063
2065
  }
2064
2066
  scene.finalizeInProgress = false;
2067
+ scene.reapplyColorOverrides(device, pipeline); // never throws: the swap above is committed
2065
2068
  resolve();
2066
2069
  }
2067
2070
  catch (err) {
@@ -2323,8 +2326,9 @@ export class Scene {
2323
2326
  * @param bucketKey - Optional unique key for this batch. When omitted the
2324
2327
  * base color key is used (fine for overlay / partial batches that don't
2325
2328
  * participate in the main buckets map).
2329
+ * @param quantization - 'auto' for bucket batches; derived batches pass `inheritedQuantization(source)` (#4832).
2326
2330
  */
2327
- createBatchedMesh(meshes, color, device, pipeline, bucketKey) {
2331
+ createBatchedMesh(meshes, color, device, pipeline, bucketKey, quantization = this.quantizedBatchesEnabled ? 'auto' : 'off') {
2328
2332
  // Keep main's model-local frame and translation registration while staging
2329
2333
  // every GPU allocation before publishing Scene state.
2330
2334
  const modelIndex = meshes[0]?.modelIndex ?? 0;
@@ -2332,7 +2336,7 @@ export class Scene {
2332
2336
  const result = createSceneBatch(meshes, color, device, pipeline, {
2333
2337
  id: this.nextBatchId, colorKey: bucketKey ?? this.colorKey(color),
2334
2338
  origin: this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex),
2335
- quantized: this.quantizedBatchesEnabled, lod: this.lodBuildsEnabled,
2339
+ quantized: quantization, lod: this.lodBuildsEnabled,
2336
2340
  }, bucketKey);
2337
2341
  this.nextBatchId++;
2338
2342
  if (!this.sharedFrameOrigins.has(modelIndex) && result.origin) {
@@ -2500,32 +2504,27 @@ export class Scene {
2500
2504
  this.partialBatchCache.delete(currentCacheKey);
2501
2505
  }
2502
2506
  }
2503
- // Collect MeshData for visible elements
2504
- // Use the base key (strip "#N" bucket suffix) for piece filtering, since
2505
- // meshData stores the original color, not the bucket key. Pieces are
2506
- // matched through bucketBaseKey so the comparison stays correct with
2507
- // spatial chunking on (base key = "cell~colour" then, and a piece only
2508
- // belongs to this batch when BOTH its cell and colour match).
2507
+ // `colorKey` is the bucket key: match each piece's OWNING bucket exactly.
2508
+ // Matching the base key (suffix "#N" stripped) pulled sibling overflow
2509
+ // buckets' pieces in — drawn twice, wrong quantization inherited (#4832).
2510
+ // Only a batch with no bucket (a streaming fragment) uses the base key.
2511
+ const bucket = this.buckets.get(colorKey);
2509
2512
  const baseKey = this.baseColorKey(colorKey);
2510
2513
  const visibleMeshData = [];
2511
2514
  for (const expressId of visibleIds) {
2512
- const pieces = this.meshDataMap.get(expressId);
2513
- if (pieces) {
2514
- // Add all pieces for this element
2515
- for (const piece of pieces) {
2516
- // Only include pieces that match this batch's cell + color
2517
- if (this.bucketBaseKey(piece) === baseKey) {
2518
- visibleMeshData.push(piece);
2519
- }
2515
+ for (const piece of this.meshDataMap.get(expressId) ?? []) {
2516
+ if (bucket ? this.meshDataBucket.get(piece) === bucket : this.bucketBaseKey(piece) === baseKey) {
2517
+ visibleMeshData.push(piece);
2520
2518
  }
2521
2519
  }
2522
2520
  }
2523
2521
  if (visibleMeshData.length === 0) {
2524
2522
  return undefined;
2525
2523
  }
2526
- // Create the partial batch
2524
+ // Drawn INSTEAD of its source batch, so it inherits the source's f32/quantized
2525
+ // decision (#4832): the overlay built from the same source must match its depth.
2527
2526
  const color = visibleMeshData[0].color;
2528
- const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline);
2527
+ const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline, undefined, inheritedQuantization(this.quantizedBatchesEnabled, bucket?.batchedMesh));
2529
2528
  // Cache it
2530
2529
  this.partialBatchCache.set(cacheKey, partialBatch);
2531
2530
  this.partialBatchCacheKeys.set(sourceBatchKey, cacheKey);
@@ -2540,12 +2539,10 @@ export class Scene {
2540
2539
  // override colors. They are rendered on top of existing depth via the overlay
2541
2540
  // pipeline (depthCompare 'equal'), so hidden entities never leak through.
2542
2541
  /**
2543
- * Set color overrides for lens coloring.
2544
- * Builds overlay batches grouped by override color.
2545
- * Original batches are NEVER modified — clearing is instant.
2546
- *
2547
- * Applies the same buffer-size splitting as regular batches to prevent
2548
- * GPU buffer overflow on large models.
2542
+ * Set color overrides for lens / chart / IDS / compare / 4D coloring. Overlay
2543
+ * batches are grouped by SOURCE bucket + override color and inherit the
2544
+ * source batch's quantization (#4832, `scene-derived-batches.ts`). Original
2545
+ * batches are NEVER modified — clearing is instant.
2549
2546
  */
2550
2547
  setColorOverrides(overrides, device, pipeline) {
2551
2548
  // Destroy previous overlay batches
@@ -2564,44 +2561,37 @@ export class Scene {
2564
2561
  this.setInstancedColorOverrides(null);
2565
2562
  return;
2566
2563
  }
2567
- // Defensive copy so external callers can mutate or reuse `overrides`
2568
- // without aliasing the renderer's pipeline-routing state. Tuples are
2569
- // frozen by the readonly type — we don't deep-clone the inner arrays
2570
- // because they're treated as immutable by every consumer.
2564
+ // Defensive copy: callers may mutate/reuse `overrides`; the tuples are treated as immutable by every consumer.
2571
2565
  this.colorOverrides = new Map(overrides);
2572
- // Mirror the overlay onto the GPU-instanced occurrences: patch their colour
2573
- // bytes in place (no separate overlay pass — the instanced records carry the
2574
- // override colour directly). No-op when no instanced data is loaded.
2566
+ // Instanced occurrences carry the override colour in their records (no overlay pass); no-op without instanced data.
2575
2567
  this.setInstancedColorOverrides(overrides);
2576
- // An overlay batch must belong to one model so placement cannot move a
2577
- // different model's highlighted geometry along with its first mesh.
2578
- const colorGroups = new Map();
2579
- for (const [expressId, color] of overrides) {
2580
- for (const piece of this.meshDataMap.get(expressId) ?? []) {
2581
- const key = `${piece.modelIndex ?? 0}:${this.colorKey(color)}`;
2582
- let group = colorGroups.get(key);
2583
- if (!group) {
2584
- group = { color, meshData: [] };
2585
- colorGroups.set(key, group);
2586
- }
2587
- group.meshData.push(piece);
2588
- }
2589
- }
2590
- // Build overlay batches per override color, splitting if buffers would exceed GPU limit
2568
+ // Bucket keys carry cell + colour + model: an overlay batch is a subset of ONE base batch, in one model.
2569
+ const colorGroups = groupOverridePieces(overrides, (id) => this.meshDataMap.get(id), (piece) => this.meshDataBucket.get(piece), (piece) => this.bucketBaseKey(piece), (c) => this.colorKey(c));
2591
2570
  const maxBufferSize = this.getMaxBufferSize(device);
2592
- for (const [, { color, meshData }] of colorGroups) {
2593
- if (meshData.length === 0)
2594
- continue;
2595
- const chunks = this.splitMeshDataForBufferLimit(meshData, maxBufferSize);
2596
- for (const chunk of chunks) {
2597
- const batch = this.createBatchedMesh(chunk, color, device, pipeline);
2598
- this.overrideBatches.push(batch);
2571
+ for (const { color, meshData, sourceBatch, sourceKey } of colorGroups.values()) {
2572
+ if (sourceKey !== null)
2573
+ for (const piece of meshData)
2574
+ this.overlaySources.set(piece, sourceKey);
2575
+ const quantization = inheritedQuantization(this.quantizedBatchesEnabled, sourceBatch);
2576
+ for (const chunk of this.splitMeshDataForBufferLimit(meshData, maxBufferSize)) {
2577
+ this.overrideBatches.push(this.createBatchedMesh(chunk, color, device, pipeline, undefined, quantization));
2599
2578
  }
2600
2579
  }
2601
2580
  }
2602
- /**
2603
- * Clear all color overrides — instant, no batch rebuild needed.
2604
- */
2581
+ /** Rebuild the installed overlays against the batches that write depth NOW
2582
+ * (#4832). Best-effort — a committed finalize/rebuild must not be undone by
2583
+ * an overlay allocation failure, so that is reported, not thrown. */
2584
+ reapplyColorOverrides(device, pipeline) {
2585
+ if (!this.colorOverrides)
2586
+ return;
2587
+ try {
2588
+ this.setColorOverrides(cloneOverrides(this.colorOverrides), device, pipeline);
2589
+ }
2590
+ catch (err) {
2591
+ console.warn('[Scene] overlay rebuild after finalize failed — colours may be missing until the next repaint', err);
2592
+ }
2593
+ }
2594
+ /** Clear all color overrides — instant, no batch rebuild needed. */
2605
2595
  clearColorOverrides() {
2606
2596
  this.destroyOverrideBatches();
2607
2597
  this.colorOverrideGeneration++;
@@ -2644,6 +2634,7 @@ export class Scene {
2644
2634
  for (const batch of this.overrideBatches)
2645
2635
  destroyGpuResources(batch);
2646
2636
  this.overrideBatches = [];
2637
+ this.overlaySources.clear();
2647
2638
  }
2648
2639
  /**
2649
2640
  * Clear scene