@ifc-lite/renderer 4.2.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 (307) 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-params.d.ts +43 -0
  21. package/dist/edge-params.d.ts.map +1 -0
  22. package/dist/edge-params.js +63 -0
  23. package/dist/edge-params.js.map +1 -0
  24. package/dist/edge-pass.d.ts +54 -0
  25. package/dist/edge-pass.d.ts.map +1 -0
  26. package/dist/edge-pass.js +185 -0
  27. package/dist/edge-pass.js.map +1 -0
  28. package/dist/entity-color-table.d.ts +140 -0
  29. package/dist/entity-color-table.d.ts.map +1 -0
  30. package/dist/entity-color-table.js +238 -0
  31. package/dist/entity-color-table.js.map +1 -0
  32. package/dist/gpu-upload-guard.d.ts +1 -1
  33. package/dist/gpu-upload-guard.js +1 -1
  34. package/dist/hover-mesh-cache.d.ts +29 -0
  35. package/dist/hover-mesh-cache.d.ts.map +1 -0
  36. package/dist/hover-mesh-cache.js +31 -0
  37. package/dist/hover-mesh-cache.js.map +1 -0
  38. package/dist/index.d.ts +43 -14
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +196 -189
  41. package/dist/index.js.map +1 -1
  42. package/dist/individual-mesh-upload.d.ts +24 -0
  43. package/dist/individual-mesh-upload.d.ts.map +1 -0
  44. package/dist/individual-mesh-upload.js +85 -0
  45. package/dist/individual-mesh-upload.js.map +1 -0
  46. package/dist/instanced-render.d.ts +22 -12
  47. package/dist/instanced-render.d.ts.map +1 -1
  48. package/dist/instanced-render.js +39 -27
  49. package/dist/instanced-render.js.map +1 -1
  50. package/dist/instanced-rte.d.ts +68 -7
  51. package/dist/instanced-rte.d.ts.map +1 -1
  52. package/dist/instanced-rte.js +98 -17
  53. package/dist/instanced-rte.js.map +1 -1
  54. package/dist/instanced-vertex-layout.d.ts +26 -0
  55. package/dist/instanced-vertex-layout.d.ts.map +1 -0
  56. package/dist/instanced-vertex-layout.js +61 -0
  57. package/dist/instanced-vertex-layout.js.map +1 -0
  58. package/dist/mesh-material.d.ts +26 -0
  59. package/dist/mesh-material.d.ts.map +1 -0
  60. package/dist/mesh-material.js +52 -0
  61. package/dist/mesh-material.js.map +1 -0
  62. package/dist/mesh-rte-uniforms.d.ts +5 -2
  63. package/dist/mesh-rte-uniforms.d.ts.map +1 -1
  64. package/dist/mesh-rte-uniforms.js +5 -2
  65. package/dist/mesh-rte-uniforms.js.map +1 -1
  66. package/dist/model-translation.d.ts.map +1 -1
  67. package/dist/model-translation.js +0 -7
  68. package/dist/model-translation.js.map +1 -1
  69. package/dist/outline-params.d.ts +45 -0
  70. package/dist/outline-params.d.ts.map +1 -0
  71. package/dist/outline-params.js +54 -0
  72. package/dist/outline-params.js.map +1 -0
  73. package/dist/overlay-routing.d.ts +8 -8
  74. package/dist/overlay-routing.js +2 -2
  75. package/dist/overlay-routing.js.map +1 -1
  76. package/dist/overlay-theme-uniforms.d.ts +54 -0
  77. package/dist/overlay-theme-uniforms.d.ts.map +1 -0
  78. package/dist/overlay-theme-uniforms.js +89 -0
  79. package/dist/overlay-theme-uniforms.js.map +1 -0
  80. package/dist/overlay-theme.d.ts +79 -0
  81. package/dist/overlay-theme.d.ts.map +1 -0
  82. package/dist/overlay-theme.js +24 -0
  83. package/dist/overlay-theme.js.map +1 -0
  84. package/dist/partial-batch-requests.d.ts +2 -2
  85. package/dist/pick-mesh-budget.d.ts +28 -0
  86. package/dist/pick-mesh-budget.d.ts.map +1 -0
  87. package/dist/pick-mesh-budget.js +74 -0
  88. package/dist/pick-mesh-budget.js.map +1 -0
  89. package/dist/picker-rte-uniforms.d.ts +7 -1
  90. package/dist/picker-rte-uniforms.d.ts.map +1 -1
  91. package/dist/picker-rte-uniforms.js +9 -1
  92. package/dist/picker-rte-uniforms.js.map +1 -1
  93. package/dist/picker.d.ts.map +1 -1
  94. package/dist/picker.js +15 -28
  95. package/dist/picker.js.map +1 -1
  96. package/dist/picking-manager.d.ts.map +1 -1
  97. package/dist/picking-manager.js +4 -47
  98. package/dist/picking-manager.js.map +1 -1
  99. package/dist/pipeline.d.ts +9 -1
  100. package/dist/pipeline.d.ts.map +1 -1
  101. package/dist/pipeline.js +47 -70
  102. package/dist/pipeline.js.map +1 -1
  103. package/dist/point-picker.d.ts.map +1 -1
  104. package/dist/point-picker.js +7 -2
  105. package/dist/point-picker.js.map +1 -1
  106. package/dist/pointcloud/point-cloud-node.js +1 -1
  107. package/dist/pointcloud/point-cloud-node.js.map +1 -1
  108. package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
  109. package/dist/pointcloud/point-cloud-renderer.js +3 -1
  110. package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
  111. package/dist/pointcloud/point-cloud-uniforms.d.ts +5 -1
  112. package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
  113. package/dist/pointcloud/point-cloud-uniforms.js +7 -1
  114. package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
  115. package/dist/post-pass-chain.d.ts +15 -2
  116. package/dist/post-pass-chain.d.ts.map +1 -1
  117. package/dist/post-pass-chain.js +49 -14
  118. package/dist/post-pass-chain.js.map +1 -1
  119. package/dist/raycast-engine.d.ts +5 -1
  120. package/dist/raycast-engine.d.ts.map +1 -1
  121. package/dist/raycast-engine.js +20 -8
  122. package/dist/raycast-engine.js.map +1 -1
  123. package/dist/raycast-point-cloud-query.d.ts +2 -0
  124. package/dist/raycast-point-cloud-query.d.ts.map +1 -1
  125. package/dist/raycast-point-cloud-query.js +7 -0
  126. package/dist/raycast-point-cloud-query.js.map +1 -1
  127. package/dist/raycast-source-curve-query.d.ts +13 -0
  128. package/dist/raycast-source-curve-query.d.ts.map +1 -0
  129. package/dist/raycast-source-curve-query.js +17 -0
  130. package/dist/raycast-source-curve-query.js.map +1 -0
  131. package/dist/reference-image-pipeline.d.ts.map +1 -1
  132. package/dist/reference-image-pipeline.js +4 -2
  133. package/dist/reference-image-pipeline.js.map +1 -1
  134. package/dist/relative-to-eye-abi.d.ts +36 -0
  135. package/dist/relative-to-eye-abi.d.ts.map +1 -0
  136. package/dist/relative-to-eye-abi.js +72 -0
  137. package/dist/relative-to-eye-abi.js.map +1 -0
  138. package/dist/relative-to-eye.d.ts +15 -35
  139. package/dist/relative-to-eye.d.ts.map +1 -1
  140. package/dist/relative-to-eye.js +46 -74
  141. package/dist/relative-to-eye.js.map +1 -1
  142. package/dist/render-stats.d.ts +5 -1
  143. package/dist/render-stats.d.ts.map +1 -1
  144. package/dist/render-stats.js +1 -1
  145. package/dist/render-stats.js.map +1 -1
  146. package/dist/renderer-overlays.d.ts +8 -7
  147. package/dist/renderer-overlays.d.ts.map +1 -1
  148. package/dist/renderer-overlays.js +14 -16
  149. package/dist/renderer-overlays.js.map +1 -1
  150. package/dist/scene-batch-rebuild.d.ts +1 -2
  151. package/dist/scene-batch-rebuild.d.ts.map +1 -1
  152. package/dist/scene-batch-rebuild.js +4 -8
  153. package/dist/scene-batch-rebuild.js.map +1 -1
  154. package/dist/scene-batch-upload.d.ts.map +1 -1
  155. package/dist/scene-batch-upload.js +7 -3
  156. package/dist/scene-batch-upload.js.map +1 -1
  157. package/dist/scene-contents.d.ts +1 -1
  158. package/dist/scene-contents.d.ts.map +1 -1
  159. package/dist/scene-derived-batches.d.ts +19 -64
  160. package/dist/scene-derived-batches.d.ts.map +1 -1
  161. package/dist/scene-derived-batches.js +16 -51
  162. package/dist/scene-derived-batches.js.map +1 -1
  163. package/dist/scene-device-recovery.d.ts +3 -3
  164. package/dist/scene-device-recovery.d.ts.map +1 -1
  165. package/dist/scene-device-recovery.js +12 -9
  166. package/dist/scene-device-recovery.js.map +1 -1
  167. package/dist/scene-geometry.d.ts +6 -6
  168. package/dist/scene-geometry.d.ts.map +1 -1
  169. package/dist/scene-geometry.js +8 -9
  170. package/dist/scene-geometry.js.map +1 -1
  171. package/dist/scene-instance-bounds.d.ts +1 -1
  172. package/dist/scene-instance-bounds.d.ts.map +1 -1
  173. package/dist/scene-instance-bounds.js +4 -16
  174. package/dist/scene-instance-bounds.js.map +1 -1
  175. package/dist/scene-instance-materialization.d.ts.map +1 -1
  176. package/dist/scene-instance-materialization.js +4 -4
  177. package/dist/scene-instance-materialization.js.map +1 -1
  178. package/dist/scene-instance-translation.d.ts +6 -10
  179. package/dist/scene-instance-translation.d.ts.map +1 -1
  180. package/dist/scene-instance-translation.js +9 -31
  181. package/dist/scene-instance-translation.js.map +1 -1
  182. package/dist/scene-instance-types.d.ts +11 -2
  183. package/dist/scene-instance-types.d.ts.map +1 -1
  184. package/dist/scene-instance-types.js +7 -1
  185. package/dist/scene-instance-types.js.map +1 -1
  186. package/dist/scene-model-translation.d.ts +1 -2
  187. package/dist/scene-model-translation.d.ts.map +1 -1
  188. package/dist/scene-model-translation.js +4 -3
  189. package/dist/scene-model-translation.js.map +1 -1
  190. package/dist/scene.d.ts +44 -30
  191. package/dist/scene.d.ts.map +1 -1
  192. package/dist/scene.js +121 -129
  193. package/dist/scene.js.map +1 -1
  194. package/dist/section-2d-line-buffer.d.ts +2 -2
  195. package/dist/section-2d-line-buffer.d.ts.map +1 -1
  196. package/dist/section-2d-line-buffer.js +10 -11
  197. package/dist/section-2d-line-buffer.js.map +1 -1
  198. package/dist/section-2d-overlay.d.ts +6 -4
  199. package/dist/section-2d-overlay.d.ts.map +1 -1
  200. package/dist/section-2d-overlay.js +45 -21
  201. package/dist/section-2d-overlay.js.map +1 -1
  202. package/dist/section-2d-types.d.ts +1 -1
  203. package/dist/section-2d-types.d.ts.map +1 -1
  204. package/dist/section-2d-types.js +1 -1
  205. package/dist/section-2d-types.js.map +1 -1
  206. package/dist/section-plane.d.ts +3 -0
  207. package/dist/section-plane.d.ts.map +1 -1
  208. package/dist/section-plane.js +18 -25
  209. package/dist/section-plane.js.map +1 -1
  210. package/dist/selection-mask-pass.d.ts +85 -0
  211. package/dist/selection-mask-pass.d.ts.map +1 -0
  212. package/dist/selection-mask-pass.js +170 -0
  213. package/dist/selection-mask-pass.js.map +1 -0
  214. package/dist/selection-mask-pipelines.d.ts +57 -0
  215. package/dist/selection-mask-pipelines.d.ts.map +1 -0
  216. package/dist/selection-mask-pipelines.js +150 -0
  217. package/dist/selection-mask-pipelines.js.map +1 -0
  218. package/dist/selection-outline-frame.d.ts +47 -0
  219. package/dist/selection-outline-frame.d.ts.map +1 -0
  220. package/dist/selection-outline-frame.js +122 -0
  221. package/dist/selection-outline-frame.js.map +1 -0
  222. package/dist/shaders/color-transfer.wgsl.d.ts +5 -3
  223. package/dist/shaders/color-transfer.wgsl.d.ts.map +1 -1
  224. package/dist/shaders/color-transfer.wgsl.js +5 -3
  225. package/dist/shaders/color-transfer.wgsl.js.map +1 -1
  226. package/dist/shaders/depth-reconstruct.wgsl.d.ts +7 -2
  227. package/dist/shaders/depth-reconstruct.wgsl.d.ts.map +1 -1
  228. package/dist/shaders/depth-reconstruct.wgsl.js +8 -3
  229. package/dist/shaders/depth-reconstruct.wgsl.js.map +1 -1
  230. package/dist/shaders/edges.wgsl.d.ts +20 -0
  231. package/dist/shaders/edges.wgsl.d.ts.map +1 -0
  232. package/dist/shaders/edges.wgsl.js +257 -0
  233. package/dist/shaders/edges.wgsl.js.map +1 -0
  234. package/dist/shaders/entity-color-table.wgsl.d.ts +2 -0
  235. package/dist/shaders/entity-color-table.wgsl.d.ts.map +1 -0
  236. package/dist/shaders/entity-color-table.wgsl.js +85 -0
  237. package/dist/shaders/entity-color-table.wgsl.js.map +1 -0
  238. package/dist/shaders/main.wgsl.d.ts +1 -1
  239. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  240. package/dist/shaders/main.wgsl.js +107 -171
  241. package/dist/shaders/main.wgsl.js.map +1 -1
  242. package/dist/shaders/mesh-uniforms.wgsl.d.ts +2 -0
  243. package/dist/shaders/mesh-uniforms.wgsl.d.ts.map +1 -0
  244. package/dist/shaders/mesh-uniforms.wgsl.js +53 -0
  245. package/dist/shaders/mesh-uniforms.wgsl.js.map +1 -0
  246. package/dist/shaders/section-2d-overlay.wgsl.d.ts +1 -0
  247. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
  248. package/dist/shaders/section-2d-overlay.wgsl.js +3 -2
  249. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
  250. package/dist/shaders/selection-mask.wgsl.d.ts +16 -0
  251. package/dist/shaders/selection-mask.wgsl.d.ts.map +1 -0
  252. package/dist/shaders/selection-mask.wgsl.js +138 -0
  253. package/dist/shaders/selection-mask.wgsl.js.map +1 -0
  254. package/dist/shaders/shadow.wgsl.d.ts +1 -1
  255. package/dist/shaders/shadow.wgsl.d.ts.map +1 -1
  256. package/dist/shaders/shadow.wgsl.js +3 -3
  257. package/dist/shaders/specular.wgsl.d.ts +34 -0
  258. package/dist/shaders/specular.wgsl.d.ts.map +1 -0
  259. package/dist/shaders/specular.wgsl.js +134 -0
  260. package/dist/shaders/specular.wgsl.js.map +1 -0
  261. package/dist/shadow-occluders.d.ts.map +1 -1
  262. package/dist/shadow-occluders.js +1 -0
  263. package/dist/shadow-occluders.js.map +1 -1
  264. package/dist/shadow-pass.d.ts +0 -1
  265. package/dist/shadow-pass.d.ts.map +1 -1
  266. package/dist/shadow-pass.js +26 -32
  267. package/dist/shadow-pass.js.map +1 -1
  268. package/dist/shadow-types.d.ts +3 -0
  269. package/dist/shadow-types.d.ts.map +1 -1
  270. package/dist/shadow-types.js.map +1 -1
  271. package/dist/snap-detector.d.ts +5 -0
  272. package/dist/snap-detector.d.ts.map +1 -1
  273. package/dist/snap-detector.js.map +1 -1
  274. package/dist/source-curve-clipping.d.ts +7 -0
  275. package/dist/source-curve-clipping.d.ts.map +1 -0
  276. package/dist/source-curve-clipping.js +165 -0
  277. package/dist/source-curve-clipping.js.map +1 -0
  278. package/dist/source-curve-snap.d.ts +49 -0
  279. package/dist/source-curve-snap.d.ts.map +1 -0
  280. package/dist/source-curve-snap.js +197 -0
  281. package/dist/source-curve-snap.js.map +1 -0
  282. package/dist/symbolic-overlay-pipelines.d.ts +1 -1
  283. package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
  284. package/dist/symbolic-overlay-pipelines.js +13 -22
  285. package/dist/symbolic-overlay-pipelines.js.map +1 -1
  286. package/dist/symbolic-text-rte.d.ts +16 -0
  287. package/dist/symbolic-text-rte.d.ts.map +1 -0
  288. package/dist/symbolic-text-rte.js +37 -0
  289. package/dist/symbolic-text-rte.js.map +1 -0
  290. package/dist/textured-vertices.d.ts.map +1 -1
  291. package/dist/textured-vertices.js +2 -3
  292. package/dist/textured-vertices.js.map +1 -1
  293. package/dist/types.d.ts +10 -0
  294. package/dist/types.d.ts.map +1 -1
  295. package/dist/visual-enhancement.d.ts +10 -7
  296. package/dist/visual-enhancement.d.ts.map +1 -1
  297. package/dist/visual-enhancement.js +9 -8
  298. package/dist/visual-enhancement.js.map +1 -1
  299. package/dist/xray-alpha.d.ts +7 -3
  300. package/dist/xray-alpha.d.ts.map +1 -1
  301. package/dist/xray-alpha.js +11 -7
  302. package/dist/xray-alpha.js.map +1 -1
  303. package/package.json +4 -4
  304. package/dist/post-processor.d.ts +0 -46
  305. package/dist/post-processor.d.ts.map +0 -1
  306. package/dist/post-processor.js +0 -295
  307. package/dist/post-processor.js.map +0 -1
