@ifc-lite/renderer 5.0.0 → 6.0.1

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 (284) 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-precision.d.ts +1 -1
  177. package/dist/scene-precision.d.ts.map +1 -1
  178. package/dist/scene-precision.js +11 -2
  179. package/dist/scene-precision.js.map +1 -1
  180. package/dist/scene.d.ts +43 -38
  181. package/dist/scene.d.ts.map +1 -1
  182. package/dist/scene.js +121 -129
  183. package/dist/scene.js.map +1 -1
  184. package/dist/section-2d-line-buffer.d.ts +2 -2
  185. package/dist/section-2d-line-buffer.d.ts.map +1 -1
  186. package/dist/section-2d-line-buffer.js +10 -11
  187. package/dist/section-2d-line-buffer.js.map +1 -1
  188. package/dist/section-2d-overlay.d.ts +6 -4
  189. package/dist/section-2d-overlay.d.ts.map +1 -1
  190. package/dist/section-2d-overlay.js +45 -21
  191. package/dist/section-2d-overlay.js.map +1 -1
  192. package/dist/section-2d-types.d.ts +1 -1
  193. package/dist/section-2d-types.d.ts.map +1 -1
  194. package/dist/section-2d-types.js +1 -1
  195. package/dist/section-2d-types.js.map +1 -1
  196. package/dist/section-plane.d.ts.map +1 -1
  197. package/dist/section-plane.js +3 -1
  198. package/dist/section-plane.js.map +1 -1
  199. package/dist/selection-mask-pass.d.ts +85 -0
  200. package/dist/selection-mask-pass.d.ts.map +1 -0
  201. package/dist/selection-mask-pass.js +170 -0
  202. package/dist/selection-mask-pass.js.map +1 -0
  203. package/dist/selection-mask-pipelines.d.ts +57 -0
  204. package/dist/selection-mask-pipelines.d.ts.map +1 -0
  205. package/dist/selection-mask-pipelines.js +150 -0
  206. package/dist/selection-mask-pipelines.js.map +1 -0
  207. package/dist/selection-outline-frame.d.ts +47 -0
  208. package/dist/selection-outline-frame.d.ts.map +1 -0
  209. package/dist/selection-outline-frame.js +122 -0
  210. package/dist/selection-outline-frame.js.map +1 -0
  211. package/dist/shaders/depth-reconstruct.wgsl.d.ts +7 -2
  212. package/dist/shaders/depth-reconstruct.wgsl.d.ts.map +1 -1
  213. package/dist/shaders/depth-reconstruct.wgsl.js +8 -3
  214. package/dist/shaders/depth-reconstruct.wgsl.js.map +1 -1
  215. package/dist/shaders/edges.wgsl.d.ts +18 -0
  216. package/dist/shaders/edges.wgsl.d.ts.map +1 -1
  217. package/dist/shaders/edges.wgsl.js +99 -0
  218. package/dist/shaders/edges.wgsl.js.map +1 -1
  219. package/dist/shaders/entity-color-table.wgsl.d.ts +2 -0
  220. package/dist/shaders/entity-color-table.wgsl.d.ts.map +1 -0
  221. package/dist/shaders/entity-color-table.wgsl.js +85 -0
  222. package/dist/shaders/entity-color-table.wgsl.js.map +1 -0
  223. package/dist/shaders/main.wgsl.d.ts +1 -1
  224. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  225. package/dist/shaders/main.wgsl.js +45 -113
  226. package/dist/shaders/main.wgsl.js.map +1 -1
  227. package/dist/shaders/mesh-uniforms.wgsl.d.ts +2 -0
  228. package/dist/shaders/mesh-uniforms.wgsl.d.ts.map +1 -0
  229. package/dist/shaders/mesh-uniforms.wgsl.js +53 -0
  230. package/dist/shaders/mesh-uniforms.wgsl.js.map +1 -0
  231. package/dist/shaders/section-2d-overlay.wgsl.d.ts +1 -0
  232. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
  233. package/dist/shaders/section-2d-overlay.wgsl.js +3 -2
  234. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
  235. package/dist/shaders/selection-mask.wgsl.d.ts +16 -0
  236. package/dist/shaders/selection-mask.wgsl.d.ts.map +1 -0
  237. package/dist/shaders/selection-mask.wgsl.js +138 -0
  238. package/dist/shaders/selection-mask.wgsl.js.map +1 -0
  239. package/dist/shaders/shadow.wgsl.d.ts +1 -1
  240. package/dist/shaders/shadow.wgsl.d.ts.map +1 -1
  241. package/dist/shaders/shadow.wgsl.js +3 -3
  242. package/dist/shadow-occluders.d.ts.map +1 -1
  243. package/dist/shadow-occluders.js +1 -0
  244. package/dist/shadow-occluders.js.map +1 -1
  245. package/dist/shadow-pass.d.ts +0 -1
  246. package/dist/shadow-pass.d.ts.map +1 -1
  247. package/dist/shadow-pass.js +26 -32
  248. package/dist/shadow-pass.js.map +1 -1
  249. package/dist/shadow-types.d.ts +3 -0
  250. package/dist/shadow-types.d.ts.map +1 -1
  251. package/dist/shadow-types.js.map +1 -1
  252. package/dist/snap-detector.d.ts +5 -0
  253. package/dist/snap-detector.d.ts.map +1 -1
  254. package/dist/snap-detector.js.map +1 -1
  255. package/dist/source-curve-clipping.d.ts +7 -0
  256. package/dist/source-curve-clipping.d.ts.map +1 -0
  257. package/dist/source-curve-clipping.js +165 -0
  258. package/dist/source-curve-clipping.js.map +1 -0
  259. package/dist/source-curve-snap.d.ts +49 -0
  260. package/dist/source-curve-snap.d.ts.map +1 -0
  261. package/dist/source-curve-snap.js +197 -0
  262. package/dist/source-curve-snap.js.map +1 -0
  263. package/dist/symbolic-overlay-pipelines.d.ts +1 -1
  264. package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
  265. package/dist/symbolic-overlay-pipelines.js +13 -22
  266. package/dist/symbolic-overlay-pipelines.js.map +1 -1
  267. package/dist/symbolic-text-rte.d.ts +16 -0
  268. package/dist/symbolic-text-rte.d.ts.map +1 -0
  269. package/dist/symbolic-text-rte.js +37 -0
  270. package/dist/symbolic-text-rte.js.map +1 -0
  271. package/dist/textured-vertices.d.ts.map +1 -1
  272. package/dist/textured-vertices.js +2 -3
  273. package/dist/textured-vertices.js.map +1 -1
  274. package/dist/types.d.ts +10 -0
  275. package/dist/types.d.ts.map +1 -1
  276. package/dist/visual-enhancement.d.ts +0 -4
  277. package/dist/visual-enhancement.d.ts.map +1 -1
  278. package/dist/visual-enhancement.js +0 -5
  279. package/dist/visual-enhancement.js.map +1 -1
  280. package/dist/xray-alpha.d.ts +7 -3
  281. package/dist/xray-alpha.d.ts.map +1 -1
  282. package/dist/xray-alpha.js +11 -7
  283. package/dist/xray-alpha.js.map +1 -1
  284. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -78,9 +78,10 @@ import { WebGPUDevice } from './device.js';
