@ifc-lite/renderer 5.0.0 → 6.0.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.
Files changed (280) hide show
  1. package/dist/camera-projection.d.ts +2 -0
  2. package/dist/camera-projection.d.ts.map +1 -1
  3. package/dist/camera-projection.js +46 -0
  4. package/dist/camera-projection.js.map +1 -1
  5. package/dist/chunk-grid.d.ts +21 -0
  6. package/dist/chunk-grid.d.ts.map +1 -1
  7. package/dist/chunk-grid.js +25 -7
  8. package/dist/chunk-grid.js.map +1 -1
  9. package/dist/clash-solid-pipeline.d.ts.map +1 -1
  10. package/dist/clash-solid-pipeline.js +4 -2
  11. package/dist/clash-solid-pipeline.js.map +1 -1
  12. package/dist/deviation/deviation-computer.d.ts +4 -0
  13. package/dist/deviation/deviation-computer.d.ts.map +1 -1
  14. package/dist/deviation/deviation-computer.js +20 -0
  15. package/dist/deviation/deviation-computer.js.map +1 -1
  16. package/dist/deviation/deviation-readback.d.ts +18 -0
  17. package/dist/deviation/deviation-readback.d.ts.map +1 -0
  18. package/dist/deviation/deviation-readback.js +67 -0
  19. package/dist/deviation/deviation-readback.js.map +1 -0
  20. package/dist/edge-pass.d.ts +16 -0
  21. package/dist/edge-pass.d.ts.map +1 -1
  22. package/dist/edge-pass.js +75 -1
  23. package/dist/edge-pass.js.map +1 -1
  24. package/dist/entity-color-table.d.ts +140 -0
  25. package/dist/entity-color-table.d.ts.map +1 -0
  26. package/dist/entity-color-table.js +238 -0
  27. package/dist/entity-color-table.js.map +1 -0
  28. package/dist/gpu-upload-guard.d.ts +1 -1
  29. package/dist/gpu-upload-guard.js +1 -1
  30. package/dist/hover-mesh-cache.d.ts +29 -0
  31. package/dist/hover-mesh-cache.d.ts.map +1 -0
  32. package/dist/hover-mesh-cache.js +31 -0
  33. package/dist/hover-mesh-cache.js.map +1 -0
  34. package/dist/index.d.ts +14 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +146 -161
  37. package/dist/index.js.map +1 -1
  38. package/dist/individual-mesh-upload.d.ts +24 -0
  39. package/dist/individual-mesh-upload.d.ts.map +1 -0
  40. package/dist/individual-mesh-upload.js +85 -0
  41. package/dist/individual-mesh-upload.js.map +1 -0
  42. package/dist/instanced-render.d.ts +19 -9
  43. package/dist/instanced-render.d.ts.map +1 -1
  44. package/dist/instanced-render.js +36 -24
  45. package/dist/instanced-render.js.map +1 -1
  46. package/dist/instanced-rte.d.ts +68 -7
  47. package/dist/instanced-rte.d.ts.map +1 -1
  48. package/dist/instanced-rte.js +98 -17
  49. package/dist/instanced-rte.js.map +1 -1
  50. package/dist/instanced-vertex-layout.d.ts +26 -0
  51. package/dist/instanced-vertex-layout.d.ts.map +1 -0
  52. package/dist/instanced-vertex-layout.js +61 -0
  53. package/dist/instanced-vertex-layout.js.map +1 -0
  54. package/dist/mesh-rte-uniforms.d.ts +4 -2
  55. package/dist/mesh-rte-uniforms.d.ts.map +1 -1
  56. package/dist/mesh-rte-uniforms.js +4 -2
  57. package/dist/mesh-rte-uniforms.js.map +1 -1
  58. package/dist/model-translation.d.ts.map +1 -1
  59. package/dist/model-translation.js +0 -7
  60. package/dist/model-translation.js.map +1 -1
  61. package/dist/outline-params.d.ts +45 -0
  62. package/dist/outline-params.d.ts.map +1 -0
  63. package/dist/outline-params.js +54 -0
  64. package/dist/outline-params.js.map +1 -0
  65. package/dist/overlay-routing.d.ts +8 -8
  66. package/dist/overlay-routing.js +2 -2
  67. package/dist/overlay-routing.js.map +1 -1
  68. package/dist/overlay-theme.d.ts +9 -3
  69. package/dist/overlay-theme.d.ts.map +1 -1
  70. package/dist/overlay-theme.js.map +1 -1
  71. package/dist/partial-batch-requests.d.ts +2 -2
  72. package/dist/pick-mesh-budget.d.ts +28 -0
  73. package/dist/pick-mesh-budget.d.ts.map +1 -0
  74. package/dist/pick-mesh-budget.js +74 -0
  75. package/dist/pick-mesh-budget.js.map +1 -0
  76. package/dist/picker-rte-uniforms.d.ts +7 -1
  77. package/dist/picker-rte-uniforms.d.ts.map +1 -1
  78. package/dist/picker-rte-uniforms.js +9 -1
  79. package/dist/picker-rte-uniforms.js.map +1 -1
  80. package/dist/picker.d.ts.map +1 -1
  81. package/dist/picker.js +15 -28
  82. package/dist/picker.js.map +1 -1
  83. package/dist/picking-manager.d.ts.map +1 -1
  84. package/dist/picking-manager.js +4 -47
  85. package/dist/picking-manager.js.map +1 -1
  86. package/dist/pipeline.d.ts +7 -1
  87. package/dist/pipeline.d.ts.map +1 -1
  88. package/dist/pipeline.js +37 -59
  89. package/dist/pipeline.js.map +1 -1
  90. package/dist/point-picker.d.ts.map +1 -1
  91. package/dist/point-picker.js +7 -2
  92. package/dist/point-picker.js.map +1 -1
  93. package/dist/pointcloud/point-cloud-node.js +1 -1
  94. package/dist/pointcloud/point-cloud-node.js.map +1 -1
  95. package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
  96. package/dist/pointcloud/point-cloud-renderer.js +3 -1
  97. package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
  98. package/dist/pointcloud/point-cloud-uniforms.d.ts +5 -1
  99. package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
  100. package/dist/pointcloud/point-cloud-uniforms.js +7 -1
  101. package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
  102. package/dist/post-pass-chain.d.ts +14 -1
  103. package/dist/post-pass-chain.d.ts.map +1 -1
  104. package/dist/post-pass-chain.js +31 -2
  105. package/dist/post-pass-chain.js.map +1 -1
  106. package/dist/raycast-engine.d.ts +5 -1
  107. package/dist/raycast-engine.d.ts.map +1 -1
  108. package/dist/raycast-engine.js +20 -8
  109. package/dist/raycast-engine.js.map +1 -1
  110. package/dist/raycast-point-cloud-query.d.ts +2 -0
  111. package/dist/raycast-point-cloud-query.d.ts.map +1 -1
  112. package/dist/raycast-point-cloud-query.js +7 -0
  113. package/dist/raycast-point-cloud-query.js.map +1 -1
  114. package/dist/raycast-source-curve-query.d.ts +13 -0
  115. package/dist/raycast-source-curve-query.d.ts.map +1 -0
  116. package/dist/raycast-source-curve-query.js +17 -0
  117. package/dist/raycast-source-curve-query.js.map +1 -0
  118. package/dist/reference-image-pipeline.d.ts.map +1 -1
  119. package/dist/reference-image-pipeline.js +4 -2
  120. package/dist/reference-image-pipeline.js.map +1 -1
  121. package/dist/relative-to-eye-abi.d.ts +36 -0
  122. package/dist/relative-to-eye-abi.d.ts.map +1 -0
  123. package/dist/relative-to-eye-abi.js +72 -0
  124. package/dist/relative-to-eye-abi.js.map +1 -0
  125. package/dist/relative-to-eye.d.ts +15 -35
  126. package/dist/relative-to-eye.d.ts.map +1 -1
  127. package/dist/relative-to-eye.js +46 -74
  128. package/dist/relative-to-eye.js.map +1 -1
  129. package/dist/render-stats.d.ts +5 -1
  130. package/dist/render-stats.d.ts.map +1 -1
  131. package/dist/render-stats.js +1 -1
  132. package/dist/render-stats.js.map +1 -1
  133. package/dist/renderer-overlays.d.ts.map +1 -1
  134. package/dist/renderer-overlays.js +3 -2
  135. package/dist/renderer-overlays.js.map +1 -1
  136. package/dist/scene-batch-rebuild.d.ts +1 -2
  137. package/dist/scene-batch-rebuild.d.ts.map +1 -1
  138. package/dist/scene-batch-rebuild.js +4 -8
  139. package/dist/scene-batch-rebuild.js.map +1 -1
  140. package/dist/scene-batch-upload.d.ts.map +1 -1
  141. package/dist/scene-batch-upload.js +7 -3
  142. package/dist/scene-batch-upload.js.map +1 -1
  143. package/dist/scene-contents.d.ts +1 -1
  144. package/dist/scene-contents.d.ts.map +1 -1
  145. package/dist/scene-derived-batches.d.ts +19 -64
  146. package/dist/scene-derived-batches.d.ts.map +1 -1
  147. package/dist/scene-derived-batches.js +16 -51
  148. package/dist/scene-derived-batches.js.map +1 -1
  149. package/dist/scene-device-recovery.d.ts +3 -3
  150. package/dist/scene-device-recovery.d.ts.map +1 -1
  151. package/dist/scene-device-recovery.js +12 -9
  152. package/dist/scene-device-recovery.js.map +1 -1
  153. package/dist/scene-geometry.d.ts +6 -6
  154. package/dist/scene-geometry.d.ts.map +1 -1
  155. package/dist/scene-geometry.js +8 -9
  156. package/dist/scene-geometry.js.map +1 -1
  157. package/dist/scene-instance-bounds.d.ts +1 -1
  158. package/dist/scene-instance-bounds.d.ts.map +1 -1
  159. package/dist/scene-instance-bounds.js +4 -16
  160. package/dist/scene-instance-bounds.js.map +1 -1
  161. package/dist/scene-instance-materialization.d.ts.map +1 -1
  162. package/dist/scene-instance-materialization.js +4 -4
  163. package/dist/scene-instance-materialization.js.map +1 -1
  164. package/dist/scene-instance-translation.d.ts +6 -10
  165. package/dist/scene-instance-translation.d.ts.map +1 -1
  166. package/dist/scene-instance-translation.js +9 -31
  167. package/dist/scene-instance-translation.js.map +1 -1
  168. package/dist/scene-instance-types.d.ts +11 -2
  169. package/dist/scene-instance-types.d.ts.map +1 -1
  170. package/dist/scene-instance-types.js +7 -1
  171. package/dist/scene-instance-types.js.map +1 -1
  172. package/dist/scene-model-translation.d.ts +1 -2
  173. package/dist/scene-model-translation.d.ts.map +1 -1
  174. package/dist/scene-model-translation.js +4 -3
  175. package/dist/scene-model-translation.js.map +1 -1
  176. package/dist/scene.d.ts +43 -38
  177. package/dist/scene.d.ts.map +1 -1
  178. package/dist/scene.js +121 -129
  179. package/dist/scene.js.map +1 -1
  180. package/dist/section-2d-line-buffer.d.ts +2 -2
  181. package/dist/section-2d-line-buffer.d.ts.map +1 -1
  182. package/dist/section-2d-line-buffer.js +10 -11
  183. package/dist/section-2d-line-buffer.js.map +1 -1
  184. package/dist/section-2d-overlay.d.ts +6 -4
  185. package/dist/section-2d-overlay.d.ts.map +1 -1
  186. package/dist/section-2d-overlay.js +45 -21
  187. package/dist/section-2d-overlay.js.map +1 -1
  188. package/dist/section-2d-types.d.ts +1 -1
  189. package/dist/section-2d-types.d.ts.map +1 -1
  190. package/dist/section-2d-types.js +1 -1
  191. package/dist/section-2d-types.js.map +1 -1
  192. package/dist/section-plane.d.ts.map +1 -1
  193. package/dist/section-plane.js +3 -1
  194. package/dist/section-plane.js.map +1 -1
  195. package/dist/selection-mask-pass.d.ts +85 -0
  196. package/dist/selection-mask-pass.d.ts.map +1 -0
  197. package/dist/selection-mask-pass.js +170 -0
  198. package/dist/selection-mask-pass.js.map +1 -0
  199. package/dist/selection-mask-pipelines.d.ts +57 -0
  200. package/dist/selection-mask-pipelines.d.ts.map +1 -0
  201. package/dist/selection-mask-pipelines.js +150 -0
  202. package/dist/selection-mask-pipelines.js.map +1 -0
  203. package/dist/selection-outline-frame.d.ts +47 -0
  204. package/dist/selection-outline-frame.d.ts.map +1 -0
  205. package/dist/selection-outline-frame.js +122 -0
  206. package/dist/selection-outline-frame.js.map +1 -0
  207. package/dist/shaders/depth-reconstruct.wgsl.d.ts +7 -2
  208. package/dist/shaders/depth-reconstruct.wgsl.d.ts.map +1 -1
  209. package/dist/shaders/depth-reconstruct.wgsl.js +8 -3
  210. package/dist/shaders/depth-reconstruct.wgsl.js.map +1 -1
  211. package/dist/shaders/edges.wgsl.d.ts +18 -0
  212. package/dist/shaders/edges.wgsl.d.ts.map +1 -1
  213. package/dist/shaders/edges.wgsl.js +99 -0
  214. package/dist/shaders/edges.wgsl.js.map +1 -1
  215. package/dist/shaders/entity-color-table.wgsl.d.ts +2 -0
  216. package/dist/shaders/entity-color-table.wgsl.d.ts.map +1 -0
  217. package/dist/shaders/entity-color-table.wgsl.js +85 -0
  218. package/dist/shaders/entity-color-table.wgsl.js.map +1 -0
  219. package/dist/shaders/main.wgsl.d.ts +1 -1
  220. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  221. package/dist/shaders/main.wgsl.js +45 -113
  222. package/dist/shaders/main.wgsl.js.map +1 -1
  223. package/dist/shaders/mesh-uniforms.wgsl.d.ts +2 -0
  224. package/dist/shaders/mesh-uniforms.wgsl.d.ts.map +1 -0
  225. package/dist/shaders/mesh-uniforms.wgsl.js +53 -0
  226. package/dist/shaders/mesh-uniforms.wgsl.js.map +1 -0
  227. package/dist/shaders/section-2d-overlay.wgsl.d.ts +1 -0
  228. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
  229. package/dist/shaders/section-2d-overlay.wgsl.js +3 -2
  230. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
  231. package/dist/shaders/selection-mask.wgsl.d.ts +16 -0
  232. package/dist/shaders/selection-mask.wgsl.d.ts.map +1 -0
  233. package/dist/shaders/selection-mask.wgsl.js +138 -0
  234. package/dist/shaders/selection-mask.wgsl.js.map +1 -0
  235. package/dist/shaders/shadow.wgsl.d.ts +1 -1
  236. package/dist/shaders/shadow.wgsl.d.ts.map +1 -1
  237. package/dist/shaders/shadow.wgsl.js +3 -3
  238. package/dist/shadow-occluders.d.ts.map +1 -1
  239. package/dist/shadow-occluders.js +1 -0
  240. package/dist/shadow-occluders.js.map +1 -1
  241. package/dist/shadow-pass.d.ts +0 -1
  242. package/dist/shadow-pass.d.ts.map +1 -1
  243. package/dist/shadow-pass.js +26 -32
  244. package/dist/shadow-pass.js.map +1 -1
  245. package/dist/shadow-types.d.ts +3 -0
  246. package/dist/shadow-types.d.ts.map +1 -1
  247. package/dist/shadow-types.js.map +1 -1
  248. package/dist/snap-detector.d.ts +5 -0
  249. package/dist/snap-detector.d.ts.map +1 -1
  250. package/dist/snap-detector.js.map +1 -1
  251. package/dist/source-curve-clipping.d.ts +7 -0
  252. package/dist/source-curve-clipping.d.ts.map +1 -0
  253. package/dist/source-curve-clipping.js +165 -0
  254. package/dist/source-curve-clipping.js.map +1 -0
  255. package/dist/source-curve-snap.d.ts +49 -0
  256. package/dist/source-curve-snap.d.ts.map +1 -0
  257. package/dist/source-curve-snap.js +197 -0
  258. package/dist/source-curve-snap.js.map +1 -0
  259. package/dist/symbolic-overlay-pipelines.d.ts +1 -1
  260. package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
  261. package/dist/symbolic-overlay-pipelines.js +13 -22
  262. package/dist/symbolic-overlay-pipelines.js.map +1 -1
  263. package/dist/symbolic-text-rte.d.ts +16 -0
  264. package/dist/symbolic-text-rte.d.ts.map +1 -0
  265. package/dist/symbolic-text-rte.js +37 -0
  266. package/dist/symbolic-text-rte.js.map +1 -0
  267. package/dist/textured-vertices.d.ts.map +1 -1
  268. package/dist/textured-vertices.js +2 -3
  269. package/dist/textured-vertices.js.map +1 -1
  270. package/dist/types.d.ts +10 -0
  271. package/dist/types.d.ts.map +1 -1
  272. package/dist/visual-enhancement.d.ts +0 -4
  273. package/dist/visual-enhancement.d.ts.map +1 -1
  274. package/dist/visual-enhancement.js +0 -5
  275. package/dist/visual-enhancement.js.map +1 -1
  276. package/dist/xray-alpha.d.ts +7 -3
  277. package/dist/xray-alpha.d.ts.map +1 -1
  278. package/dist/xray-alpha.js +11 -7
  279. package/dist/xray-alpha.js.map +1 -1
  280. package/package.json +4 -4