package/dist/index.js CHANGED
@@ -43,6 +43,8 @@ export { hostsOtherEntities } from './mesh-entity-hosting.js';
43
43
  export { FederationRegistry, federationRegistry } from './federation-registry.js';
44
44
  export * from './types.js';
45
45
  export { resolveEnvironment, deriveSkyGradient, packEnvironmentUniforms, ENVIRONMENT_UNIFORM_SIZE, } from './environment.js';
46
+ export { DEFAULT_OVERLAY_THEME } from './overlay-theme.js';
47
+ import { DEFAULT_OVERLAY_THEME } from './overlay-theme.js';
46
48
  export { resolveContributionThresholdPx, projectedAabbRadiusPx } from './contribution-cull.js';
47
49
  export { chunkCellKey, bucketBaseKeyFor, DEFAULT_CHUNK_CELL_SIZE } from './chunk-grid.js';
48
50
  export { selectEvictions, MIN_EVICTION_AGE_FRAMES } from './residency.js';
@@ -76,19 +78,23 @@ import { WebGPUDevice } from './device.js';
76
78
  import { RenderPipeline } from './pipeline.js';
77
79
  import { Camera } from './camera.js';
78
80
  import { Scene } from './scene.js';
81
+ import { remapOverrideColors } from './scene-derived-batches.js';
79
82
  import { Picker } from './picker.js';