78
78
  import { RenderPipeline } from './pipeline.js';
79
79
  import { Camera } from './camera.js';
80
80
  import { Scene } from './scene.js';
81
+ import { remapOverrideColors } from './scene-derived-batches.js';
81
82
  import { Picker } from './picker.js';
82
83
  import { reportableItemId } from './pick-resolve.js';
83
- import { MathUtils, viewBasis } from './math.js';
84
+ import { viewBasis } from './math.js';
84
85
  import { isRteAabbVisible, rteFrustum, sourceFrustumFromRte } from './rte-frustum.js';
85
86
  import { VisualEnhancementResolver, livePostEffects } from './visual-enhancement.js';
86
87
  import { packClipBox } from './clip-box.js';
@@ -92,6 +93,8 @@ import { PickingManager } from './picking-manager.js';
92
93
  import { RaycastEngine } from './raycast-engine.js';
93
94
  import { RenderDegradationMonitor } from './render-degradation.js';
94
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';
95
98
  import { InteractionEffectsGovernor } from './interaction-effects-governor.js';
96
99
  import { VisibilityEpochTracker } from './visibility-epoch.js';
97
100
  import { isEntityVisible } from './entity-visibility.js';
@@ -103,21 +106,21 @@ import { resolveEnvironment } from './environment.js';
103
106
  import { ShadowPass, resolveShadowMapResolution } from './shadow-pass.js';
104
107
  import { fitSunLightMatrix, cameraFrustumFocusCorners, resolveShadowNormalBiasMetres } from './shadow-light-matrix.js';
105
108
  import { collectShadowOccluders } from './shadow-occluders.js';
106
- 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';
107
111
  import { shadowOccluderBatches } from './shadow-occluder-batches.js';
108
112
  import { captureRendererScreenshot } from './renderer-screenshot.js';
109
113
  import { beginRendererColorFrameCapture, cancelRendererColorFrame, discardRendererColorFrameReadback, encodeRendererColorFrameCapture, requestRendererColorFrame, retryRendererColorFrame, settleRendererColorFrameCapture } from './renderer-color-readback.js';
110
114
  import { shouldRouteMeshTransparent, shouldRouteBatchTransparent, splitVisibleIdsByPromotion, DEFAULT_GHOST_ALPHA } from './overlay-routing.js';
115
+ import { batchEntityIdAnchor, batchHasColorOverride, packOverrideParams } from './entity-color-table.js';
111
116
  import { XRayAlpha, XRayEpochTracker } from './xray-alpha.js';
112
117
  import { PartialBatchRequests } from './partial-batch-requests.js';
113
- import { colorSaltByte, packEntityLane } from './scene-geometry.js';
118
+ import { uploadIndividualMesh } from './individual-mesh-upload.js';
114
119
  import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
115
120
  import { appendPointCloudChunk as appendPointCloudChunkImpl, beginPointCloudStream as beginPointCloudStreamImpl, endPointCloudStream as endPointCloudStreamImpl, removePointCloudAsset as removePointCloudAssetImpl, } from './pointcloud/point-cloud-stream-lifecycle.js';
116
121
  import { DeviationComputer } from './deviation/deviation-computer.js';
117
122
  import { runGuardedGpuUpload, isDeviceLossThrow } from './gpu-upload-guard.js';
118
123
  import { recoverRendererDevice, rendererDeviceLostError } from './device-recovery.js';
119
- const MAX_ENCODED_ENTITY_ID = 0xFFFFFF;
120
- let warnedEntityIdRange = false;
121
124
  /**
122
125
  * The reason `whenReady()` rejects when the renderer is destroyed.
123
126
  *
@@ -165,6 +168,8 @@ export class Renderer {
165
168
  // Ambient occlusion, separation lines and eye-dome lighting (post-pass-chain.ts),
166
169
  // created on the first rendered frame.
167
170
  postPasses = null;
171
+ /** GPU copies of the hovered entity for the hover outline (#5390); see hover-mesh-cache.ts. */
172
+ hoverMeshes = new HoverMeshCache();
168
173
  /** Device px per CSS px in the drawing buffer; CSS-px sizes scale by it (#5383). */