package/dist/scene.js CHANGED
@@ -1,6 +1,11 @@
1
1
  /* This Source Code Form is subject to the terms of the Mozilla Public
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Scene graph and mesh management
6
+ */
7
+ import { destroyInstancedTemplateGpu } from './scene-instance-types.js';
8
+ import { createInstancedRteDeltaStream, invalidateInstancedRteDeltas } from './instanced-rte.js';
4
9
  import { materializeInstances } from './scene-instance-materialization.js';
5
10
  import { InstanceSuppression } from './scene-instance-suppression.js';
6
11
  import { createSceneBatch } from './scene-batch-upload.js';
@@ -19,8 +24,9 @@ import { splitMeshDataForBufferLimit, cachedWorldAabb, worldAabbFromPieces, dest
19
24
  import { resolvePrecisionBucket } from './scene-bucket-routing.js';
20
25
  import { sumResidentGpuBytes } from './render-stats.js';
21
26
  import { composeInstancedOverrideColor, writeOriginalInstancedColors } from './instanced-override-color.js';
22
- import { bucketBaseKeyFor } from './chunk-grid.js';
23
- import { inheritedQuantization, groupOverridePieces, cloneOverrides, overlaysInvalidatedBy } from './scene-derived-batches.js';
27
+ import { bucketBaseKeyFor, colorKey } from './chunk-grid.js';
28
+ import { cloneOverrides, inheritedQuantization } from './scene-derived-batches.js';
29
+ import { EntityColorTable, entityIdPageKey } from './entity-color-table.js';
24
30
  import { VisibilityEpochTracker } from './visibility-epoch.js';
25
31
  import { isEntityVisible } from './entity-visibility.js';
26
32
  import { planInstancedGhosting } from './instanced-ghost-plan.js';
@@ -33,7 +39,7 @@ import { DerivedMeshProvenance } from './scene-derived-mesh-provenance.js';
33
39
  import { rebuildSceneBatches } from './scene-batch-rebuild.js';
34
40
  import { regroupStreamedBuckets } from './scene-finalize-regroup.js';
35
41
  import { dropAllPartialCaches as dropAllPartialCachesIn, dropPartialCacheForBatch as dropPartialCacheForBatchIn, retireUnusedAlphaSlots as retireUnusedAlphaSlotsIn, } from './partial-batch-cache.js';
36
- import { prepareInstancedRender, foldOccurrenceWorldBox, INSTANCE_STRIDE_BYTES, INSTANCE_COLOR_OFFSET, INSTANCE_FLAGS_OFFSET, INSTANCE_FLAG_SELECTED, INSTANCE_FLAG_HIDDEN, } from './instanced-render.js';
42
+ import { prepareInstancedRender, foldOccurrenceWorldBox, INSTANCE_STRIDE_BYTES, INSTANCE_COLOR_OFFSET, INSTANCE_FLAGS_OFFSET, INSTANCE_FLAG_SELECTED, INSTANCE_FLAG_HIDDEN, INSTANCE_FINISH_FLAGS_MASK, } from './instanced-render.js';
37
43
  import { translateInstanceRecord } from './scene-instance-translation.js';
38
44
  import { discardSceneGpuResourcesForRecovery, prepareSceneDeviceRecovery, repartitionHydratedRecoveryBucket, restoreSceneGpuResourcesAfterRecovery } from './scene-device-recovery.js';
39
45
  const EMPTY_INSTANCED_TEMPLATES = [];
@@ -286,11 +292,9 @@ export class Scene {
286
292
  keys: this.partialBatchCacheKeys,
287
293
  versions: this.partialBatchCacheVersions,
288
294
  };
289
- // Color overlay system for lens coloring — NEVER modifies original batches.
290
- // Overlay batches render on top using depthCompare 'equal', so they only
291
- // paint where original geometry already wrote depth. Clearing is instant.
292
- overrideBatches = [];
293
- overlaySources = new Map(); // overridden piece -> bucket key its overlay inherited from (#4832)
295
+ // Colour overrides shade in the base pass from this per-entity table (#6076,
296
+ // entity-color-table.ts): no geometry copies, nothing to rebuild on stream.
297
+ entityColorTable = new EntityColorTable();
294
298
  // Defensively-typed: the renderer is the sole writer (via setColorOverrides),
295
299
  // external readers go through getColorOverrides() and get a ReadonlyMap.
296
300
  colorOverrides = null;
@@ -368,7 +372,7 @@ export class Scene {
368
372
  }
369
373
  // ─── GPU residency (issue #1682 phase 3a) ──────────────────────────────
370
374
  // The budget applies to bucket-owned colour/chunk batches (the evictable
371
- // set). Streaming fragments, partial sub-batches, overlay batches,
375
+ // set). Streaming fragments, partial sub-batches,
372
376
  // textured meshes and instanced templates are never evicted: fragments are
373
377
  // transient, the rest are small or lack a rebuild source. Enforcement
374
378
  // no-ops while geometry is released or in ephemeral streaming mode (no CPU
@@ -816,10 +820,17 @@ export class Scene {
816
820
  * partial-batch piece filter) must go through this so a mesh always
817
821
  * resolves to the same bucket. `color` overrides the mesh's own colour for
818
822
  * recolour routing.
823
+ *
824
+ * Ids past 2^24 also key by their 2^24 page (#6076): a batch then never
825
+ * spans 2^24 ids, which the entity colour table needs to rebuild a full id
826
+ * from the 24-bit vertex lane (`entityIdAnchor`, entity-color-table.ts).
819
827
  */
820
828
  bucketBaseKey(meshData, color) {
821
829
  const source = this.modelTranslations.sourceMesh(meshData);
822
- const key = bucketBaseKeyFor(source, this.colorKey(color ?? meshData.color), this.spatialChunking);
830
+ // #5582: material is the mesh's OWN authored finish regardless of a
831
+ // colour override — a recolour changes what a piece looks like, not
832
+ // what it is physically made of.
833
+ const key = entityIdPageKey(source.expressId, bucketBaseKeyFor(source, this.colorKey(color ?? meshData.color, meshData.material), this.spatialChunking));
823
834
  return source.modelIndex ? `model${source.modelIndex}~${key}` : key;
824
835
  }
825
836
  /**
@@ -1002,6 +1013,19 @@ export class Scene {
1002
1013
  // Check if any piece matches the modelIndex
1003
1014
  return pieces.some(p => p.modelIndex === modelIndex);
1004
1015
  }
1016
+ /**
1017
+ * Whether more than `limit` entities with mesh data pass the hide/isolate
1018
+ * filter. Stops at `limit + 1` and allocates nothing, so a per-pick budget
1019
+ * check stays cheap on a large model (#6392).
1020
+ */
1021
+ visibleMeshDataEntitiesExceed(limit, hiddenIds, isolatedIds) {
1022
+ let count = 0;
1023
+ for (const expressId of this.meshDataMap.keys()) {
1024
+ if (isEntityVisible(expressId, hiddenIds, isolatedIds) && ++count > limit)
1025
+ return true;
1026
+ }
1027
+ return false;
1028
+ }
1005
1029
  /**
1006
1030
  * Get all MeshData pieces for an expressId (without merging).
1007
1031
  * Optionally filter by modelIndex for multi-model safety.
@@ -1071,18 +1095,12 @@ export class Scene {
1071
1095
  return pieces;
1072
1096
  }
1073
1097
  /**
1074
- * Generate color key for grouping meshes.
1075
- * Quantizes RGBA to 10-bit per channel and packs into a compact string.
1076
- * Avoids floating-point template literal overhead of the old approach.
1098
+ * Colour key for grouping meshes: `chunk-grid.ts`'s `colorKey` (RGBA
1099
+ * quantized to 1/1000), with the authored finish folded in (#5582) so one
1100
+ * batch never mixes finishes.
1077
1101
  */
1078
- colorKey(color) {
1079
- // Quantize to 1000 levels (same precision as before, but integer math only)
1080
- const r = Math.round(color[0] * 1000);
1081
- const g = Math.round(color[1] * 1000);
1082
- const b = Math.round(color[2] * 1000);
1083
- const a = Math.round(color[3] * 1000);
1084
- // Pack into single string with fixed-width separator for uniqueness
1085
- return `${r}|${g}|${b}|${a}`;
1102
+ colorKey(color, material) {
1103
+ return colorKey(color, material);
1086
1104
  }
1087
1105
  /**
1088
1106
  * Append meshes to color batches incrementally
@@ -1175,16 +1193,12 @@ export class Scene {
1175
1193
  rebuildPendingBatches(device, pipeline) {
1176
1194
  if (this.pendingBatchKeys.size === 0)
1177
1195
  return;
1178
- const rebuilt = rebuildSceneBatches({ pendingKeys: this.pendingBatchKeys, buckets: this.buckets,
1196
+ rebuildSceneBatches({ pendingKeys: this.pendingBatchKeys, buckets: this.buckets,
1179
1197
  create: (meshes, color, target, renderPipeline, key) => this.createBatchedMesh(meshes, color, target, renderPipeline, key),
1180
1198
  dropPartial: batch => this.dropPartialCacheForBatch(batch),
1181
1199
  }, device, pipeline);
1182
1200
  this.batchedMeshes = [...this.buckets.values()].flatMap(bucket => bucket.batchedMesh ? [bucket.batchedMesh] : []);
1183
1201
  this.pendingBatchKeys.clear();
1184
- // Overlays follow their depth writers only when one actually changed under
1185
- // them (flip / bucket move, #4832); finalize re-applies once itself.
1186
- if (!this.finalizeInProgress && overlaysInvalidatedBy(rebuilt, this.overlaySources))
1187
- this.reapplyColorOverrides(device, pipeline);
1188
1202
  }
1189
1203
  /**
1190
1204
  * Check if there are pending batch rebuilds
@@ -1513,17 +1527,15 @@ export class Scene {
1513
1527
  if (!cpu)
1514
1528
  continue;
1515
1529
  const b = occ.byteOffset;
1516
- const translated = translateInstanceRecord(cpu, b, [dx, dy, dz]);
1517
- // Push only the 12 translation bytes to the GPU buffer (in place). Guarded
1518
- // on the cached device so CPU-only tests still exercise the matrix math.
1519
- if (device) {
1520
- const gpu = this.instancedTemplates[occ.templateIndex]?.instanceBuffer;
1521
- if (gpu) {
1522
- device.queue.writeBuffer(gpu, b + 48, translated.translation);
1523
- if (translated.legacyAnchors)
1524
- device.queue.writeBuffer(gpu, b + 88, translated.legacyAnchors);
1525
- }
1526
- }
1530
+ const translation = translateInstanceRecord(cpu, b, [dx, dy, dz]);
1531
+ // Push only the 12 translation bytes to the GPU buffer (in place), and
1532
+ // repack the moved anchors' delta stream on the next upload. Guarded on
1533
+ // the cached device so CPU-only tests still exercise the matrix math.
1534
+ const gpu = this.instancedTemplates[occ.templateIndex];
1535
+ if (gpu)
1536
+ invalidateInstancedRteDeltas(gpu.rteDeltas);
1537
+ if (device && gpu)
1538
+ device.queue.writeBuffer(gpu.instanceBuffer, b + 48, translation);
1527
1539
  moved = true;
1528
1540
  }
1529
1541
  if (!moved)
@@ -1550,7 +1562,7 @@ export class Scene {
1550
1562
  if (!cpu)
1551
1563
  continue;
1552
1564
  const dv = new DataView(cpu.instanceData);
1553
- const w = this.unionInstancedWorldAabb(expressId, dv, occ.byteOffset, cpu.localMin[0], cpu.localMin[1], cpu.localMin[2], cpu.localMax[0], cpu.localMax[1], cpu.localMax[2]);
1565
+ const w = this.unionInstancedWorldAabb(expressId, dv, occ.byteOffset, cpu.canonicalAnchors, cpu.localMin[0], cpu.localMin[1], cpu.localMin[2], cpu.localMax[0], cpu.localMax[1], cpu.localMax[2]);
1554
1566
  // GROW the template's cull union so a moved occurrence (Exploded mode,
1555
1567
  // #1289) can't be frustum/contribution-culled by its pre-move bounds.
1556
1568
  // The pre-move region stays in the union — monotonic growth only ever
@@ -1587,11 +1599,11 @@ export class Scene {
1587
1599
  const batches = this.finalizeInProgress ? [...new Set([...this.batchedMeshes,
1588
1600
  ...[...this.buckets.values()].flatMap((bucket) => bucket.batchedMesh ? [bucket.batchedMesh] : [])])] : this.batchedMeshes;
1589
1601
  return { translations: this.modelTranslations, pieces: this.meshDataMap,
1590
- bounds: this.boundingBoxes, batches, meshes: this.meshes, overrides: this.overrideBatches, textured: this.texturedMeshes,
1602
+ bounds: this.boundingBoxes, batches, meshes: this.meshes, textured: this.texturedMeshes,
1591
1603
  templates: this.instancedTemplates, cpu: this.instancedTemplateCpu, occurrences: this.instancedEntityMap,
1592
1604
  device: this.instancedDevice, evictHighlight: (id) => this.evictHighlightMeshes(id, true),
1593
1605
  clearPartial: () => this.dropAllPartialCaches(),
1594
- unionBounds: (id, view, offset, min, max) => this.unionInstancedWorldAabb(id, view, offset, ...min, ...max) };
1606
+ unionBounds: (id, view, offset, anchors, min, max) => this.unionInstancedWorldAabb(id, view, offset, anchors, ...min, ...max) };
1595
1607
  }
1596
1608
  dropOrphanedSuppressedBounds() {
1597
1609
  for (const id of this.instanceSuppression.suppressedIds())
@@ -1767,9 +1779,12 @@ export class Scene {
1767
1779
  * Processes queued meshes through appendToBatches in streaming mode
1768
1780
  * (creates lightweight fragment batches for immediate rendering).
1769
1781
  *
1782
+ * @param budgetMs Time slice per call. The 12 ms default keeps the main
1783
+ * thread returning to the worker-message pump while geometry streams; a
1784
+ * host with nothing else to serve may drain faster (#6436).
1770
1785
  * @returns true if any meshes were processed (caller should render)
1771
1786
  */
1772
- flushPending(device, pipeline) {
1787
+ flushPending(device, pipeline, budgetMs = 12) {
1773
1788
  if (!this.hasQueuedMeshes())
1774
1789
  return false;
1775
1790
  // Drain the queue in chunks bounded by BOTH mesh count AND triangle volume,
@@ -1782,13 +1797,12 @@ export class Scene {
1782
1797
  const MAX_MESHES_PER_FLUSH = 4096;
1783
1798
  const MESHES_PER_APPEND = 512;
1784
1799
  const MAX_INDICES_PER_APPEND = Scene.STREAMING_FRAGMENT_MAX_INDICES;
1785
- const FLUSH_BUDGET_MS = 12;
1786
1800
  const start = performance.now();
1787
1801
  let processed = 0;
1788
1802
  while (this.meshQueueReadIndex < this.meshQueue.length && processed < MAX_MESHES_PER_FLUSH) {
1789
1803
  // Yield once the budget is spent (after at least one append) so the main
1790
1804
  // thread returns to the worker-message pump and the watchdog never trips.
1791
- if (processed > 0 && performance.now() - start >= FLUSH_BUDGET_MS) {
1805
+ if (processed > 0 && performance.now() - start >= budgetMs) {
1792
1806
  break;
1793
1807
  }
1794
1808
  const hardEnd = Math.min(this.meshQueue.length, this.meshQueueReadIndex + MESHES_PER_APPEND, this.meshQueueReadIndex + (MAX_MESHES_PER_FLUSH - processed));
@@ -1948,7 +1962,6 @@ export class Scene {
1948
1962
  // already retired by rebuildPendingBatches.
1949
1963
  this.retireFinalizedBatches(oldFragments);
1950
1964
  this.retireFinalizedBatches(regroup?.retired ?? []);
1951
- this.reapplyColorOverrides(device, pipeline);
1952
1965
  }
1953
1966
  /**
1954
1967
  * Time-sliced version of finalizeStreaming.
@@ -2058,7 +2071,6 @@ export class Scene {
2058
2071
  ];
2059
2072
  scene.retireFinalizedBatches(retired);
2060
2073
  scene.finalizeInProgress = false;
2061
- scene.reapplyColorOverrides(device, pipeline); // never throws: the swap above is committed
2062
2074
  resolve();
2063
2075
  }
2064
2076
  catch (err) {
@@ -2124,7 +2136,8 @@ export class Scene {
2124
2136
  * - Bounding boxes are precomputed and cached for all entities
2125
2137
  * - meshDataMap and bucket meshData arrays are cleared (typed arrays become GC-eligible)
2126
2138
  * - Color updates (updateMeshColors) are no longer available
2127
- * - Partial batch creation and color overlays are no longer available
2139
+ * - Partial batch creation is no longer available; color overrides still
2140
+ * work through the GPU-resident per-entity table
2128
2141
  * - CPU raycasting falls back to bounding-box-only (no triangle intersection)
2129
2142
  * - Selection highlighting must use GPU picking instead of CPU mesh reconstruction
2130
2143
  *
@@ -2297,7 +2310,9 @@ export class Scene {
2297
2310
  if (!origin)
2298
2311
  throw new Error('Unable to resolve a topology-safe GPU frame for mesh geometry.');
2299
2312
  const result = createSceneBatch(meshes, color, device, pipeline, {
2300
- id: this.nextBatchId, colorKey: bucketKey ?? this.colorKey(color),
2313
+ // A derived batch (no bucketKey) is a subset of ONE material-uniform
2314
+ // bucket, so its first piece's finish labels it like bucketBaseKey does (#5582).
2315
+ id: this.nextBatchId, colorKey: bucketKey ?? this.colorKey(color, meshes[0]?.material),
2301
2316
  origin,
2302
2317
  quantized: quantization, lod: this.lodBuildsEnabled,
2303
2318
  }, bucketKey);
@@ -2473,69 +2488,39 @@ export class Scene {
2473
2488
  }
2474
2489
  return partialBatch;
2475
2490
  }
2476
- // ─── Color overlay system ────────────────────────────────────────────
2477
- // Builds overlay batches for lens coloring without modifying original batches.
2478
- // Overlay batches reuse the same geometry (re-merged from MeshData) but with
2479
- // override colors. They are rendered on top of existing depth via the overlay
2480
- // pipeline (depthCompare 'equal'), so hidden entities never leak through.
2491
+ // ─── Colour overrides (#6076) ────────────────────────────────────────
2492
+ // Overrides live in a per-entity colour table the base pass reads
2493
+ // (entity-color-table.ts). Batches are never modified or copied, and a mesh
2494
+ // streamed in later is painted by the ids it already carries.
2481
2495
  /**
2482
- * Set color overrides for lens / chart / IDS / compare / 4D coloring. Overlay
2483
- * batches are grouped by SOURCE bucket + override color and inherit the
2484
- * source batch's quantization (#4832, `scene-derived-batches.ts`). Original
2485
- * batches are NEVER modified — clearing is instant.
2496
+ * Set colour overrides for lens / chart / IDS / compare / 4D colouring:
2497
+ * rewrites the entity colour table (one `writeBuffer`) and the instanced
2498
+ * occurrence records. No batch is built, rebuilt or destroyed. `pipeline`
2499
+ * stays in the signature because `SceneContents` publishes it.
2486
2500
  */
2487
- setColorOverrides(overrides, device, pipeline) {
2488
- // Destroy previous overlay batches
2489
- this.destroyOverrideBatches();
2501
+ setColorOverrides(overrides, device, _pipeline) {
2490
2502
  // The override set is changing — invalidate the partial-batch cache epoch so
2491
2503
  // the render loop rebuilds any promotion-split sub-batches (see render loop).
2492
2504
  this.colorOverrideGeneration++;
2493
- if (this.geometryReleased) {
2494
- console.warn('[Scene] setColorOverrides called after geometry data was released — skipping.');
2495
- this.colorOverrides = null;
2496
- this.setInstancedColorOverrides(null);
2497
- return;
2498
- }
2499
2505
  if (overrides.size === 0) {
2500
- this.colorOverrides = null;
2501
- this.setInstancedColorOverrides(null);
2506
+ this.clearColorOverrideState();
2502
2507
  return;
2503
2508
  }
2504
- // Defensive copy: callers may mutate/reuse `overrides`; the tuples are treated as immutable by every consumer.
2505
- this.colorOverrides = new Map(overrides);
2506
- // Instanced occurrences carry the override colour in their records (no overlay pass); no-op without instanced data.
2509
+ // A caller can mutate a clash-tint tuple after this call (#5490). Keep the
2510
+ // retained map and the uploaded table in sync by copying its values.
2511
+ this.colorOverrides = cloneOverrides(overrides);
2512
+ this.entityColorTable.write(device, this.colorOverrides);
2513
+ // Instanced occurrences carry the override colour in their records; no-op without instanced data.
2507
2514
  this.setInstancedColorOverrides(overrides);
2508
- // Bucket keys carry cell + colour + model: an overlay batch is a subset of ONE base batch, in one model.
2509
- const colorGroups = groupOverridePieces(overrides, (id) => this.meshDataMap.get(id), (piece) => this.meshDataBucket.get(piece), (piece) => this.bucketBaseKey(piece), (c) => this.colorKey(c));
2510
- const maxBufferSize = this.getMaxBufferSize(device);
2511
- for (const { color, meshData, sourceBatch, sourceKey } of colorGroups.values()) {
2512
- if (sourceKey !== null)
2513
- for (const piece of meshData)
2514
- this.overlaySources.set(piece, sourceKey);
2515
- const quantization = inheritedQuantization(this.quantizedBatchesEnabled, sourceBatch);
2516
- for (const chunk of this.splitMeshDataForBufferLimit(meshData, maxBufferSize)) {
2517
- this.overrideBatches.push(this.createBatchedMesh(chunk, color, device, pipeline, undefined, quantization, sourceBatch?.origin));
2518
- }
2519
- }
2520
- }
2521
- /** Rebuild the installed overlays against the batches that write depth NOW
2522
- * (#4832). Best-effort — a committed finalize/rebuild must not be undone by
2523
- * an overlay allocation failure, so that is reported, not thrown. */
2524
- reapplyColorOverrides(device, pipeline) {
2525
- if (!this.colorOverrides)
2526
- return;
2527
- try {
2528
- this.setColorOverrides(cloneOverrides(this.colorOverrides), device, pipeline);
2529
- }
2530
- catch (err) {
2531
- console.warn('[Scene] overlay rebuild after finalize failed — colours may be missing until the next repaint', err);
2532
- }
2533
2515
  }
2534
2516
  /** Clear all color overrides — instant, no batch rebuild needed. */
2535
2517
  clearColorOverrides() {
2536
- this.destroyOverrideBatches();
2537
2518
  this.colorOverrideGeneration++;
2519
+ this.clearColorOverrideState();
2520
+ }
2521
+ clearColorOverrideState() {
2538
2522
  this.colorOverrides = null;
2523
+ this.entityColorTable.clear();
2539
2524
  this.setInstancedColorOverrides(null);
2540
2525
  }
2541
2526
  /** Monotonic counter that changes whenever the colour-override set changes.
@@ -2544,37 +2529,28 @@ export class Scene {
2544
2529
  getColorOverrideGeneration() {
2545
2530
  return this.colorOverrideGeneration;
2546
2531
  }
2547
- /** Get overlay batches for rendering */
2548
- getOverrideBatches() {
2549
- return this.overrideBatches;
2550
- }
2551
- /** Check if color overrides are active */
2552
- hasColorOverrides() {
2553
- return this.overrideBatches.length > 0;
2532
+ /** The per-entity colour table the renderer binds at group(1) (#6076). */
2533
+ getEntityColorTable() {
2534
+ return this.entityColorTable;
2554
2535
  }
2555
2536
  /**
2556
2537
  * Get the active expressId → RGBA override map, or null if none.
2557
2538
  *
2558
2539
  * Used by the renderer to promote overridden meshes/batches to the opaque
2559
- * pipeline so the overlay paint pass (depthCompare 'equal') finds matching
2560
- * depth. Without this, an override on an entity that defaults to the
2561
- * transparent pipeline (IfcSpace, IfcOpeningElement, glass, …) silently
2562
- * fails to paint — the transparent pipeline doesn't write depth, so the
2563
- * equality test rejects every fragment.
2540
+ * pipeline (overlay-routing.ts): the table paints only depth-writing opaque
2541
+ * draws, so an override on an entity that defaults to the transparent
2542
+ * pipeline (IfcSpace, IfcOpeningElement, glass, …) is promoted to be seen.
2564
2543
  *
2565
2544
  * Returns a `ReadonlyMap` view: the renderer holds the only writeable
2566
2545
  * reference (via `setColorOverrides`) so routing decisions stay in sync
2567
- * with the overlay batches we built from the same data.
2546
+ * with the colour table written from the same data.
2568
2547
  */
2569
2548
  getColorOverrides() {
2570
2549
  return this.colorOverrides;
2571
2550
  }
2572
- /** Destroy GPU resources for overlay batches */
2573
- destroyOverrideBatches() {
2574
- for (const batch of this.overrideBatches)
2575
- destroyGpuResources(batch);
2576
- this.overrideBatches = [];
2577
- this.overlaySources.clear();
2551
+ /** Drop the colour table's GPU buffer (device-loss recovery host); `setColorOverrides` re-uploads it. */
2552
+ releaseColorOverrideGpu() {
2553
+ this.entityColorTable.releaseGpu();
2578
2554
  }
2579
2555
  /**
2580
2556
  * Clear scene
@@ -2670,9 +2646,7 @@ export class Scene {
2670
2646
  const t = this.instancedTemplates[i];
2671
2647
  if (!t || t.modelIndex !== modelIndex)
2672
2648
  continue;
2673
- t.vertexBuffer.destroy();
2674
- t.indexBuffer.destroy();
2675
- t.instanceBuffer.destroy();
2649
+ destroyInstancedTemplateGpu(t);
2676
2650
  this.instancedTemplates[i] = undefined;
2677
2651
  this.instancedTemplateCpu[i] = undefined;
2678
2652
  freed.add(i);
@@ -2820,6 +2794,17 @@ export class Scene {
2820
2794
  this.modelTranslations.placeInstances(t.instanceBuffer, modelIndex, INSTANCE_STRIDE_BYTES, t.canonicalAnchors, t.canonicalMatrixTranslations);
2821
2795
  const instSize = t.instanceCount * INSTANCE_STRIDE_BYTES;
2822
2796
  const instanceBuffer = createStaticGpuBuffer(device, new Uint8Array(t.instanceBuffer, 0, instSize), GPUBufferUsage.VERTEX);
2797
+ let rteDeltas;
2798
+ try {
2799
+ rteDeltas = createInstancedRteDeltaStream(device, t.instanceCount);
2800
+ }
2801
+ catch (error) {
2802
+ // Nothing references this template's buffers yet; free them rather than leak them.
2803
+ vertexBuffer.destroy();
2804
+ indexBuffer.destroy();
2805
+ instanceBuffer.destroy();
2806
+ throw error;
2807
+ }
2823
2808
  // Always append: slots are stable identities, never recycled.
2824
2809
  const templateIndex = this.instancedTemplates.length;
2825
2810
  const template = {
@@ -2830,6 +2815,7 @@ export class Scene {
2830
2815
  instanceBuffer,
2831
2816
  instanceCount: t.instanceCount,
2832
2817
  canonicalAnchors: t.canonicalAnchors,
2818
+ rteDeltas,
2833
2819
  bounds: null,
2834
2820
  maxOccRadius: 0,
2835
2821
  selectedCount: 0,
@@ -2892,7 +2878,7 @@ export class Scene {
2892
2878
  this.instancedEntityMap.set(eid, arr);
2893
2879
  }
2894
2880
  // #2985; no id column or the 0 sentinel ⇒ none. ASSIGNED, never conditionally spread: ONE object shape for records that outlive the shard.
2895
- arr.push({ templateIndex, byteOffset, originalColor, itemId: t.itemIds?.[i] || undefined });
2881
+ arr.push({ templateIndex, byteOffset, originalColor, itemId: t.itemIds?.[i] || undefined, finishBits: (cdv.getUint32(byteOffset + INSTANCE_FLAGS_OFFSET, true) & INSTANCE_FINISH_FLAGS_MASK) >>> 0 });
2896
2882
  // A shard can stream in AFTER a selection was recorded (its ids may
2897
2883
  // exist in earlier shards or the flat path). setInstancedSelection
2898
2884
  // diffs by id and would early-return on the unchanged set, so seed the
@@ -2905,7 +2891,7 @@ export class Scene {
2905
2891
  if (this.instancedOverrideColors?.has(eid))
2906
2892
  lateOverriddenEids.add(eid);
2907
2893
  if (haveBox) {
2908
- const w = this.unionInstancedWorldAabb(eid, cdv, byteOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz);
2894
+ const w = this.unionInstancedWorldAabb(eid, cdv, byteOffset, t.canonicalAnchors, lmnx, lmny, lmnz, lmxx, lmxy, lmxz);
2909
2895
  // Fold the occurrence's world box into the template's cull metadata
2910
2896
  // (union bounds + largest occurrence bounding-sphere radius) for the
2911
2897
  // per-frame instanced frustum/contribution culls. Non-finite boxes
@@ -2935,8 +2921,15 @@ export class Scene {
2935
2921
  this.instancedVisibilityDirty = true;
2936
2922
  this.instancedGhostDirty = true;
2937
2923
  }
2938
- unionInstancedWorldAabb(eid, dv, matOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz) {
2939
- return unionInstanceBounds(this.boundingBoxes, eid, dv, matOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz);
2924
+ unionInstancedWorldAabb(eid, dv, matOffset, anchors, lmnx, lmny, lmnz, lmxx, lmxy, lmxz) {
2925
+ return unionInstanceBounds(this.boundingBoxes, eid, dv, matOffset, anchors, lmnx, lmny, lmnz, lmxx, lmxy, lmxz);
2926
+ }
2927
+ /** Drawn templates holding an occurrence of `expressId` (in `modelIndex` if given), for the hover outline (#5745). */
2928
+ getInstancedTemplatesOf(expressId, modelIndex) {
2929
+ if (!this.instancedVisible)
2930
+ return [];
2931
+ const slots = new Set((this.instancedEntityMap.get(expressId) ?? []).map((o) => o.templateIndex));
2932
+ return [...slots].flatMap((s) => { const t = this.instancedTemplates[s]; return t && (modelIndex === undefined || t.modelIndex === modelIndex) ? [t] : []; });
2940
2933
  }
2941
2934
  /** True when `expressId` has a GPU-instanced occurrence. */
2942
2935
  isInstancedEntity(expressId) {
@@ -3229,7 +3222,7 @@ export class Scene {
3229
3222
  : (eidSelected ? INSTANCE_FLAG_SELECTED : 0);
3230
3223
  const buf = this.instancedTemplates[loc.templateIndex]?.instanceBuffer;
3231
3224
  if (buf)
3232
- device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_FLAGS_OFFSET, new Uint32Array([(selectedBit | hiddenBit) >>> 0]));
3225
+ device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_FLAGS_OFFSET, new Uint32Array([(selectedBit | hiddenBit | (loc.finishBits ?? 0)) >>> 0]));
3233
3226
  }
3234
3227
  }
3235
3228
  writeInstanceColor(device, eid, rgba) {
@@ -3351,6 +3344,7 @@ export class Scene {
3351
3344
  sampler,
3352
3345
  bindGroup,
3353
3346
  color: meshData.color,
3347
+ ...(meshData.material ? { finish: meshData.material } : {}), // IFC-authored (#5984)
3354
3348
  // `world = origin + position` (#1973). Absent on the orphan
3355
3349
  // type-geometry path, whose positions are already absolute.
3356
3350
  origin: meshData.origin
@@ -3395,11 +3389,8 @@ export class Scene {
3395
3389
  */
3396
3390
  destroyAllInstancedTemplates() {
3397
3391
  for (const it of this.instancedTemplates) {
3398
- if (!it)
3399
- continue;
3400
- it.vertexBuffer.destroy();
3401
- it.indexBuffer.destroy();
3402
- it.instanceBuffer.destroy();
3392
+ if (it)
3393
+ destroyInstancedTemplateGpu(it);
3403
3394
  }
3404
3395
  this.instancedTemplates = [];
3405
3396
  this.liveInstancedTemplates = [];
@@ -3482,8 +3473,9 @@ export class Scene {
3482
3473
  this.colorOverrideGeneration++;
3483
3474
  // Destroy streaming fragments (already included in batchedMeshes, but tracked separately)
3484
3475
  this.streamingFragments = [];
3485
- this.destroyOverrideBatches();
3486
3476
  this.colorOverrides = null;
3477
+ this.entityColorTable.clear();
3478
+ this.entityColorTable.releaseGpu();
3487
3479
  // Reset the shared frame origin so the next model picks its own. Retained
3488
3480
  // instanced templates are unaffected — their per-occurrence transforms are
3489
3481
  // already baked to absolute world coordinates at upload time, not relative