80
83
  import { reportableItemId } from './pick-resolve.js';
81
- import { MathUtils, viewBasis } from './math.js';
84
+ import { viewBasis } from './math.js';
82
85
  import { isRteAabbVisible, rteFrustum, sourceFrustumFromRte } from './rte-frustum.js';
83
86
  import { VisualEnhancementResolver, livePostEffects } from './visual-enhancement.js';
84
87
  import { packClipBox } from './clip-box.js';
85
88
  import { MESH_FLAGS_BYTE_OFFSET, MESH_FLAG_RTE_DRAWABLE, MESH_UNIFORM_FLOATS, MESH_UNIFORM_OFFSET, packRteCameraOrigin, packRteFragmentSpace, } from './mesh-rte-uniforms.js';
89
+ import { packMeshMaterial } from './mesh-material.js';
86
90
  import { RendererOverlays } from './renderer-overlays.js';
87
91
  import { resolveSectionPlaneFrame } from './render-section-plane.js';
88
92
  import { PickingManager } from './picking-manager.js';
89
93
  import { RaycastEngine } from './raycast-engine.js';
90
94
  import { RenderDegradationMonitor } from './render-degradation.js';
91
95
  import { PostPassChain } from './post-pass-chain.js';
96
+ import { buildSelectionOutlineFrame, matchesHoveredMesh } from './selection-outline-frame.js';
97
+ import { HoverMeshCache } from './hover-mesh-cache.js';
92
98
  import { InteractionEffectsGovernor } from './interaction-effects-governor.js';
93
99
  import { VisibilityEpochTracker } from './visibility-epoch.js';
94
100
  import { isEntityVisible } from './entity-visibility.js';
@@ -100,21 +106,21 @@ import { resolveEnvironment } from './environment.js';
100
106
  import { ShadowPass, resolveShadowMapResolution } from './shadow-pass.js';
101
107
  import { fitSunLightMatrix, cameraFrustumFocusCorners, resolveShadowNormalBiasMetres } from './shadow-light-matrix.js';
102
108
  import { collectShadowOccluders } from './shadow-occluders.js';
103
- import { uploadInstancedRteDeltas } from './instanced-rte.js';
109
+ import { drawInstanceRuns, uploadInstancedRteDeltas } from './instanced-rte.js';
110
+ import { INSTANCED_RTE_DELTA_SLOT } from './instanced-vertex-layout.js';
104
111
  import { shadowOccluderBatches } from './shadow-occluder-batches.js';
105
112
  import { captureRendererScreenshot } from './renderer-screenshot.js';
106
113
  import { beginRendererColorFrameCapture, cancelRendererColorFrame, discardRendererColorFrameReadback, encodeRendererColorFrameCapture, requestRendererColorFrame, retryRendererColorFrame, settleRendererColorFrameCapture } from './renderer-color-readback.js';
107
114
  import { shouldRouteMeshTransparent, shouldRouteBatchTransparent, splitVisibleIdsByPromotion, DEFAULT_GHOST_ALPHA } from './overlay-routing.js';
115
+ import { batchEntityIdAnchor, batchHasColorOverride, packOverrideParams } from './entity-color-table.js';
108
116
  import { XRayAlpha, XRayEpochTracker } from './xray-alpha.js';
109
117
  import { PartialBatchRequests } from './partial-batch-requests.js';
110
- import { colorSaltByte, packEntityLane } from './scene-geometry.js';
118
+ import { uploadIndividualMesh } from './individual-mesh-upload.js';
111
119
  import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
112
120
  import { appendPointCloudChunk as appendPointCloudChunkImpl, beginPointCloudStream as beginPointCloudStreamImpl, endPointCloudStream as endPointCloudStreamImpl, removePointCloudAsset as removePointCloudAssetImpl, } from './pointcloud/point-cloud-stream-lifecycle.js';
113
121
  import { DeviationComputer } from './deviation/deviation-computer.js';
114
122
  import { runGuardedGpuUpload, isDeviceLossThrow } from './gpu-upload-guard.js';
115
123
  import { recoverRendererDevice, rendererDeviceLostError } from './device-recovery.js';
116
- const MAX_ENCODED_ENTITY_ID = 0xFFFFFF;
117
- let warnedEntityIdRange = false;
118
124
  /**
119
125
  * The reason `whenReady()` rejects when the renderer is destroyed.
120
126
  *
@@ -138,10 +144,13 @@ export class Renderer {
138
144
  scene;
139
145
  picker = null;
140
146
  canvas;
147
+ // The overlay theme (#5484), kept so a pre-init `setOverlayTheme` call
148
+ // (and a later re-init) still lands — `init()` re-applies it to `pipeline`.
149
+ overlayTheme = DEFAULT_OVERLAY_THEME;
141
150
  /**
142
151
  * Section-plane gizmo, 2D section drawing/cap, and the standalone 3D line
143
152
  * + symbolic annotation overlays (issue #2425). Created here rather than in
144
- * `init()` so a pre-init `setOverlayLineColor` still lands — the GPU
153
+ * `init()` so a pre-init `setOverlayTheme` still lands — the GPU
145
154
  * objects inside stay null until `init()` calls `overlays.init()`.
146
155
  */