169
174
  pixelRatio = 1;
170
175
  interactionEffects = new InteractionEffectsGovernor();
@@ -372,6 +377,8 @@ export class Renderer {
372
377
  _xrayEpochs = new XRayEpochTracker();
373
378
  _visibilityVersion = 0;
374
379
  _partialBatchEpoch = 0;
380
+ _xrayAlpha = null;
381
+ _xrayAlphaEpoch = -1;
375
382
  _lastColorOverrideGen = -1;
376
383
  _xrayVersion = 0;
377
384
  _lastHadPartialSources = false;
@@ -400,6 +407,8 @@ export class Renderer {
400
407
  // One 336-byte buffer serves all frame batches/meshes (84 floats including RTE lanes).
401
408
  uniformScratch = new Float32Array(MESH_UNIFORM_FLOATS);
402
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);
403
412
  // What the last render() actually clipped, so the GPU picker can mirror it and
404
413
  // section/crop-clipped geometry stays unpickable, not just invisible. Updated
405
414
  // every render; read by pick()/pickRect(). null = nothing clipped that frame.
@@ -979,6 +988,15 @@ export class Renderer {
979
988
  requestRender: () => this.requestRender(),
980
989
  });
981
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
+ }
982
1000
  /**
983
1001
  * Toggle Eye-Dome Lighting and tune its strength.
984
1002
  *
@@ -1152,6 +1170,10 @@ export class Renderer {
1152
1170
  createMeshFromData(meshData) {
1153
1171
  return runGuardedGpuUpload(() => this.isDeviceLost(), () => this.createMeshFromDataUnguarded(meshData), (error) => this.reportUploadLoss(error));
1154
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
+ }
1155
1177
  /**
1156
1178
  * Create a GPU Mesh from MeshData (lazy creation for selection highlighting)
1157
1179
  * This is called on-demand when a mesh is selected, avoiding 2x buffer creation during streaming
@@ -1159,71 +1181,7 @@ export class Renderer {
1159
1181
  createMeshFromDataUnguarded(meshData) {
1160
1182
  if (!this.device.isInitialized())
1161
1183
  return;
1162
- const device = this.device.getDevice();
1163
- const vertexCount = meshData.positions.length / 3;
1164
- const interleavedRaw = new ArrayBuffer(vertexCount * 7 * 4);
1165
- const interleaved = new Float32Array(interleavedRaw);
1166
- const interleavedU32 = new Uint32Array(interleavedRaw);
1167
- // Build this individual mesh (selection highlight + GPU object-id picker)
1168
- // in the same small local frame as its source batch.
1169
- // CRITICAL: replicate the BATCH's exact two-step f32 path so the highlight
1170
- // is bit-coincident with its source surface (no z-fight, no depth bias):
1171
- // batch stores s = f32(local + (origin - sharedOrigin)) [merge]
1172
- // batch shader RTE = sharedOrigin - eye + s [draw]
1173
- // We retain `s` and the canonical origin separately. When there is no
1174
- // shared origin, retain the piece origin instead of folding it in.
1175
- const o = meshData.origin;
1176
- const so = this.scene.getSharedFrameOrigin(meshData.modelIndex, meshData);
1177
- const ox = o ? o[0] : 0, oy = o ? o[1] : 0, oz = o ? o[2] : 0;
1178
- const fr = Math.fround;
1179
- const dx = so ? (ox - so[0]) : ox, dy = so ? (oy - so[1]) : oy, dz = so ? (oz - so[2]) : oz;
1180
- // Quantized batches (issue #1682 phase 6) render lattice-snapped
1181
- // positions: the shader's quantMin + q*step is exactly the lattice
1182
- // node nearest the batch's stored f32 rel coordinate. Reproduce it by
1183
- // snapping the SAME rel coordinate here (round(s*1024)/1024 in f64
1184
- // yields the identical exact-f32 lattice value — see quantize.ts), so
1185
- // the highlight/picker mesh stays BIT-coincident with its quantized
1186
- // source surface, exactly as the two-step fold above achieves for the
1187
- // f32 path. Meshes whose batch fell back to f32 must not snap.
1188
- const snap = this.scene.isMeshQuantized(meshData)
1189
- ? (v) => Math.round(v * 1024) / 1024
1190
- : (v) => v;
1191
- const p = meshData.positions;
1192
- for (let i = 0; i < vertexCount; i++) {
1193
- const base = i * 7;
1194
- const posBase = i * 3;
1195
- interleaved[base] = so ? snap(fr(p[posBase] + dx)) : snap(p[posBase]);
1196
- interleaved[base + 1] = so ? snap(fr(p[posBase + 1] + dy)) : snap(p[posBase + 1]);
1197
- interleaved[base + 2] = so ? snap(fr(p[posBase + 2] + dz)) : snap(p[posBase + 2]);
1198
- const hasNormals = meshData.normals.length > 0;
1199
- interleaved[base + 3] = hasNormals ? meshData.normals[posBase] : 0;
1200
- interleaved[base + 4] = hasNormals ? meshData.normals[posBase + 1] : 0;
1201
- interleaved[base + 5] = hasNormals ? meshData.normals[posBase + 2] : 0;
1202
- let encodedId = meshData.expressId >>> 0;
1203
- if (encodedId > MAX_ENCODED_ENTITY_ID) {
1204
- if (!warnedEntityIdRange) {
1205
- warnedEntityIdRange = true;
1206
- console.warn('[Renderer] expressId exceeds 24-bit seam-ID encoding range; seam lines may collide.');
1207
- }
1208
- encodedId = encodedId & MAX_ENCODED_ENTITY_ID;
1209
- }
1210
- // Stamp the SAME high-byte material-colour salt as the batch path
1211
- // (mergeGeometry) so this individual/selection mesh computes the
1212
- // identical depth nudge as its source batch — otherwise the highlight
1213
- // (selection pipeline, reverse-Z 'greater-equal') would z-fight or drop
1214
- // out against the salted base depth. Low 24 bits stay the picking id.
1215
- interleavedU32[base + 6] = packEntityLane(encodedId, colorSaltByte(meshData.color));
1216
- }
1217
- const vertexBuffer = device.createBuffer({
1218
- size: interleaved.byteLength,
1219
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1220
- });
1221
- device.queue.writeBuffer(vertexBuffer, 0, interleaved);
1222
- const indexBuffer = device.createBuffer({
1223
- size: meshData.indices.byteLength,
1224
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
1225
- });
1226
- device.queue.writeBuffer(indexBuffer, 0, meshData.indices);
1184
+ const { vertexBuffer, indexBuffer, indexCount, transform, rteOrigin } = uploadIndividualMesh(this.device.getDevice(), meshData, this.individualMeshFrame(meshData));
1227
1185
  // Keep the hydrated mesh in its decoded local frame. Folding this
1228
1186
  // origin back into f32 vertices used to make selection/highlight the
1229
1187
  // only mesh path that lost centimetres at national-grid coordinates.
@@ -1240,16 +1198,11 @@ export class Renderer {
1240
1198
  geometryItemId: reportableItemId(meshData, meshData.expressId),
1241
1199
  vertexBuffer,
1242
1200
  indexBuffer,
1243
- indexCount: meshData.indices.length,
1244
- transform: (() => {
1245
- const transform = MathUtils.identity();
1246
- transform.m[12] = so ? so[0] : ox;
1247
- transform.m[13] = so ? so[1] : oy;
1248
- transform.m[14] = so ? so[2] : oz;
1249
- return transform;
1250
- })(),
1251
- rteOrigin: so ? [so[0], so[1], so[2]] : [ox, oy, oz],
1201
+ indexCount,
1202
+ transform,
1203
+ rteOrigin,
1252
1204
  color: meshData.color,
1205
+ ...(meshData.material ? { finish: meshData.material } : {}), // #5582
1253
1206
  hydrated: true,
1254
1207
  });
1255
1208
  }
@@ -1519,14 +1472,6 @@ export class Renderer {
1519
1472
  // governor's verdict so early navigation can't degrade a session.
1520
1473
  const timingUnstable = options.isStreaming === true || this.scene.hasQueuedMeshes();
1521
1474
  const effectsLive = this.interactionEffects.frame(interacting, performance.now(), timingUnstable, options.interactionFrameIntervalMs ?? 0);
1522
- // Edge contrast is NOT interaction-gated: its per-fragment work runs
1523
- // unconditionally in the shader and the gated tail is a handful of
1524
- // ALU ops, so disabling it bought nothing and only made the crease
1525
- // darkening pop off/on around gestures (visible in ortho).
1526
- const edgeEnabled = visualEnhancement.enabled && visualEnhancement.edgeContrast.enabled;
1527
- const edgeIntensity = Math.min(3.0, Math.max(0.0, visualEnhancement.edgeContrast.intensity));
1528
- const edgeEnabledU32 = edgeEnabled ? 1 : 0;
1529
- const edgeIntensityMilliU32 = Math.round(edgeIntensity * 1000);
1530
1475
  // Only the edge pass reads the object-id attachment after the pass.
1531
1476
  const needsObjectIdPass = livePostEffects(visualEnhancement, effectsLive).edges;
1532
1477
  // Check if visibility filtering is active
@@ -1603,10 +1548,6 @@ export class Renderer {
1603
1548
  // it carries a per-instance hidden flag the shader discards on). Diffed → a
1604
1549
  // no-op when visibility is unchanged.
1605
1550
  this.scene.setInstancedVisibility(options.hiddenIds, options.isolatedIds);
1606
- // Per-frame alpha overrides for X-Ray mode. See RenderOptions.transparencyOverrides.
1607
- // XRayAlpha snapshots the caller's map so mid-frame mutation can't desync
1608
- // classification and uniform-write decisions for the same batch/mesh, and
1609
- // owns the per-entity resolution + the mixed-batch partition (#4129).
1610
1551
  // X-Ray *context* mode (`ghostExceptIds`) feeds the same machinery, so it
1611
1552
  // routes through the transparent pipeline with no extra call sites — and
1612
1553
  // avoids building a Map over every element just to fade "the rest".
@@ -1616,16 +1557,19 @@ export class Renderer {
1616
1557
  // the user asked to fade the building and got a solid facade standing
1617
1558
  // in front of a ghosted interior.
1618
1559
  this.scene.setInstancedGhosting(options.ghostExceptIds ?? null, selectedExpressIds, ghostAlpha);
1619
- 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;
1620
1565
  const alphaForMesh = (expressId, fallback) => xray.forEntity(expressId, fallback);
1621
1566
  const alphaForBatch = (batch, fallback) => xray.forBatch(batch, fallback);
1622
1567
  // Lens / Pset color overrides: when an entity has an override, force
1623
- // its base draw through the opaque pipeline so it writes depth. The
1624
- // overlay paint pass uses depthCompare 'equal' and otherwise silently
1625
- // drops fragments belonging to entities whose default pipeline is
1626
- // transparent (IfcSpace, IfcOpeningElement, glass, …). See issue #677.
1627
- // Pure routing decision lives in overlay-routing.ts and is unit-tested
1628
- // 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.
1629
1573
  const colorOverrides = this.scene.getColorOverrides();
1630
1574
  // PERFORMANCE FIX: Use batch-level visibility filtering instead of creating individual meshes
1631
1575
  // Only create individual meshes for selected elements (for highlighting)
@@ -1753,6 +1697,11 @@ export class Renderer {
1753
1697
  max: [...options.clipBox.max],
1754
1698
  }
1755
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();
1756
1705
  // Reuse pooled scratch buffer for per-mesh uniform writes
1757
1706
  const meshBuf = this.uniformScratch;
1758
1707
  const meshFlags = this.uniformScratchU32;
@@ -1773,7 +1722,7 @@ export class Renderer {
1773
1722
  meshBuf[34] = mesh.color[2];
1774
1723
  // Selected meshes always keep their own alpha so highlights stay opaque
1775
1724
  meshBuf[35] = isSelected ? mesh.color[3] : alphaForMesh(mesh.expressId, mesh.color[3]);
1776
- packMeshMaterial(meshBuf, mesh.color[3], mesh.material);
1725
+ packMeshMaterial(meshBuf, mesh.color[3], mesh.finish ?? mesh.material);
1777
1726
  // Section plane data (offset 40-43)
1778
1727
  if (sectionPlaneData) {
1779
1728
  meshBuf[40] = sectionPlaneData.normal[0];
@@ -1796,14 +1745,17 @@ export class Renderer {
1796
1745
  (sectionPlaneData?.enabled ? 1 : 0) |
1797
1746
  (options.sectionPlane?.flipped ? 2 : 0) |
1798
1747
  clipBit;
1799
- meshFlags[2] = edgeEnabledU32;
1800
- meshFlags[3] = edgeIntensityMilliU32;
1748
+ meshFlags[2] = 0; // unused since #5746 (was the derivative edge darkening)
1749
+ meshFlags[3] = 0;
1801
1750
  // Individual meshes retain their origin and share the RTE
1802
1751
  // fragment contract with colour batches.
1803
1752
  packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, meshBuf);
1804
1753
  const meshOrigin = mesh.rteOrigin
1805
1754
  ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
1806
- 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
+ }
1807
1759
  meshFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
1808
1760
  device.queue.writeBuffer(mesh.uniformBuffer, 0, meshBuf);
1809
1761
  }
@@ -2041,7 +1993,13 @@ export class Renderer {
2041
1993
  // only presets that enable the sky — blanked the model on those
2042
1994
  // drivers. Binding while the main pipeline is current keeps it
2043
1995
  // valid for every main-family draw that follows.
1996
+ this.pipeline.setEntityColorTableBuffer(this.scene.getEntityColorTable().getBuffer());
2044
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 = [];
2045
2003
  // Check if we have batched meshes (preferred for performance)
2046
2004
  const allBatchedMeshes = this.scene.getBatchedMeshes();
2047
2005
  // PERFORMANCE FIX: Always use batch rendering when we have batches
@@ -2271,22 +2229,25 @@ export class Renderer {
2271
2229
  // x = isSelected (0/1)
2272
2230
  // y = section/clip bitfield:
2273
2231
  // bit 0 = sectionEnabled, bit 1 = flipped, bit 2 = clipBoxEnabled
2274
- // z = edgeEnabled (0/1)
2275
- // 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.
2276
2235
  tplFlags[0] = 0;
2277
2236
  tplFlags[1] =
2278
2237
  (sectionPlaneData?.enabled ? 1 : 0) |
2279
2238
  (options.sectionPlane?.flipped ? 2 : 0) |
2280
2239
  tplClipBit;
2281
- tplFlags[2] = edgeEnabledU32;
2282
- tplFlags[3] = edgeIntensityMilliU32;
2240
+ tplFlags[2] = 0;
2241
+ tplFlags[3] = 0;
2283
2242
  // Flat/quantized/textured batches enter WGSL in the single
2284
2243
  // camera-relative frame. Their fragment clip inputs must use
2285
2244
  // that same frame; mixing a local vertex with a 5,000 km f32
2286
2245
  // world plane loses centimetre cuts before the comparison.
2287
2246
  packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2288
- // Helper function to render a batch — patches color into the shared template
2289
- 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) => {
2290
2251
  if (!batch.bindGroup || !batch.uniformBuffer)
2291
2252
  return;
2292
2253
  // Patch the per-batch colour, and the material its AUTHORED
@@ -2295,7 +2256,7 @@ export class Renderer {
2295
2256
  tpl[33] = batch.color[1];
2296
2257
  tpl[34] = batch.color[2];
2297
2258
  tpl[35] = alphaForBatch(batch, batch.color[3]);
2298
- packMeshMaterial(tpl, batch.color[3]);
2259
+ packMeshMaterial(tpl, batch.color[3], batch.finish);
2299
2260
  // Per-batch local frame: the batch's vertices are stored
2300
2261
  // RELATIVE to batch.origin (f32-small), so set the model
2301
2262
  // matrix translation column to origin (world = origin + pos).
@@ -2309,7 +2270,8 @@ export class Renderer {
2309
2270
  // The regular model matrix remains for absolute-space
2310
2271
  // fragment work (section/shadow); vertex projection uses
2311
2272
  // this f64-subtracted high/low origin instead.
2312
- 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;
2313
2275
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2314
2276
  // Quantized dequantization params (issue #1682 phase 6);
2315
2277
  // zeroed for f32 batches (their pipelines ignore them).
@@ -2318,7 +2280,10 @@ export class Renderer {
2318
2280
  tpl[57] = qz ? qz.min[1] : 0;
2319
2281
  tpl[58] = qz ? qz.min[2] : 0;
2320
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);
2321
2285
  device.queue.writeBuffer(batch.uniformBuffer, 0, tpl);
2286
+ this.uniformOverrideU32.fill(0);
2322
2287
  // Single draw call for entire batch! LOD1-selected batches
2323
2288
  // bind their simplified index range over the same vertices.
2324
2289
  const useLod1 = batch.lod1IndexBuffer && batch.lod1IndexCount && lodBatches.has(batch.id);
@@ -2341,11 +2306,7 @@ export class Renderer {
2341
2306
  // probe (enableQuantizedBatches) verified they exist, so the
2342
2307
  // base fallback here is type-safety, never taken.
2343
2308
  const pipeFor = (batch, kind) => {
2344
- const base = kind === 'opaque'
2345
- ? this.pipeline.getPipeline()
2346
- : kind === 'transparent'
2347
- ? this.pipeline.getTransparentPipeline()
2348
- : this.pipeline.getOverlayPipeline();
2309
+ const base = kind === 'opaque' ? this.pipeline.getPipeline() : this.pipeline.getTransparentPipeline();
2349
2310
  if (!batch.quantized)
2350
2311
  return base;
2351
2312
  return this.pipeline.getQuantizedPipelineVariant(kind) ?? base;
@@ -2362,7 +2323,7 @@ export class Renderer {
2362
2323
  pass.setPipeline(this.pipeline.getPipeline());
2363
2324
  for (const batch of opaqueBatches) {
2364
2325
  pass.setPipeline(pipeFor(batch, 'opaque'));
2365
- renderBatch(batch);
2326
+ renderBatch(batch, true);
2366
2327
  }
2367
2328
  pass.setPipeline(this.pipeline.getPipeline());
2368
2329
  // GPU-instancing pass — repeated geometry collated by the producer
@@ -2407,8 +2368,11 @@ export class Renderer {
2407
2368
  }
2408
2369
  visibleInstanced = kept;
2409
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 = [];
2410
2374
  if (visibleInstanced.length > 0) {
2411
- uploadInstancedRteDeltas(device, visibleInstanced, relativeToEyeFrame.getCameraWorld());
2375
+ visibleInstancedRuns = uploadInstancedRteDeltas(device, visibleInstanced, relativeToEyeFrame.getCameraWorld());
2412
2376
  // Opaque instanced pass. flags.x bit 2 marks "instanced pass" so the
2413
2377
  // shader routes per-instance opacity: opaque (or selected) occurrences
2414
2378
  // draw here; translucent ones (lens/x-ray/compare overrides) are
@@ -2417,13 +2381,15 @@ export class Renderer {
2417
2381
  pass.setPipeline(this.pipeline.getInstancedPipeline());
2418
2382
  pass.setBindGroup(0, this.pipeline.getBindGroup());
2419
2383
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
2420
- for (const it of visibleInstanced) {
2384
+ for (const [i, it] of visibleInstanced.entries()) {
2421
2385
  pass.setVertexBuffer(0, it.vertexBuffer);
2422
2386
  pass.setVertexBuffer(1, it.instanceBuffer);
2387
+ pass.setVertexBuffer(INSTANCED_RTE_DELTA_SLOT, it.rteDeltas.buffer);
2423
2388
  pass.setIndexBuffer(it.indexBuffer, 'uint32');
2424
- pass.drawIndexed(it.indexCount, it.instanceCount);
2425
- frameDrawCalls++;
2426
- frameInstancedDrawn++;
2389
+ const runs = visibleInstancedRuns[i];
2390
+ frameDrawCalls += drawInstanceRuns(pass, it.indexCount, runs);
2391
+ if (runs.length > 0)
2392
+ frameInstancedDrawn++;
2427
2393
  }
2428
2394
  pass.setPipeline(this.pipeline.getPipeline());
2429
2395
  // The TRANSPARENT instanced sub-pass is drawn later, alongside the
@@ -2458,7 +2424,7 @@ export class Renderer {
2458
2424
  if (txAlpha <= 0.01)
2459
2425
  continue;
2460
2426
  // Lens / Pset colour override tints the sampled texel — the
2461
- // batch overlay paint pass doesn't iterate textured meshes,
2427
+ // entity colour table is off for textured draws (#6076),
2462
2428
  // so applying the override here is what recolours them.
2463
2429
  const txOverride = colorOverrides?.get(tm.expressId);
2464
2430
  // `world = origin + position`: the vertex buffer stores
@@ -2473,7 +2439,8 @@ export class Renderer {
2473
2439
  tpl[28] = tm.origin[0];
2474
2440
  tpl[29] = tm.origin[1];
2475
2441
  tpl[30] = tm.origin[2];
2476
- relativeToEyeFrame.packDrawableOrigin(tm.origin, tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2442
+ if (!relativeToEyeFrame.tryPackDrawableOrigin(tm.origin, tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2443
+ continue;
2477
2444
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2478
2445
  tpl[32] = txOverride ? txOverride[0] : tm.color[0];
2479
2446
  tpl[33] = txOverride ? txOverride[1] : tm.color[1];
@@ -2488,7 +2455,7 @@ export class Renderer {
2488
2455
  // never actually blends here — only the glass/roughness
2489
2456
  // CHOICE in the shader follows the same rule as everywhere
2490
2457
  // else, for the same reason `tm.material` does.
2491
- packMeshMaterial(tpl, tm.color[3], tm.material);
2458
+ packMeshMaterial(tpl, tm.color[3], tm.finish ?? tm.material);
2492
2459
  device.queue.writeBuffer(tm.uniformBuffer, 0, tpl);
2493
2460
  pass.setBindGroup(0, tm.bindGroup);
2494
2461
  pass.setVertexBuffer(0, tm.vertexBuffer);
@@ -2519,7 +2486,7 @@ export class Renderer {
2519
2486
  // Use opaque or transparent pipeline based on resolved alpha
2520
2487
  // (not the parent batch's color[3] — that ignores transparencyOverrides).
2521
2488
  // Promote to opaque if any expressId in the sub-batch carries a
2522
- // lens/Pset colour override, so the overlay paint pass finds depth.
2489
+ // lens/Pset colour override, so the colour table paints it (#6076).
2523
2490
  const isTransparent = shouldRouteBatchTransparent(alphaForBatch(subBatch, color[3]), subBatch.expressIds, colorOverrides);
2524
2491
  if (isTransparent) {
2525
2492
  transparentSubBatches.push(subBatch);
@@ -2533,7 +2500,7 @@ export class Renderer {
2533
2500
  pass.setPipeline(pipeFor(subBatch, 'opaque'));
2534
2501
  opaqueSubBatches.push(subBatch);
2535
2502
  // Render the sub-batch as a single draw call
2536
- renderBatch(subBatch);
2503
+ renderBatch(subBatch, true);
2537
2504
  }
2538
2505
  }
2539
2506
  for (const subBatch of transparentSubBatches) {
@@ -2543,28 +2510,6 @@ export class Renderer {
2543
2510
  // Reset to opaque pipeline for subsequent rendering
2544
2511
  pass.setPipeline(this.pipeline.getPipeline());
2545
2512
  }
2546
- // Render color overlay batches (lens coloring) on top of ALL opaque geometry.
2547
- // Placed AFTER partial batches so depth buffer is complete for both full
2548
- // and partial batches. Uses 'equal' depth compare — only paints where
2549
- // original geometry wrote depth, so hidden entities never leak through.
2550
- //
2551
- // flags.x bit 1 = overlay: tells the shader to preserve baseColor.a
2552
- // (the overlay pipeline now has src-alpha blending so low-alpha ghost
2553
- // tints composite correctly against the opaque pass) AND skip the
2554
- // specular term (glass reflections are meant for real glass and
2555
- // would whiten low-alpha colour overrides at grazing angles).
2556
- const overrideBatches = this.scene.getOverrideBatches();
2557
- if (overrideBatches.length > 0) {
2558
- pass.setPipeline(this.pipeline.getOverlayPipeline());
2559
- // bit 1 = overlay; bit 5 (32) = emphasize (pop) — see shader.
2560
- tplFlags[0] = options.emphasizeOverrides ? (2 | 32) : 2;
2561
- for (const batch of overrideBatches) {
2562
- pass.setPipeline(pipeFor(batch, 'overlay'));
2563
- renderBatch(batch);
2564
- }
2565
- tplFlags[0] = 0; // restore for any downstream use of the template
2566
- pass.setPipeline(this.pipeline.getPipeline());
2567
- }
2568
2513
  // Filled, hatched 3D cut surfaces are now rendered by
2569
2514
  // Section2DOverlayRenderer using the exact polygons from
2570
2515
  // SectionCutter (triangle-plane intersection). The old
@@ -2622,6 +2567,7 @@ export class Renderer {
2622
2567
  return true;
2623
2568
  })
2624
2569
  : [];
2570
+ selectedMeshesForMask = selectedMeshes;
2625
2571
  // Transparent instanced sub-pass — drawn here (after ALL opaque incl. the
2626
2572
  // textured sub-pass) so ghosted/x-rayed instanced occurrences blend over
2627
2573
  // a complete depth buffer. Only runs when an override actually made some
@@ -2635,12 +2581,12 @@ export class Renderer {
2635
2581
  pass.setPipeline(instancedTransparentPipeline);
2636
2582
  pass.setBindGroup(0, this.pipeline.getBindGroup());
2637
2583
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
2638
- for (const it of visibleInstanced) {
2584
+ for (const [i, it] of visibleInstanced.entries()) {
2639
2585
  pass.setVertexBuffer(0, it.vertexBuffer);
2640
2586
  pass.setVertexBuffer(1, it.instanceBuffer);
2587
+ pass.setVertexBuffer(INSTANCED_RTE_DELTA_SLOT, it.rteDeltas.buffer);
2641
2588
  pass.setIndexBuffer(it.indexBuffer, 'uint32');
2642
- pass.drawIndexed(it.indexCount, it.instanceCount);
2643
- frameDrawCalls++;
2589
+ frameDrawCalls += drawInstanceRuns(pass, it.indexCount, visibleInstancedRuns[i]);
2644
2590
  }
2645
2591
  pass.setPipeline(this.pipeline.getPipeline());
2646
2592
  }
@@ -2665,7 +2611,7 @@ export class Renderer {
2665
2611
  tpl[33] = mesh.color[1];
2666
2612
  tpl[34] = mesh.color[2];
2667
2613
  tpl[35] = alphaForMesh(mesh.expressId, mesh.color[3]);
2668
- packMeshMaterial(tpl, mesh.color[3], mesh.material);
2614
+ packMeshMaterial(tpl, mesh.color[3], mesh.finish ?? mesh.material);
2669
2615
  if (sectionPlaneData) {
2670
2616
  tpl[40] = sectionPlaneData.normal[0];
2671
2617
  tpl[41] = sectionPlaneData.normal[1];
@@ -2683,11 +2629,12 @@ export class Renderer {
2683
2629
  (sectionPlaneData?.enabled ? 1 : 0) |
2684
2630
  (options.sectionPlane?.flipped ? 2 : 0) |
2685
2631
  tplClipBit;
2686
- tplFlags[2] = edgeEnabledU32;
2687
- tplFlags[3] = edgeIntensityMilliU32;
2632
+ tplFlags[2] = 0;
2633
+ tplFlags[3] = 0;
2688
2634
  packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2689
2635
  const transparentOrigin = mesh.rteOrigin ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
2690
- relativeToEyeFrame.packDrawableOrigin(transparentOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2636
+ if (!relativeToEyeFrame.tryPackDrawableOrigin(transparentOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2637
+ continue;
2691
2638
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2692
2639
  device.queue.writeBuffer(mesh.uniformBuffer, 0, tpl);
2693
2640
  pass.setBindGroup(0, mesh.bindGroup);
@@ -2726,7 +2673,7 @@ export class Renderer {
2726
2673
  tpl[33] = mesh.color[1];
2727
2674
  tpl[34] = mesh.color[2];
2728
2675
  tpl[35] = mesh.color[3];
2729
- packMeshMaterial(tpl, mesh.color[3], mesh.material);
2676
+ packMeshMaterial(tpl, mesh.color[3], mesh.finish ?? mesh.material);
2730
2677
  if (sectionPlaneData) {
2731
2678
  tpl[40] = sectionPlaneData.normal[0];
2732
2679
  tpl[41] = sectionPlaneData.normal[1];
@@ -2744,11 +2691,12 @@ export class Renderer {
2744
2691
  (sectionPlaneData?.enabled ? 1 : 0) |
2745
2692
  (options.sectionPlane?.flipped ? 2 : 0) |
2746
2693
  tplClipBit;
2747
- tplFlags[2] = edgeEnabledU32;
2748
- tplFlags[3] = edgeIntensityMilliU32;
2694
+ tplFlags[2] = 0;
2695
+ tplFlags[3] = 0;
2749
2696
  packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2750
2697
  const selectedOrigin = mesh.rteOrigin ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
2751
- relativeToEyeFrame.packDrawableOrigin(selectedOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2698
+ if (!relativeToEyeFrame.tryPackDrawableOrigin(selectedOrigin, tpl, MESH_UNIFORM_OFFSET.drawableDelta))
2699
+ continue;
2752
2700
  tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2753
2701
  device.queue.writeBuffer(mesh.uniformBuffer, 0, tpl);
2754
2702
  pass.setPipeline(this.pipeline.getSelectionPipeline());
@@ -2763,6 +2711,8 @@ export class Renderer {
2763
2711
  // Fallback: render individual meshes (only when no batches exist)
2764
2712
  // Render opaque meshes with per-mesh bind groups
2765
2713
  for (const mesh of opaqueMeshes) {
2714
+ if (outsideEyeEnvelope.has(mesh))
2715
+ continue;
2766
2716
  if (mesh.bindGroup) {
2767
2717
  pass.setBindGroup(0, mesh.bindGroup);
2768
2718
  }
@@ -2778,6 +2728,8 @@ export class Renderer {
2778
2728
  if (transparentMeshes.length > 0) {
2779
2729
  pass.setPipeline(this.pipeline.getTransparentPipeline());
2780
2730
  for (const mesh of transparentMeshes) {
2731
+ if (outsideEyeEnvelope.has(mesh))
2732
+ continue;
2781
2733
  if (mesh.bindGroup) {
2782
2734
  pass.setBindGroup(0, mesh.bindGroup);
2783
2735
  }
@@ -2822,8 +2774,25 @@ export class Renderer {
2822
2774
  rteCamera: relativeToEyeFrame.getCameraWorld(),
2823
2775
  });
2824
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
+ });
2825
2794
  // Created lazily like the sky/shadow passes; each pass inside is too.
2826
- this.postPasses ??= new PostPassChain(this.device, this.pipeline.getSampleCount());
2795
+ this.postPasses ??= new PostPassChain(this.device, this.pipeline.getSampleCount(), this.pipeline.getBindGroupLayout());
2827
2796
  this.postPasses.encode({
2828
2797
  encoder,
2829
2798
  targetView: textureView,
@@ -2838,6 +2807,7 @@ export class Renderer {
2838
2807
  pixelRatio: this.pixelRatio,
2839
2808
  // EDL only when point clouds are loaded and the user enabled it.
2840
2809
  edl: this.edlOptions.enabled && this.pointCloudRenderer?.hasAssets() ? this.edlOptions : null,
2810
+ selectionOutline,
2841
2811
  });
2842
2812
  colorReadback = colorCapture && encodeRendererColorFrameCapture(device, encoder, colorCapture);
2843
2813
  device.queue.submit([encoder.finish()]);
@@ -2982,7 +2952,11 @@ export class Renderer {
2982
2952
  * These are scaled internally to match the actual canvas pixel dimensions.
2983
2953
  */
2984
2954
  raycastSceneMagnetic(x, y, currentEdgeLock, options) {
2985
- 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);
2986
2960
  }
2987
2961
  /**
2988
2962
  * Invalidate BVH cache (call when geometry changes)
@@ -3097,9 +3071,19 @@ export class Renderer {
3097
3071
  * `SymbolicTextInput.color` on `uploadAnnotationTexts3D`.
3098
3072
  */
3099
3073
  setOverlayTheme(theme) {
3074
+ const previous = this.overlayTheme;
3100
3075
  this.overlayTheme = theme;
3101
3076
  this.pipeline?.selectionColorUniform.update(theme.selection);
3102
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
+ }
3103
3087
  }
3104
3088
  /**
3105
3089
  * Set one standalone 3D line overlay, or clear it by passing `null`.
@@ -3314,6 +3298,7 @@ export class Renderer {
3314
3298
  this.pickingManager.setPicker(null);
3315
3299
  this.postPasses?.destroy();
3316
3300
  this.postPasses = null;
3301
+ this.hoverMeshes.release();
3317
3302
  this.skyPass?.destroy();
3318
3303
  this.skyPass = null;
3319
3304
  this.shadowPass?.destroy();