147
156
  overlays = new RendererOverlays({
@@ -159,6 +168,8 @@ export class Renderer {
159
168
  // Ambient occlusion, separation lines and eye-dome lighting (post-pass-chain.ts),
160
169
  // created on the first rendered frame.
161
170
  postPasses = null;
171
+ /** GPU copies of the hovered entity for the hover outline (#5390); see hover-mesh-cache.ts. */
172
+ hoverMeshes = new HoverMeshCache();
162
173
  /** Device px per CSS px in the drawing buffer; CSS-px sizes scale by it (#5383). */
163
174
  pixelRatio = 1;
164
175
  interactionEffects = new InteractionEffectsGovernor();
@@ -366,6 +377,8 @@ export class Renderer {
366
377
  _xrayEpochs = new XRayEpochTracker();
367
378
  _visibilityVersion = 0;
368
379
  _partialBatchEpoch = 0;
380
+ _xrayAlpha = null;
381
+ _xrayAlphaEpoch = -1;
369
382
  _lastColorOverrideGen = -1;
370
383
  _xrayVersion = 0;
371
384
  _lastHadPartialSources = false;
@@ -394,6 +407,8 @@ export class Renderer {
394
407
  // One 336-byte buffer serves all frame batches/meshes (84 floats including RTE lanes).
395
408
  uniformScratch = new Float32Array(MESH_UNIFORM_FLOATS);
396
409
  uniformScratchU32 = new Uint32Array(this.uniformScratch.buffer, MESH_FLAGS_BYTE_OFFSET, 4);
410
+ // overrideParams lanes (#6076): zero except inside a painting renderBatch call.
411
+ uniformOverrideU32 = new Uint32Array(this.uniformScratch.buffer, MESH_UNIFORM_OFFSET.overrideParams * 4, 4);
397
412
  // What the last render() actually clipped, so the GPU picker can mirror it and
398
413
  // section/crop-clipped geometry stays unpickable, not just invisible. Updated
399
414
  // every render; read by pick()/pickRect(). null = nothing clipped that frame.
@@ -505,6 +520,9 @@ export class Renderer {
505
520
  this.canvas.height = height;
506
521
  }
507
522
  this.pipeline = new RenderPipeline(this.device, width, height);
523
+ // Re-apply any theme set before this (re)creation, or set by a prior
524
+ // init() before a device-loss re-init, so it isn't lost (#5484).
525
+ this.pipeline.selectionColorUniform.update(this.overlayTheme.selection);
508
526
  this.picker = new Picker(this.device, width, height);
509
527
  this.overlays.init(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
510
528
  this.referenceImages.init(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
@@ -970,6 +988,15 @@ export class Renderer {
970
988
  requestRender: () => this.requestRender(),
971
989
  });
972
990
  }
991
+ /** Read per-scan-asset signed-distance statistics after a completed run. */
992
+ async readDeviationAssetStats() {
993
+ return this.deviationComputer.readAssetStats({
994
+ device: this.device,
995
+ scene: this.scene,
996
+ pointCloudRenderer: this.pointCloudRenderer,
997
+ requestRender: () => this.requestRender(),
998
+ });
999
+ }
973
1000
  /**
974
1001
  * Toggle Eye-Dome Lighting and tune its strength.
975
1002
  *
@@ -1143,6 +1170,10 @@ export class Renderer {
1143
1170
  createMeshFromData(meshData) {
1144
1171
  return runGuardedGpuUpload(() => this.isDeviceLost(), () => this.createMeshFromDataUnguarded(meshData), (error) => this.reportUploadLoss(error));
1145
1172
  }
1173
+ /** The source batch's frame for an individual copy of `meshData` (see individual-mesh-upload.ts). */
1174
+ individualMeshFrame(meshData) {
1175
+ return { sharedOrigin: this.scene.getSharedFrameOrigin(meshData.modelIndex, meshData), quantized: this.scene.isMeshQuantized(meshData) };
1176
+ }
1146
1177
  /**
1147
1178
  * Create a GPU Mesh from MeshData (lazy creation for selection highlighting)
1148
1179
  * This is called on-demand when a mesh is selected, avoiding 2x buffer creation during streaming
@@ -1150,71 +1181,7 @@ export class Renderer {
1150
1181
  createMeshFromDataUnguarded(meshData) {
1151
1182
  if (!this.device.isInitialized())
1152
1183
  return;
1153
- const device = this.device.getDevice();
1154
- const vertexCount = meshData.positions.length / 3;
1155
- const interleavedRaw = new ArrayBuffer(vertexCount * 7 * 4);
1156
- const interleaved = new Float32Array(interleavedRaw);
1157
- const interleavedU32 = new Uint32Array(interleavedRaw);
1158
- // Build this individual mesh (selection highlight + GPU object-id picker)
1159
- // in the same small local frame as its source batch.
1160
- // CRITICAL: replicate the BATCH's exact two-step f32 path so the highlight
1161
- // is bit-coincident with its source surface (no z-fight, no depth bias):
1162
- // batch stores s = f32(local + (origin - sharedOrigin)) [merge]
1163
- // batch shader RTE = sharedOrigin - eye + s [draw]
1164
- // We retain `s` and the canonical origin separately. When there is no
1165
- // shared origin, retain the piece origin instead of folding it in.
1166
- const o = meshData.origin;
1167
- const so = this.scene.getSharedFrameOrigin(meshData.modelIndex, meshData);
1168
- const ox = o ? o[0] : 0, oy = o ? o[1] : 0, oz = o ? o[2] : 0;
1169
- const fr = Math.fround;
1170
- const dx = so ? (ox - so[0]) : ox, dy = so ? (oy - so[1]) : oy, dz = so ? (oz - so[2]) : oz;
1171
- // Quantized batches (issue #1682 phase 6) render lattice-snapped
1172
- // positions: the shader's quantMin + q*step is exactly the lattice
1173
- // node nearest the batch's stored f32 rel coordinate. Reproduce it by
1174
- // snapping the SAME rel coordinate here (round(s*1024)/1024 in f64
1175
- // yields the identical exact-f32 lattice value — see quantize.ts), so
1176
- // the highlight/picker mesh stays BIT-coincident with its quantized
1177
- // source surface, exactly as the two-step fold above achieves for the
1178
- // f32 path. Meshes whose batch fell back to f32 must not snap.
1179
- const snap = this.scene.isMeshQuantized(meshData)
1180
- ? (v) => Math.round(v * 1024) / 1024
1181
- : (v) => v;
1182
- const p = meshData.positions;
1183
- for (let i = 0; i < vertexCount; i++) {
1184
- const base = i * 7;
1185
- const posBase = i * 3;
1186
- interleaved[base] = so ? snap(fr(p[posBase] + dx)) : snap(p[posBase]);
1187
- interleaved[base + 1] = so ? snap(fr(p[posBase + 1] + dy)) : snap(p[posBase + 1]);
1188
- interleaved[base + 2] = so ? snap(fr(p[posBase + 2] + dz)) : snap(p[posBase + 2]);
1189
- const hasNormals = meshData.normals.length > 0;
1190
- interleaved[base + 3] = hasNormals ? meshData.normals[posBase] : 0;
1191
- interleaved[base + 4] = hasNormals ? meshData.normals[posBase + 1] : 0;
1192
- interleaved[base + 5] = hasNormals ? meshData.normals[posBase + 2] : 0;
1193
- let encodedId = meshData.expressId >>> 0;
1194
- if (encodedId > MAX_ENCODED_ENTITY_ID) {
1195
- if (!warnedEntityIdRange) {
1196
- warnedEntityIdRange = true;
1197
- console.warn('[Renderer] expressId exceeds 24-bit seam-ID encoding range; seam lines may collide.');
1198
- }
1199
- encodedId = encodedId & MAX_ENCODED_ENTITY_ID;
1200
- }
1201
- // Stamp the SAME high-byte material-colour salt as the batch path
1202
- // (mergeGeometry) so this individual/selection mesh computes the
1203
- // identical depth nudge as its source batch — otherwise the highlight
1204
- // (selection pipeline, reverse-Z 'greater-equal') would z-fight or drop
1205
- // out against the salted base depth. Low 24 bits stay the picking id.
1206
- interleavedU32[base + 6] = packEntityLane(encodedId, colorSaltByte(meshData.color));
1207
- }
1208
- const vertexBuffer = device.createBuffer({
1209
- size: interleaved.byteLength,
1210
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1211
- });
1212
- device.queue.writeBuffer(vertexBuffer, 0, interleaved);
1213
- const indexBuffer = device.createBuffer({
1214
- size: meshData.indices.byteLength,
1215
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
1216
- });
1217
- device.queue.writeBuffer(indexBuffer, 0, meshData.indices);
1184
+ const { vertexBuffer, indexBuffer, indexCount, transform, rteOrigin } = uploadIndividualMesh(this.device.getDevice(), meshData, this.individualMeshFrame(meshData));
1218
1185
  // Keep the hydrated mesh in its decoded local frame. Folding this
1219
1186
  // origin back into f32 vertices used to make selection/highlight the
1220
1187
  // only mesh path that lost centimetres at national-grid coordinates.
@@ -1231,16 +1198,11 @@ export class Renderer {
1231
1198
  geometryItemId: reportableItemId(meshData, meshData.expressId),
1232
1199
  vertexBuffer,
1233
1200
  indexBuffer,
1234
- indexCount: meshData.indices.length,
1235
- transform: (() => {
1236
- const transform = MathUtils.identity();
1237
- transform.m[12] = so ? so[0] : ox;
1238
- transform.m[13] = so ? so[1] : oy;
1239
- transform.m[14] = so ? so[2] : oz;
1240
- return transform;
1241
- })(),
1242
- rteOrigin: so ? [so[0], so[1], so[2]] : [ox, oy, oz],
1201
+ indexCount,
1202
+ transform,
1203
+ rteOrigin,
1243
1204
  color: meshData.color,
1205
+ ...(meshData.material ? { finish: meshData.material } : {}), // #5582
1244
1206
  hydrated: true,
1245
1207
  });
1246
1208
  }
@@ -1510,16 +1472,8 @@ export class Renderer {
1510
1472
  // governor's verdict so early navigation can't degrade a session.
1511
1473
  const timingUnstable = options.isStreaming === true || this.scene.hasQueuedMeshes();
1512
1474
  const effectsLive = this.interactionEffects.frame(interacting, performance.now(), timingUnstable, options.interactionFrameIntervalMs ?? 0);
1513
- // Edge contrast is NOT interaction-gated: its per-fragment work runs
1514
- // unconditionally in the shader and the gated tail is a handful of
1515
- // ALU ops, so disabling it bought nothing and only made the crease
1516
- // darkening pop off/on around gestures (visible in ortho).
1517
- const edgeEnabled = visualEnhancement.enabled && visualEnhancement.edgeContrast.enabled;
1518
- const edgeIntensity = Math.min(3.0, Math.max(0.0, visualEnhancement.edgeContrast.intensity));
1519
- const edgeEnabledU32 = edgeEnabled ? 1 : 0;
1520
- const edgeIntensityMilliU32 = Math.round(edgeIntensity * 1000);
1521
- // Only the separation lines read the object-id attachment after the pass.
1522
- const needsObjectIdPass = livePostEffects(visualEnhancement, effectsLive).separationLines;
1475
+ // Only the edge pass reads the object-id attachment after the pass.
1476
+ const needsObjectIdPass = livePostEffects(visualEnhancement, effectsLive).edges;
1523
1477
  // Check if visibility filtering is active
1524
1478
  const hasHiddenFilter = options.hiddenIds && options.hiddenIds.size > 0;
1525
1479
  const hasIsolatedFilter = options.isolatedIds !== null && options.isolatedIds !== undefined;
@@ -1594,10 +1548,6 @@ export class Renderer {
1594
1548
  // it carries a per-instance hidden flag the shader discards on). Diffed → a
1595
1549
  // no-op when visibility is unchanged.
1596
1550
  this.scene.setInstancedVisibility(options.hiddenIds, options.isolatedIds);
1597
- // Per-frame alpha overrides for X-Ray mode. See RenderOptions.transparencyOverrides.
1598
- // XRayAlpha snapshots the caller's map so mid-frame mutation can't desync
1599
- // classification and uniform-write decisions for the same batch/mesh, and
1600
- // owns the per-entity resolution + the mixed-batch partition (#4129).
1601
1551
  // X-Ray *context* mode (`ghostExceptIds`) feeds the same machinery, so it
1602
1552
  // routes through the transparent pipeline with no extra call sites — and
1603
1553
  // avoids building a Map over every element just to fade "the rest".
@@ -1607,16 +1557,19 @@ export class Renderer {
1607
1557
  // the user asked to fade the building and got a solid facade standing
1608
1558
  // in front of a ghosted interior.
1609
1559
  this.scene.setInstancedGhosting(options.ghostExceptIds ?? null, selectedExpressIds, ghostAlpha);
1610
- const xray = new XRayAlpha(options, selectedExpressIds);
1560
+ if (this._xrayAlpha === null || this._xrayAlphaEpoch !== this._partialBatchEpoch) {
1561
+ this._xrayAlpha = new XRayAlpha(options, selectedExpressIds);
1562
+ this._xrayAlphaEpoch = this._partialBatchEpoch;
1563
+ }
1564
+ const xray = this._xrayAlpha;
1611
1565
  const alphaForMesh = (expressId, fallback) => xray.forEntity(expressId, fallback);
1612
1566
  const alphaForBatch = (batch, fallback) => xray.forBatch(batch, fallback);
1613
1567
  // Lens / Pset color overrides: when an entity has an override, force
1614
- // its base draw through the opaque pipeline so it writes depth. The
1615
- // overlay paint pass uses depthCompare 'equal' and otherwise silently
1616
- // drops fragments belonging to entities whose default pipeline is
1617
- // transparent (IfcSpace, IfcOpeningElement, glass, …). See issue #677.
1618
- // Pure routing decision lives in overlay-routing.ts and is unit-tested
1619
- // there.
1568
+ // its base draw through the opaque pipeline — the only draws the
1569
+ // entity colour table paints (#6076) — or an entity whose default
1570
+ // pipeline is transparent (IfcSpace, IfcOpeningElement, glass, …)
1571
+ // would never show it. See issue #677. Pure routing decision lives in
1572
+ // overlay-routing.ts and is unit-tested there.
1620
1573
  const colorOverrides = this.scene.getColorOverrides();
1621
1574
  // PERFORMANCE FIX: Use batch-level visibility filtering instead of creating individual meshes
1622
1575
  // Only create individual meshes for selected elements (for highlighting)
@@ -1744,6 +1697,11 @@ export class Renderer {
1744
1697
  max: [...options.clipBox.max],
1745
1698
  }
1746
1699
  : null;
1700
+ // Meshes this camera's RTE frame cannot represent (#6128). Nothing
1701
+ // culls individual meshes before this point, so a far-off mesh (a
1702
+ // stray element, one hydrated for picking) must be skipped, not
1703
+ // allowed to fail every frame.
1704
+ const outsideEyeEnvelope = new Set();
1747
1705
  // Reuse pooled scratch buffer for per-mesh uniform writes
1748
1706
  const meshBuf = this.uniformScratch;
1749
1707
  const meshFlags = this.uniformScratchU32;
@@ -1764,10 +1722,7 @@ export class Renderer {
1764
1722
  meshBuf[34] = mesh.color[2];
1765
1723
  // Selected meshes always keep their own alpha so highlights stay opaque
1766
1724
  meshBuf[35] = isSelected ? mesh.color[3] : alphaForMesh(mesh.expressId, mesh.color[3]);
1767
- meshBuf[36] = mesh.material?.metallic ?? 0.0;
1768
- meshBuf[37] = mesh.material?.roughness ?? 0.6;
1769
- meshBuf[38] = 0;
1770
- meshBuf[39] = 0;
1725
+ packMeshMaterial(meshBuf, mesh.color[3], mesh.finish ?? mesh.material);
1771
1726
  // Section plane data (offset 40-43)
1772
1727
  if (sectionPlaneData) {
1773
1728
  meshBuf[40] = sectionPlaneData.normal[0];
@@ -1790,14 +1745,17 @@ export class Renderer {
1790
1745
  (sectionPlaneData?.enabled ? 1 : 0) |
1791
1746
  (options.sectionPlane?.flipped ? 2 : 0) |
1792
1747
  clipBit;
1793
- meshFlags[2] = edgeEnabledU32;
1794
- meshFlags[3] = edgeIntensityMilliU32;
1748
+ meshFlags[2] = 0; // unused since #5746 (was the derivative edge darkening)
1749
+ meshFlags[3] = 0;
1795
1750
  // Individual meshes retain their origin and share the RTE
1796
1751
  // fragment contract with colour batches.
1797
1752
  packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, meshBuf);
1798
1753
  const meshOrigin = mesh.rteOrigin
1799
1754
  ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
1800
- relativeToEyeFrame.packDrawableOrigin(meshOrigin, meshBuf, MESH_UNIFORM_OFFSET.drawableDelta);
1755
+ if (!relativeToEyeFrame.tryPackDrawableOrigin(meshOrigin, meshBuf, MESH_UNIFORM_OFFSET.drawableDelta)) {
1756
+ outsideEyeEnvelope.add(mesh);
1757
+ continue;
1758
+ }
1801
1759
  meshFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
1802
1760
  device.queue.writeBuffer(mesh.uniformBuffer, 0, meshBuf);
1803
1761
  }
@@ -2035,7 +1993,13 @@ export class Renderer {
2035
1993
  // only presets that enable the sky — blanked the model on those
2036
1994
  // drivers. Binding while the main pipeline is current keeps it
2037
1995
  // valid for every main-family draw that follows.
1996
+ this.pipeline.setEntityColorTableBuffer(this.scene.getEntityColorTable().getBuffer());
2038
1997
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
1998
+ // Selected meshes drawn this frame, for the selection-mask pass
1999
+ // (#5390) after `pass.end()` below. Populated inside the batched
2000
+ // branch, where the highlight draw itself happens; empty in the
2001
+ // no-batches fallback, which does not draw selection either.
2002
+ let selectedMeshesForMask = [];
2039
2003
  // Check if we have batched meshes (preferred for performance)
2040
2004
  const allBatchedMeshes = this.scene.getBatchedMeshes();
2041
2005
  // PERFORMANCE FIX: Always use batch rendering when we have batches
@@ -2244,12 +2208,9 @@ export class Renderer {
2244
2208
  tpl[29] = 0;
2245
2209
  tpl[30] = 0;
2246
2210
  tpl[31] = 1;
2247
- // Color placeholder — overwritten per batch
2248
- // tpl[32..35] set per batch
2249
- tpl[36] = 0.0; // metallic
2250
- tpl[37] = 0.6; // roughness
2251
- tpl[38] = 0;
2252
- tpl[39] = 0; // padding
2211
+ // Colour + material row (tpl[32..39]) are patched per draw; the
2212
+ // instanced passes keep this opaque default.
2213
+ packMeshMaterial(tpl);
2253
2214
  if (sectionPlaneData) {
2254
2215
  tpl[40] = sectionPlaneData.normal[0];
2255
2216
  tpl[41] = sectionPlaneData.normal[1];
@@ -2268,29 +2229,34 @@ export class Renderer {
2268
2229
  // x = isSelected (0/1)
2269
2230
  // y = section/clip bitfield:
2270
2231
  // bit 0 = sectionEnabled, bit 1 = flipped, bit 2 = clipBoxEnabled
2271
- // z = edgeEnabled (0/1)
2272
- // w = edgeIntensityMilli
2232
+ // z, w = unused, written as 0. They fed the in-shader derivative
2233
+ // edge darkening, removed in #5746; edges come from the
2234
+ // edge pass. The lanes stay so the struct layout is unchanged.
2273
2235
  tplFlags[0] = 0;
2274
2236
  tplFlags[1] =
2275
2237
  (sectionPlaneData?.enabled ? 1 : 0) |
2276
2238
  (options.sectionPlane?.flipped ? 2 : 0) |
2277
2239
  tplClipBit;
2278
- tplFlags[2] = edgeEnabledU32;
2279
- tplFlags[3] = edgeIntensityMilliU32;
2240
+ tplFlags[2] = 0;
2241
+ tplFlags[3] = 0;
2280
2242
  // Flat/quantized/textured batches enter WGSL in the single
2281
2243
  // camera-relative frame. Their fragment clip inputs must use
2282
2244
  // that same frame; mixing a local vertex with a 5,000 km f32
2283
2245
  // world plane loses centimetre cuts before the comparison.
2284
2246
  packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2285
- // Helper function to render a batch — patches color into the shared template
2286
- const renderBatch = (batch) => {
2247
+ // Helper function to render a batch — patches color into the shared template.
2248
+ // `paint` = a depth-writing opaque draw: colour overrides shade it from the
2249
+ // entity colour table (#6076); transparent draws keep their own colour.
2250
+ const renderBatch = (batch, paint = false) => {
2287
2251
  if (!batch.bindGroup || !batch.uniformBuffer)
2288
2252
  return;
2289
- // Patch only the per-batch color (4 floats at offset 32)
2253
+ // Patch the per-batch colour, and the material its AUTHORED
2254
+ // alpha implies: an X-Ray fade is not glass (#5386).
2290
2255
  tpl[32] = batch.color[0];
2291
2256
  tpl[33] = batch.color[1];
2292
2257
  tpl[34] = batch.color[2];
2293
2258
  tpl[35] = alphaForBatch(batch, batch.color[3]);
2259
+ packMeshMaterial(tpl, batch.color[3], batch.finish);
2294
2260
  // Per-batch local frame: the batch's vertices are stored
2295
2261
  // RELATIVE to batch.origin (f32-small), so set the model
2296
2262
  // matrix translation column to origin (world = origin + pos).
@@ -2304,7 +2270,8 @@ export class Renderer {
2304
2270
  // The regular model matrix remains for absolute-space
2305
2271
  // fragment work (section/shadow); vertex projection uses
2306
2272
  // this f64-subtracted high/low origin instead.
2307
- relativeToEyeFrame.packDrawableOrigin(o ?? [0, 0, 0], tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2273
+ if (!relativeToEyeFrame.tryPackDrawableOrigin(o ?? [0, 0, 0], tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2274
+ return;
2308
2275
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2309
2276
  // Quantized dequantization params (issue #1682 phase 6);
2310
2277
  // zeroed for f32 batches (their pipelines ignore them).
@@ -2313,7 +2280,10 @@ export class Renderer {
2313
2280
  tpl[57] = qz ? qz.min[1] : 0;
2314
2281
  tpl[58] = qz ? qz.min[2] : 0;
2315
2282
  tpl[59] = qz ? qz.step : 0;
2283
+ const painted = paint && batchHasColorOverride(batch, colorOverrides, colorOverrideGen);
2284
+ packOverrideParams(this.uniformOverrideU32, painted ? batchEntityIdAnchor(batch) : null, painted, options.emphasizeOverrides === true);
2316
2285
  device.queue.writeBuffer(batch.uniformBuffer, 0, tpl);
2286
+ this.uniformOverrideU32.fill(0);
2317
2287
  // Single draw call for entire batch! LOD1-selected batches
2318
2288
  // bind their simplified index range over the same vertices.
2319
2289
  const useLod1 = batch.lod1IndexBuffer && batch.lod1IndexCount && lodBatches.has(batch.id);
@@ -2336,11 +2306,7 @@ export class Renderer {
2336
2306
  // probe (enableQuantizedBatches) verified they exist, so the
2337
2307
  // base fallback here is type-safety, never taken.
2338
2308
  const pipeFor = (batch, kind) => {
2339
- const base = kind === 'opaque'
2340
- ? this.pipeline.getPipeline()
2341
- : kind === 'transparent'
2342
- ? this.pipeline.getTransparentPipeline()
2343
- : this.pipeline.getOverlayPipeline();
2309
+ const base = kind === 'opaque' ? this.pipeline.getPipeline() : this.pipeline.getTransparentPipeline();
2344
2310
  if (!batch.quantized)
2345
2311
  return base;
2346
2312
  return this.pipeline.getQuantizedPipelineVariant(kind) ?? base;
@@ -2357,7 +2323,7 @@ export class Renderer {
2357
2323
  pass.setPipeline(this.pipeline.getPipeline());
2358
2324
  for (const batch of opaqueBatches) {
2359
2325
  pass.setPipeline(pipeFor(batch, 'opaque'));
2360
- renderBatch(batch);
2326
+ renderBatch(batch, true);
2361
2327
  }
2362
2328
  pass.setPipeline(this.pipeline.getPipeline());
2363
2329
  // GPU-instancing pass — repeated geometry collated by the producer
@@ -2402,8 +2368,11 @@ export class Renderer {
2402
2368
  }
2403
2369
  visibleInstanced = kept;
2404
2370
  }
2371
+ // Per-template instance runs inside this frame's RTE envelope,
2372
+ // shared by the opaque and transparent instanced sub-passes.
2373
+ let visibleInstancedRuns = [];
2405
2374
  if (visibleInstanced.length > 0) {
2406
- uploadInstancedRteDeltas(device, visibleInstanced, relativeToEyeFrame.getCameraWorld());
2375
+ visibleInstancedRuns = uploadInstancedRteDeltas(device, visibleInstanced, relativeToEyeFrame.getCameraWorld());
2407
2376
  // Opaque instanced pass. flags.x bit 2 marks "instanced pass" so the
2408
2377
  // shader routes per-instance opacity: opaque (or selected) occurrences
2409
2378
  // draw here; translucent ones (lens/x-ray/compare overrides) are
@@ -2412,13 +2381,15 @@ export class Renderer {
2412
2381
  pass.setPipeline(this.pipeline.getInstancedPipeline());
2413
2382
  pass.setBindGroup(0, this.pipeline.getBindGroup());
2414
2383
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
2415
- for (const it of visibleInstanced) {
2384
+ for (const [i, it] of visibleInstanced.entries()) {
2416
2385
  pass.setVertexBuffer(0, it.vertexBuffer);
2417
2386
  pass.setVertexBuffer(1, it.instanceBuffer);
2387
+ pass.setVertexBuffer(INSTANCED_RTE_DELTA_SLOT, it.rteDeltas.buffer);
2418
2388
  pass.setIndexBuffer(it.indexBuffer, 'uint32');
2419
- pass.drawIndexed(it.indexCount, it.instanceCount);
2420
- frameDrawCalls++;
2421
- frameInstancedDrawn++;
2389
+ const runs = visibleInstancedRuns[i];
2390
+ frameDrawCalls += drawInstanceRuns(pass, it.indexCount, runs);
2391
+ if (runs.length > 0)
2392
+ frameInstancedDrawn++;
2422
2393
  }
2423
2394
  pass.setPipeline(this.pipeline.getPipeline());
2424
2395
  // The TRANSPARENT instanced sub-pass is drawn later, alongside the
@@ -2453,7 +2424,7 @@ export class Renderer {
2453
2424
  if (txAlpha <= 0.01)
2454
2425
  continue;
2455
2426
  // Lens / Pset colour override tints the sampled texel — the
2456
- // batch overlay paint pass doesn't iterate textured meshes,
2427
+ // entity colour table is off for textured draws (#6076),
2457
2428
  // so applying the override here is what recolours them.
2458
2429
  const txOverride = colorOverrides?.get(tm.expressId);
2459
2430
  // `world = origin + position`: the vertex buffer stores
@@ -2468,12 +2439,23 @@ export class Renderer {
2468
2439
  tpl[28] = tm.origin[0];
2469
2440
  tpl[29] = tm.origin[1];
2470
2441
  tpl[30] = tm.origin[2];
2471
- relativeToEyeFrame.packDrawableOrigin(tm.origin, tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2442
+ if (!relativeToEyeFrame.tryPackDrawableOrigin(tm.origin, tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2443
+ continue;
2472
2444
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2473
2445
  tpl[32] = txOverride ? txOverride[0] : tm.color[0];
2474
2446
  tpl[33] = txOverride ? txOverride[1] : tm.color[1];
2475
2447
  tpl[34] = txOverride ? txOverride[2] : tm.color[2];
2476
2448
  tpl[35] = txAlpha;
2449
+ // Same authored-alpha + material contract as the flat/batched
2450
+ // paths (mesh-material.ts) — a call with neither argument
2451
+ // silently gave every textured metal or translucent mesh the
2452
+ // opaque dielectric default (#5623 review). The pipeline
2453
+ // itself is still opaque/depth-write only (no blend state,
2454
+ // see `txAlpha` above), so a translucent authored alpha still
2455
+ // never actually blends here — only the glass/roughness
2456
+ // CHOICE in the shader follows the same rule as everywhere
2457
+ // else, for the same reason `tm.material` does.
2458
+ packMeshMaterial(tpl, tm.color[3], tm.finish ?? tm.material);
2477
2459
  device.queue.writeBuffer(tm.uniformBuffer, 0, tpl);
2478
2460
  pass.setBindGroup(0, tm.bindGroup);
2479
2461
  pass.setVertexBuffer(0, tm.vertexBuffer);
@@ -2504,7 +2486,7 @@ export class Renderer {
2504
2486
  // Use opaque or transparent pipeline based on resolved alpha
2505
2487
  // (not the parent batch's color[3] — that ignores transparencyOverrides).
2506
2488
  // Promote to opaque if any expressId in the sub-batch carries a
2507
- // lens/Pset colour override, so the overlay paint pass finds depth.
2489
+ // lens/Pset colour override, so the colour table paints it (#6076).
2508
2490
  const isTransparent = shouldRouteBatchTransparent(alphaForBatch(subBatch, color[3]), subBatch.expressIds, colorOverrides);
2509
2491
  if (isTransparent) {
2510
2492
  transparentSubBatches.push(subBatch);
@@ -2518,7 +2500,7 @@ export class Renderer {
2518
2500
  pass.setPipeline(pipeFor(subBatch, 'opaque'));
2519
2501
  opaqueSubBatches.push(subBatch);
2520
2502
  // Render the sub-batch as a single draw call
2521
- renderBatch(subBatch);
2503
+ renderBatch(subBatch, true);
2522
2504
  }
2523
2505
  }
2524
2506
  for (const subBatch of transparentSubBatches) {
@@ -2528,28 +2510,6 @@ export class Renderer {
2528
2510
  // Reset to opaque pipeline for subsequent rendering
2529
2511
  pass.setPipeline(this.pipeline.getPipeline());
2530
2512
  }
2531
- // Render color overlay batches (lens coloring) on top of ALL opaque geometry.
2532
- // Placed AFTER partial batches so depth buffer is complete for both full
2533
- // and partial batches. Uses 'equal' depth compare — only paints where
2534
- // original geometry wrote depth, so hidden entities never leak through.
2535
- //
2536
- // flags.x bit 1 = overlay: tells the shader to preserve baseColor.a
2537
- // (the overlay pipeline now has src-alpha blending so low-alpha ghost
2538
- // tints composite correctly against the opaque pass) AND skip the
2539
- // glass-fresnel branch (which is meant for real glass materials and
2540
- // would whiten low-alpha colour overrides at grazing angles).
2541
- const overrideBatches = this.scene.getOverrideBatches();
2542
- if (overrideBatches.length > 0) {
2543
- pass.setPipeline(this.pipeline.getOverlayPipeline());
2544
- // bit 1 = overlay; bit 5 (32) = emphasize (pop) — see shader.
2545
- tplFlags[0] = options.emphasizeOverrides ? (2 | 32) : 2;
2546
- for (const batch of overrideBatches) {
2547
- pass.setPipeline(pipeFor(batch, 'overlay'));
2548
- renderBatch(batch);
2549
- }
2550
- tplFlags[0] = 0; // restore for any downstream use of the template
2551
- pass.setPipeline(this.pipeline.getPipeline());
2552
- }
2553
2513
  // Filled, hatched 3D cut surfaces are now rendered by
2554
2514
  // Section2DOverlayRenderer using the exact polygons from
2555
2515
  // SectionCutter (triangle-plane intersection). The old
@@ -2607,6 +2567,7 @@ export class Renderer {
2607
2567
  return true;
2608
2568
  })
2609
2569
  : [];
2570
+ selectedMeshesForMask = selectedMeshes;
2610
2571
  // Transparent instanced sub-pass — drawn here (after ALL opaque incl. the
2611
2572
  // textured sub-pass) so ghosted/x-rayed instanced occurrences blend over
2612
2573
  // a complete depth buffer. Only runs when an override actually made some
@@ -2620,12 +2581,12 @@ export class Renderer {
2620
2581
  pass.setPipeline(instancedTransparentPipeline);
2621
2582
  pass.setBindGroup(0, this.pipeline.getBindGroup());
2622
2583
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
2623
- for (const it of visibleInstanced) {
2584
+ for (const [i, it] of visibleInstanced.entries()) {
2624
2585
  pass.setVertexBuffer(0, it.vertexBuffer);
2625
2586
  pass.setVertexBuffer(1, it.instanceBuffer);
2587
+ pass.setVertexBuffer(INSTANCED_RTE_DELTA_SLOT, it.rteDeltas.buffer);
2626
2588
  pass.setIndexBuffer(it.indexBuffer, 'uint32');
2627
- pass.drawIndexed(it.indexCount, it.instanceCount);
2628
- frameDrawCalls++;
2589
+ frameDrawCalls += drawInstanceRuns(pass, it.indexCount, visibleInstancedRuns[i]);
2629
2590
  }
2630
2591
  pass.setPipeline(this.pipeline.getPipeline());
2631
2592
  }
@@ -2650,10 +2611,7 @@ export class Renderer {
2650
2611
  tpl[33] = mesh.color[1];
2651
2612
  tpl[34] = mesh.color[2];
2652
2613
  tpl[35] = alphaForMesh(mesh.expressId, mesh.color[3]);
2653
- tpl[36] = mesh.material?.metallic ?? 0.0;
2654
- tpl[37] = mesh.material?.roughness ?? 0.6;
2655
- tpl[38] = 0;
2656
- tpl[39] = 0;
2614
+ packMeshMaterial(tpl, mesh.color[3], mesh.finish ?? mesh.material);
2657
2615
  if (sectionPlaneData) {
2658
2616
  tpl[40] = sectionPlaneData.normal[0];
2659
2617
  tpl[41] = sectionPlaneData.normal[1];
@@ -2671,11 +2629,12 @@ export class Renderer {
2671
2629
  (sectionPlaneData?.enabled ? 1 : 0) |
2672
2630
  (options.sectionPlane?.flipped ? 2 : 0) |
2673
2631
  tplClipBit;
2674
- tplFlags[2] = edgeEnabledU32;
2675
- tplFlags[3] = edgeIntensityMilliU32;
2632
+ tplFlags[2] = 0;
2633
+ tplFlags[3] = 0;
2676
2634
  packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2677
2635
  const transparentOrigin = mesh.rteOrigin ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
2678
- relativeToEyeFrame.packDrawableOrigin(transparentOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2636
+ if (!relativeToEyeFrame.tryPackDrawableOrigin(transparentOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2637
+ continue;
2679
2638
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2680
2639
  device.queue.writeBuffer(mesh.uniformBuffer, 0, tpl);
2681
2640
  pass.setBindGroup(0, mesh.bindGroup);
@@ -2714,10 +2673,7 @@ export class Renderer {
2714
2673
  tpl[33] = mesh.color[1];
2715
2674
  tpl[34] = mesh.color[2];
2716
2675
  tpl[35] = mesh.color[3];
2717
- tpl[36] = mesh.material?.metallic ?? 0.0;
2718
- tpl[37] = mesh.material?.roughness ?? 0.6;
2719
- tpl[38] = 0;
2720
- tpl[39] = 0;
2676
+ packMeshMaterial(tpl, mesh.color[3], mesh.finish ?? mesh.material);
2721
2677
  if (sectionPlaneData) {
2722
2678
  tpl[40] = sectionPlaneData.normal[0];
2723
2679
  tpl[41] = sectionPlaneData.normal[1];
@@ -2735,11 +2691,12 @@ export class Renderer {
2735
2691
  (sectionPlaneData?.enabled ? 1 : 0) |
2736
2692
  (options.sectionPlane?.flipped ? 2 : 0) |
2737
2693
  tplClipBit;
2738
- tplFlags[2] = edgeEnabledU32;
2739
- tplFlags[3] = edgeIntensityMilliU32;
2694
+ tplFlags[2] = 0;
2695
+ tplFlags[3] = 0;
2740
2696
  packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2741
2697
  const selectedOrigin = mesh.rteOrigin ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
2742
- relativeToEyeFrame.packDrawableOrigin(selectedOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2698
+ if (!relativeToEyeFrame.tryPackDrawableOrigin(selectedOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2699
+ continue;
2743
2700
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2744
2701
  device.queue.writeBuffer(mesh.uniformBuffer, 0, tpl);
2745
2702
  pass.setPipeline(this.pipeline.getSelectionPipeline());
@@ -2754,6 +2711,8 @@ export class Renderer {
2754
2711
  // Fallback: render individual meshes (only when no batches exist)
2755
2712
  // Render opaque meshes with per-mesh bind groups
2756
2713
  for (const mesh of opaqueMeshes) {
2714
+ if (outsideEyeEnvelope.has(mesh))
2715
+ continue;
2757
2716
  if (mesh.bindGroup) {
2758
2717
  pass.setBindGroup(0, mesh.bindGroup);
2759
2718
  }
@@ -2769,6 +2728,8 @@ export class Renderer {
2769
2728
  if (transparentMeshes.length > 0) {
2770
2729
  pass.setPipeline(this.pipeline.getTransparentPipeline());
2771
2730
  for (const mesh of transparentMeshes) {
2731
+ if (outsideEyeEnvelope.has(mesh))
2732
+ continue;
2772
2733
  if (mesh.bindGroup) {
2773
2734
  pass.setBindGroup(0, mesh.bindGroup);
2774
2735
  }
@@ -2813,8 +2774,25 @@ export class Renderer {
2813
2774
  rteCamera: relativeToEyeFrame.getCameraWorld(),
2814
2775
  });
2815
2776
  pass.end();
2777
+ // Selection/hover outline input (#5390): built from the meshes the
2778
+ // highlight-draw loop above already prepared this frame.
2779
+ // A hidden / isolated-out entity is never outlined; the lookup is scoped to the hovered entity's own model.
2780
+ const hovered = options.hoverOutline;
2781
+ const hoverId = hovered && !options.hiddenIds?.has(hovered.id)
2782
+ && (!hasIsolatedFilter || options.isolatedIds.has(hovered.id)) ? hovered.id : null;
2783
+ const hoverModel = hovered?.modelIndex;
2784
+ const hoverPieces = hoverId != null && !selectedMeshesForMask.some((m) => matchesHoveredMesh(m, hoverId, hoverModel))
2785
+ ? this.hoverMeshes.resolve(device, hoverId, hoverModel, () => this.scene.getMeshDataPieces(hoverId, hoverModel), (m) => this.individualMeshFrame(m))
2786
+ : (this.hoverMeshes.release(), []);
2787
+ const selectionOutline = buildSelectionOutlineFrame({
2788
+ uniformBufferSize: this.pipeline.getUniformBufferSize(), viewProj, relativeToEyeFrame,
2789
+ section: sectionPlaneData, sectionFlipped: options.sectionPlane?.flipped, clipBox: options.clipBox,
2790
+ selectedMeshes: selectedMeshesForMask, hoverPieces, hoveredId: hoverId, selectedModelIndex: hoverModel,
2791
+ instancedTemplates: this.scene.getInstancedTemplates(),
2792
+ instancedHovered: hoverId != null ? this.scene.getInstancedTemplatesOf(hoverId, hoverModel) : [],
2793
+ });
2816
2794
  // Created lazily like the sky/shadow passes; each pass inside is too.
2817
- this.postPasses ??= new PostPassChain(this.device, this.pipeline.getSampleCount());
2795
+ this.postPasses ??= new PostPassChain(this.device, this.pipeline.getSampleCount(), this.pipeline.getBindGroupLayout());
2818
2796
  this.postPasses.encode({
2819
2797
  encoder,
2820
2798
  targetView: textureView,
@@ -2829,6 +2807,7 @@ export class Renderer {
2829
2807
  pixelRatio: this.pixelRatio,
2830
2808
  // EDL only when point clouds are loaded and the user enabled it.
2831
2809
  edl: this.edlOptions.enabled && this.pointCloudRenderer?.hasAssets() ? this.edlOptions : null,
2810
+ selectionOutline,
2832
2811
  });
2833
2812
  colorReadback = colorCapture && encodeRendererColorFrameCapture(device, encoder, colorCapture);
2834
2813
  device.queue.submit([encoder.finish()]);
@@ -2973,7 +2952,11 @@ export class Renderer {
2973
2952
  * These are scaled internally to match the actual canvas pixel dimensions.
2974
2953
  */
2975
2954
  raycastSceneMagnetic(x, y, currentEdgeLock, options) {
2976
- return this.raycastEngine.raycastSceneMagnetic(x, y, currentEdgeLock, options);
2955
+ return this.raycastEngine.raycastSceneMagnetic(x, y, currentEdgeLock, options, this.activePickClip());
2956
+ }
2957
+ /** Opt in the currently selected authored curves for exact magnetic picking. */
2958
+ setSourceSnapCurves(curves) {
2959
+ this.raycastEngine.setSourceSnapCurves(curves);
2977
2960
  }
2978
2961
  /**
2979
2962
  * Invalidate BVH cache (call when geometry changes)
@@ -3076,13 +3059,31 @@ export class Renderer {
3076
3059
  this.overlays.clearSection2DOverlay();
3077
3060
  }
3078
3061
  /**
3079
- * Set the colour of the overlay lines (annotation / alignment / grid) and the
3080
- * section-cut outline (RGBA, 0..1). Defaults to opaque black; theme it to keep
3081
- * lines legible on a dark canvas. The matching label colour is per-text via
3062
+ * Set the one colour vocabulary every overlay draws in (#5484): the selection
3063
+ * highlight, the section-plane preview (one accent for every axis), every
3064
+ * overlay line channel (section-cut outline, IfcAnnotation, alignment,
3065
+ * IfcGrid, DXF / LandXML) and the clash pair / overlap tints. Call this on
3066
+ * theme change and once after `init()`; the underlying GPU uniforms are
3067
+ * written only here, never per frame. Supersedes `setOverlayLineColor`
3068
+ * (deleted — the app never called it with a themed colour). See
3069
+ * `OverlayTheme` for which fields want linear-light RGBA and which want
3070
+ * sRGB-direct RGBA. The matching label colour is per-text via
3082
3071
  * `SymbolicTextInput.color` on `uploadAnnotationTexts3D`.
3083
3072
  */
3084
- setOverlayLineColor(color) {
3085
- this.overlays.setOverlayLineColor(color);
3073
+ setOverlayTheme(theme) {
3074
+ const previous = this.overlayTheme;
3075
+ this.overlayTheme = theme;
3076
+ this.pipeline?.selectionColorUniform.update(theme.selection);
3077
+ this.overlays.setTheme(theme);
3078
+ // Clash-pair tints already uploaded to the entity colour table must
3079
+ // follow a theme change as the other clash overlays do (#5490).
3080
+ const installed = this.scene.getColorOverrides();
3081
+ const repainted = installed && remapOverrideColors(installed, [[previous.clashA, theme.clashA], [previous.clashB, theme.clashB]]);
3082
+ const device = this.getGPUDevice();
3083
+ if (repainted && device && this.pipeline) {
3084
+ this.scene.setColorOverrides(repainted, device, this.pipeline);
3085
+ this.requestRender();
3086
+ }
3086
3087
  }
3087
3088
  /**
3088
3089
  * Set one standalone 3D line overlay, or clear it by passing `null`.
@@ -3095,7 +3096,7 @@ export class Renderer {
3095
3096
  *
3096
3097
  * Every channel is an independent buffer with its own visibility, so
3097
3098
  * setting one leaves the other three untouched. All four share the colour
3098
- * set by {@link setOverlayLineColor}; label colour is per-text via
3099
+ * set by `Renderer.setOverlayTheme`'s `overlayLine` field; label colour is per-text via
3099
3100
  * `SymbolicTextInput.color` on `uploadAnnotationTexts3D`.
3100
3101
  *
3101
3102
  * The channels differ in exactly one way — whether they grow the scene
@@ -3115,8 +3116,10 @@ export class Renderer {
3115
3116
  /**
3116
3117
  * Show (or clear) the clash-overlap box: the wireframe AABB of a focused
3117
3118
  * clash, drawn in `color` so the overlap region reads as a distinct third
3118
- * colour next to the two glowing clash elements (#1277). Pass `null` to
3119
- * clear. `min`/`max` are world-space corners (clash works in world frame).
3119
+ * colour next to the two glowing clash elements (#1277). Omit `color` to
3120
+ * draw it in the overlay theme's `clashOverlap` and keep following it
3121
+ * across `setOverlayTheme` calls (#5490). Pass `null` to clear.
3122
+ * `min`/`max` are world-space corners (clash works in world frame).
3120
3123
  */
3121
3124
  setClashOverlapBox(box) {
3122
3125
  this.overlays.setClashOverlapBox(box);
@@ -3125,8 +3128,10 @@ export class Renderer {
3125
3128
  * Draw the focused clash's CONTACT geometry as 3D line segments — the real
3126
3129
  * shared-face polygon outlines / intersection lines, not the AABB box.
3127
3130
  * `vertices` is a flat line-list (x,y,z per endpoint, 2 endpoints per
3128
- * segment) in world frame. Pass `null` to clear. Shares the clash-box line
3129
- * buffer, so only one of this / setClashOverlapBox is shown at a time.
3131
+ * segment) in world frame. Omit `color` to follow the overlay theme's
3132
+ * `clashOverlap`, as for `setClashOverlapBox`. Pass `null` to clear. Shares
3133
+ * the clash-box line buffer, so only one of this / setClashOverlapBox is
3134
+ * shown at a time.
3130
3135
  */
3131
3136
  setClashContactLines(lines) {
3132
3137
  this.overlays.setClashContactLines(lines);
@@ -3138,7 +3143,8 @@ export class Renderer {
3138
3143
  * BIMcollab Zoom / Solibri presentation). Pass `null` to clear. Independent
3139
3144
  * of `setClashOverlapBox` / `setClashContactLines`: the caller decides
3140
3145
  * which one is current for a given clash (solid when the kernel resolved
3141
- * one, box/lines as the fallback when it didn't).
3146
+ * one, box/lines as the fallback when it didn't). Omit `color` to follow
3147
+ * the overlay theme's `clashOverlap`, as for `setClashOverlapBox`.
3142
3148
  */
3143
3149
  setClashIntersectionSolid(solid) {
3144
3150
  this.overlays.setClashIntersectionSolid(solid);
@@ -3292,6 +3298,7 @@ export class Renderer {
3292
3298
  this.pickingManager.setPicker(null);
3293
3299
  this.postPasses?.destroy();
3294
3300
  this.postPasses = null;
3301
+ this.hoverMeshes.release();
3295
3302
  this.skyPass?.destroy();
3296
3303
  this.skyPass = null;
3297
3304
  this.shadowPass?.destroy();