@ifc-lite/renderer 3.2.0 → 5.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 (399) hide show
  1. package/dist/aabb-edges.d.ts +10 -0
  2. package/dist/aabb-edges.d.ts.map +1 -1
  3. package/dist/aabb-edges.js +13 -0
  4. package/dist/aabb-edges.js.map +1 -1
  5. package/dist/ao-params.d.ts +85 -0
  6. package/dist/ao-params.d.ts.map +1 -0
  7. package/dist/ao-params.js +109 -0
  8. package/dist/ao-params.js.map +1 -0
  9. package/dist/ao-pass.d.ts +47 -0
  10. package/dist/ao-pass.d.ts.map +1 -0
  11. package/dist/ao-pass.js +157 -0
  12. package/dist/ao-pass.js.map +1 -0
  13. package/dist/camera-matrices.d.ts +3 -0
  14. package/dist/camera-matrices.d.ts.map +1 -1
  15. package/dist/camera-matrices.js +12 -1
  16. package/dist/camera-matrices.js.map +1 -1
  17. package/dist/camera-orientation.d.ts +62 -0
  18. package/dist/camera-orientation.d.ts.map +1 -0
  19. package/dist/camera-orientation.js +155 -0
  20. package/dist/camera-orientation.js.map +1 -0
  21. package/dist/camera-projection.d.ts +3 -1
  22. package/dist/camera-projection.d.ts.map +1 -1
  23. package/dist/camera-projection.js +26 -20
  24. package/dist/camera-projection.js.map +1 -1
  25. package/dist/camera-surface-zoom.d.ts +32 -0
  26. package/dist/camera-surface-zoom.d.ts.map +1 -0
  27. package/dist/camera-surface-zoom.js +37 -0
  28. package/dist/camera-surface-zoom.js.map +1 -0
  29. package/dist/camera.d.ts +32 -61
  30. package/dist/camera.d.ts.map +1 -1
  31. package/dist/camera.js +71 -167
  32. package/dist/camera.js.map +1 -1
  33. package/dist/clash-solid-pipeline.d.ts +9 -3
  34. package/dist/clash-solid-pipeline.d.ts.map +1 -1
  35. package/dist/clash-solid-pipeline.js +31 -8
  36. package/dist/clash-solid-pipeline.js.map +1 -1
  37. package/dist/constants.d.ts +1 -1
  38. package/dist/constants.d.ts.map +1 -1
  39. package/dist/constants.js +4 -3
  40. package/dist/constants.js.map +1 -1
  41. package/dist/depth-reconstruct.d.ts +43 -0
  42. package/dist/depth-reconstruct.d.ts.map +1 -0
  43. package/dist/depth-reconstruct.js +35 -0
  44. package/dist/depth-reconstruct.js.map +1 -0
  45. package/dist/device.js +1 -1
  46. package/dist/device.js.map +1 -1
  47. package/dist/edge-params.d.ts +43 -0
  48. package/dist/edge-params.d.ts.map +1 -0
  49. package/dist/edge-params.js +63 -0
  50. package/dist/edge-params.js.map +1 -0
  51. package/dist/edge-pass.d.ts +38 -0
  52. package/dist/edge-pass.d.ts.map +1 -0
  53. package/dist/edge-pass.js +111 -0
  54. package/dist/edge-pass.js.map +1 -0
  55. package/dist/environment.d.ts +26 -15
  56. package/dist/environment.d.ts.map +1 -1
  57. package/dist/environment.js +24 -13
  58. package/dist/environment.js.map +1 -1
  59. package/dist/federation-registry.d.ts +16 -0
  60. package/dist/federation-registry.d.ts.map +1 -1
  61. package/dist/federation-registry.js +80 -9
  62. package/dist/federation-registry.js.map +1 -1
  63. package/dist/gpu-static-upload.d.ts +39 -0
  64. package/dist/gpu-static-upload.d.ts.map +1 -0
  65. package/dist/gpu-static-upload.js +70 -0
  66. package/dist/gpu-static-upload.js.map +1 -0
  67. package/dist/gpu-upload-guard.d.ts +12 -23
  68. package/dist/gpu-upload-guard.d.ts.map +1 -1
  69. package/dist/gpu-upload-guard.js +12 -26
  70. package/dist/gpu-upload-guard.js.map +1 -1
  71. package/dist/index.d.ts +58 -35
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +255 -284
  74. package/dist/index.js.map +1 -1
  75. package/dist/instanced-render.d.ts +25 -5
  76. package/dist/instanced-render.d.ts.map +1 -1
  77. package/dist/instanced-render.js +50 -13
  78. package/dist/instanced-render.js.map +1 -1
  79. package/dist/instanced-rte.d.ts +17 -0
  80. package/dist/instanced-rte.d.ts.map +1 -0
  81. package/dist/instanced-rte.js +32 -0
  82. package/dist/instanced-rte.js.map +1 -0
  83. package/dist/mesh-material.d.ts +26 -0
  84. package/dist/mesh-material.d.ts.map +1 -0
  85. package/dist/mesh-material.js +52 -0
  86. package/dist/mesh-material.js.map +1 -0
  87. package/dist/mesh-rte-uniforms.d.ts +47 -0
  88. package/dist/mesh-rte-uniforms.d.ts.map +1 -0
  89. package/dist/mesh-rte-uniforms.js +60 -0
  90. package/dist/mesh-rte-uniforms.js.map +1 -0
  91. package/dist/model-bounds-tracker.d.ts +7 -0
  92. package/dist/model-bounds-tracker.d.ts.map +1 -1
  93. package/dist/model-bounds-tracker.js +16 -5
  94. package/dist/model-bounds-tracker.js.map +1 -1
  95. package/dist/model-translation.d.ts +1 -1
  96. package/dist/model-translation.d.ts.map +1 -1
  97. package/dist/model-translation.js +50 -2
  98. package/dist/model-translation.js.map +1 -1
  99. package/dist/overlay-theme-uniforms.d.ts +54 -0
  100. package/dist/overlay-theme-uniforms.d.ts.map +1 -0
  101. package/dist/overlay-theme-uniforms.js +89 -0
  102. package/dist/overlay-theme-uniforms.js.map +1 -0
  103. package/dist/overlay-theme.d.ts +73 -0
  104. package/dist/overlay-theme.d.ts.map +1 -0
  105. package/dist/overlay-theme.js +24 -0
  106. package/dist/overlay-theme.js.map +1 -0
  107. package/dist/pick-rte-snapshot.d.ts +7 -0
  108. package/dist/pick-rte-snapshot.d.ts.map +1 -0
  109. package/dist/pick-rte-snapshot.js +15 -0
  110. package/dist/pick-rte-snapshot.js.map +1 -0
  111. package/dist/pick-types.d.ts +17 -0
  112. package/dist/pick-types.d.ts.map +1 -0
  113. package/dist/pick-types.js +5 -0
  114. package/dist/pick-types.js.map +1 -0
  115. package/dist/pick-uniforms.d.ts +6 -0
  116. package/dist/pick-uniforms.d.ts.map +1 -1
  117. package/dist/pick-uniforms.js +22 -18
  118. package/dist/pick-uniforms.js.map +1 -1
  119. package/dist/pick-world-position.d.ts +18 -0
  120. package/dist/pick-world-position.d.ts.map +1 -0
  121. package/dist/pick-world-position.js +21 -0
  122. package/dist/pick-world-position.js.map +1 -0
  123. package/dist/picker-readbacks.d.ts +6 -0
  124. package/dist/picker-readbacks.d.ts.map +1 -0
  125. package/dist/picker-readbacks.js +20 -0
  126. package/dist/picker-readbacks.js.map +1 -0
  127. package/dist/picker-rte-uniforms.d.ts +6 -0
  128. package/dist/picker-rte-uniforms.d.ts.map +1 -0
  129. package/dist/picker-rte-uniforms.js +41 -0
  130. package/dist/picker-rte-uniforms.js.map +1 -0
  131. package/dist/picker.d.ts +6 -7
  132. package/dist/picker.d.ts.map +1 -1
  133. package/dist/picker.js +88 -115
  134. package/dist/picker.js.map +1 -1
  135. package/dist/picking-manager.d.ts +8 -0
  136. package/dist/picking-manager.d.ts.map +1 -1
  137. package/dist/picking-manager.js +46 -19
  138. package/dist/picking-manager.js.map +1 -1
  139. package/dist/pipeline.d.ts +3 -7
  140. package/dist/pipeline.d.ts.map +1 -1
  141. package/dist/pipeline.js +27 -31
  142. package/dist/pipeline.js.map +1 -1
  143. package/dist/point-picker.d.ts +5 -1
  144. package/dist/point-picker.d.ts.map +1 -1
  145. package/dist/point-picker.js +49 -9
  146. package/dist/point-picker.js.map +1 -1
  147. package/dist/pointcloud/point-cloud-handle-ids.d.ts +22 -0
  148. package/dist/pointcloud/point-cloud-handle-ids.d.ts.map +1 -0
  149. package/dist/pointcloud/point-cloud-handle-ids.js +31 -0
  150. package/dist/pointcloud/point-cloud-handle-ids.js.map +1 -0
  151. package/dist/pointcloud/point-cloud-node.d.ts +14 -6
  152. package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
  153. package/dist/pointcloud/point-cloud-node.js.map +1 -1
  154. package/dist/pointcloud/point-cloud-pick-sources.d.ts +47 -0
  155. package/dist/pointcloud/point-cloud-pick-sources.d.ts.map +1 -0
  156. package/dist/pointcloud/point-cloud-pick-sources.js +32 -0
  157. package/dist/pointcloud/point-cloud-pick-sources.js.map +1 -0
  158. package/dist/pointcloud/point-cloud-placement.d.ts.map +1 -1
  159. package/dist/pointcloud/point-cloud-placement.js +20 -5
  160. package/dist/pointcloud/point-cloud-placement.js.map +1 -1
  161. package/dist/pointcloud/point-cloud-ray-transform.d.ts +78 -4
  162. package/dist/pointcloud/point-cloud-ray-transform.d.ts.map +1 -1
  163. package/dist/pointcloud/point-cloud-ray-transform.js +31 -84
  164. package/dist/pointcloud/point-cloud-ray-transform.js.map +1 -1
  165. package/dist/pointcloud/point-cloud-renderer.d.ts +21 -19
  166. package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
  167. package/dist/pointcloud/point-cloud-renderer.js +46 -52
  168. package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
  169. package/dist/pointcloud/point-cloud-stream-lifecycle.d.ts +44 -0
  170. package/dist/pointcloud/point-cloud-stream-lifecycle.d.ts.map +1 -0
  171. package/dist/pointcloud/point-cloud-stream-lifecycle.js +44 -0
  172. package/dist/pointcloud/point-cloud-stream-lifecycle.js.map +1 -0
  173. package/dist/pointcloud/point-cloud-uniforms.d.ts +5 -0
  174. package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
  175. package/dist/pointcloud/point-cloud-uniforms.js +55 -44
  176. package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
  177. package/dist/pointcloud/point-cloud-visibility.d.ts +10 -0
  178. package/dist/pointcloud/point-cloud-visibility.d.ts.map +1 -0
  179. package/dist/pointcloud/point-cloud-visibility.js +18 -0
  180. package/dist/pointcloud/point-cloud-visibility.js.map +1 -0
  181. package/dist/pointcloud/point-pipeline.d.ts +4 -9
  182. package/dist/pointcloud/point-pipeline.d.ts.map +1 -1
  183. package/dist/pointcloud/point-pipeline.js +4 -9
  184. package/dist/pointcloud/point-pipeline.js.map +1 -1
  185. package/dist/pointcloud/point-shader.wgsl.d.ts +1 -21
  186. package/dist/pointcloud/point-shader.wgsl.d.ts.map +1 -1
  187. package/dist/pointcloud/point-shader.wgsl.js +25 -5
  188. package/dist/pointcloud/point-shader.wgsl.js.map +1 -1
  189. package/dist/post-pass-chain.d.ts +38 -0
  190. package/dist/post-pass-chain.d.ts.map +1 -0
  191. package/dist/post-pass-chain.js +105 -0
  192. package/dist/post-pass-chain.js.map +1 -0
  193. package/dist/raycast-engine.d.ts +7 -5
  194. package/dist/raycast-engine.d.ts.map +1 -1
  195. package/dist/raycast-engine.js +17 -20
  196. package/dist/raycast-engine.js.map +1 -1
  197. package/dist/raycast-point-cloud-query.d.ts +1 -1
  198. package/dist/raycast-point-cloud-query.d.ts.map +1 -1
  199. package/dist/reference-image-pipeline.d.ts +1 -1
  200. package/dist/reference-image-pipeline.d.ts.map +1 -1
  201. package/dist/reference-image-pipeline.js +25 -7
  202. package/dist/reference-image-pipeline.js.map +1 -1
  203. package/dist/reference-images.d.ts +1 -1
  204. package/dist/reference-images.d.ts.map +1 -1
  205. package/dist/reference-images.js +13 -5
  206. package/dist/reference-images.js.map +1 -1
  207. package/dist/relative-to-eye.d.ts +146 -0
  208. package/dist/relative-to-eye.d.ts.map +1 -0
  209. package/dist/relative-to-eye.js +326 -0
  210. package/dist/relative-to-eye.js.map +1 -0
  211. package/dist/render-section-draw.d.ts +11 -0
  212. package/dist/render-section-draw.d.ts.map +1 -1
  213. package/dist/render-section-draw.js +6 -3
  214. package/dist/render-section-draw.js.map +1 -1
  215. package/dist/renderer-color-readback.d.ts +50 -0
  216. package/dist/renderer-color-readback.d.ts.map +1 -0
  217. package/dist/renderer-color-readback.js +145 -0
  218. package/dist/renderer-color-readback.js.map +1 -0
  219. package/dist/renderer-overlays.d.ts +20 -10
  220. package/dist/renderer-overlays.d.ts.map +1 -1
  221. package/dist/renderer-overlays.js +44 -34
  222. package/dist/renderer-overlays.js.map +1 -1
  223. package/dist/renderer-screenshot.d.ts +4 -0
  224. package/dist/renderer-screenshot.d.ts.map +1 -0
  225. package/dist/renderer-screenshot.js +17 -0
  226. package/dist/renderer-screenshot.js.map +1 -0
  227. package/dist/renderer-symbolic-overlays.d.ts +4 -2
  228. package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
  229. package/dist/renderer-symbolic-overlays.js +33 -13
  230. package/dist/renderer-symbolic-overlays.js.map +1 -1
  231. package/dist/renderer-viewport.d.ts +45 -0
  232. package/dist/renderer-viewport.d.ts.map +1 -1
  233. package/dist/renderer-viewport.js +44 -0
  234. package/dist/renderer-viewport.js.map +1 -1
  235. package/dist/rte-clip-space.d.ts +12 -0
  236. package/dist/rte-clip-space.d.ts.map +1 -0
  237. package/dist/rte-clip-space.js +22 -0
  238. package/dist/rte-clip-space.js.map +1 -0
  239. package/dist/rte-frustum.d.ts +24 -0
  240. package/dist/rte-frustum.d.ts.map +1 -0
  241. package/dist/rte-frustum.js +45 -0
  242. package/dist/rte-frustum.js.map +1 -0
  243. package/dist/scene-batch-upload.d.ts.map +1 -1
  244. package/dist/scene-batch-upload.js +14 -38
  245. package/dist/scene-batch-upload.js.map +1 -1
  246. package/dist/scene-device-recovery.d.ts.map +1 -1
  247. package/dist/scene-device-recovery.js +5 -9
  248. package/dist/scene-device-recovery.js.map +1 -1
  249. package/dist/scene-finalize-regroup.d.ts +54 -0
  250. package/dist/scene-finalize-regroup.d.ts.map +1 -0
  251. package/dist/scene-finalize-regroup.js +136 -0
  252. package/dist/scene-finalize-regroup.js.map +1 -0
  253. package/dist/scene-instance-bounds.d.ts +12 -0
  254. package/dist/scene-instance-bounds.d.ts.map +1 -0
  255. package/dist/scene-instance-bounds.js +48 -0
  256. package/dist/scene-instance-bounds.js.map +1 -0
  257. package/dist/scene-instance-materialization.d.ts +4 -1
  258. package/dist/scene-instance-materialization.d.ts.map +1 -1
  259. package/dist/scene-instance-materialization.js +21 -6
  260. package/dist/scene-instance-materialization.js.map +1 -1
  261. package/dist/scene-instance-translation.d.ts +17 -0
  262. package/dist/scene-instance-translation.d.ts.map +1 -0
  263. package/dist/scene-instance-translation.js +50 -0
  264. package/dist/scene-instance-translation.js.map +1 -0
  265. package/dist/scene-instance-types.d.ts +6 -0
  266. package/dist/scene-instance-types.d.ts.map +1 -1
  267. package/dist/scene-model-translation.d.ts +2 -0
  268. package/dist/scene-model-translation.d.ts.map +1 -1
  269. package/dist/scene-model-translation.js +1 -1
  270. package/dist/scene-model-translation.js.map +1 -1
  271. package/dist/scene-rect-select.d.ts +11 -1
  272. package/dist/scene-rect-select.d.ts.map +1 -1
  273. package/dist/scene-rect-select.js +7 -4
  274. package/dist/scene-rect-select.js.map +1 -1
  275. package/dist/scene.d.ts +27 -20
  276. package/dist/scene.d.ts.map +1 -1
  277. package/dist/scene.js +109 -238
  278. package/dist/scene.js.map +1 -1
  279. package/dist/section-2d-lift.d.ts +10 -2
  280. package/dist/section-2d-lift.d.ts.map +1 -1
  281. package/dist/section-2d-lift.js +14 -6
  282. package/dist/section-2d-lift.js.map +1 -1
  283. package/dist/section-2d-line-buffer.d.ts +12 -4
  284. package/dist/section-2d-line-buffer.d.ts.map +1 -1
  285. package/dist/section-2d-line-buffer.js +59 -33
  286. package/dist/section-2d-line-buffer.js.map +1 -1
  287. package/dist/section-2d-overlay.d.ts +18 -72
  288. package/dist/section-2d-overlay.d.ts.map +1 -1
  289. package/dist/section-2d-overlay.js +72 -103
  290. package/dist/section-2d-overlay.js.map +1 -1
  291. package/dist/section-2d-types.d.ts +38 -0
  292. package/dist/section-2d-types.d.ts.map +1 -0
  293. package/dist/section-2d-types.js +6 -0
  294. package/dist/section-2d-types.js.map +1 -0
  295. package/dist/section-cap-pipelines.d.ts +6 -0
  296. package/dist/section-cap-pipelines.d.ts.map +1 -0
  297. package/dist/section-cap-pipelines.js +87 -0
  298. package/dist/section-cap-pipelines.js.map +1 -0
  299. package/dist/section-plane-geometry.d.ts +24 -0
  300. package/dist/section-plane-geometry.d.ts.map +1 -0
  301. package/dist/section-plane-geometry.js +93 -0
  302. package/dist/section-plane-geometry.js.map +1 -0
  303. package/dist/section-plane.d.ts +26 -24
  304. package/dist/section-plane.d.ts.map +1 -1
  305. package/dist/section-plane.js +57 -182
  306. package/dist/section-plane.js.map +1 -1
  307. package/dist/shaders/ao.wgsl.d.ts +2 -0
  308. package/dist/shaders/ao.wgsl.d.ts.map +1 -0
  309. package/dist/shaders/ao.wgsl.js +234 -0
  310. package/dist/shaders/ao.wgsl.js.map +1 -0
  311. package/dist/shaders/color-transfer.wgsl.d.ts +28 -0
  312. package/dist/shaders/color-transfer.wgsl.d.ts.map +1 -0
  313. package/dist/shaders/color-transfer.wgsl.js +55 -0
  314. package/dist/shaders/color-transfer.wgsl.js.map +1 -0
  315. package/dist/shaders/depth-reconstruct.wgsl.d.ts +16 -0
  316. package/dist/shaders/depth-reconstruct.wgsl.d.ts.map +1 -0
  317. package/dist/shaders/depth-reconstruct.wgsl.js +69 -0
  318. package/dist/shaders/depth-reconstruct.wgsl.js.map +1 -0
  319. package/dist/shaders/edges.wgsl.d.ts +2 -0
  320. package/dist/shaders/edges.wgsl.d.ts.map +1 -0
  321. package/dist/shaders/edges.wgsl.js +158 -0
  322. package/dist/shaders/edges.wgsl.js.map +1 -0
  323. package/dist/shaders/main-rte-shadow.wgsl.d.ts +4 -0
  324. package/dist/shaders/main-rte-shadow.wgsl.d.ts.map +1 -0
  325. package/dist/shaders/main-rte-shadow.wgsl.js +54 -0
  326. package/dist/shaders/main-rte-shadow.wgsl.js.map +1 -0
  327. package/dist/shaders/main.wgsl.d.ts +1 -6
  328. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  329. package/dist/shaders/main.wgsl.js +183 -228
  330. package/dist/shaders/main.wgsl.js.map +1 -1
  331. package/dist/shaders/relative-to-eye.wgsl.d.ts +12 -0
  332. package/dist/shaders/relative-to-eye.wgsl.d.ts.map +1 -0
  333. package/dist/shaders/relative-to-eye.wgsl.js +40 -0
  334. package/dist/shaders/relative-to-eye.wgsl.js.map +1 -0
  335. package/dist/shaders/section-2d-overlay.wgsl.d.ts +28 -12
  336. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
  337. package/dist/shaders/section-2d-overlay.wgsl.js +40 -13
  338. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
  339. package/dist/shaders/shadow.wgsl.d.ts +1 -1
  340. package/dist/shaders/shadow.wgsl.d.ts.map +1 -1
  341. package/dist/shaders/shadow.wgsl.js +25 -7
  342. package/dist/shaders/shadow.wgsl.js.map +1 -1
  343. package/dist/shaders/sky.wgsl.d.ts +1 -11
  344. package/dist/shaders/sky.wgsl.d.ts.map +1 -1
  345. package/dist/shaders/sky.wgsl.js +17 -19
  346. package/dist/shaders/sky.wgsl.js.map +1 -1
  347. package/dist/shaders/specular.wgsl.d.ts +34 -0
  348. package/dist/shaders/specular.wgsl.d.ts.map +1 -0
  349. package/dist/shaders/specular.wgsl.js +134 -0
  350. package/dist/shaders/specular.wgsl.js.map +1 -0
  351. package/dist/shaders/symbolic-overlay.wgsl.d.ts +2 -9
  352. package/dist/shaders/symbolic-overlay.wgsl.d.ts.map +1 -1
  353. package/dist/shaders/symbolic-overlay.wgsl.js +88 -17
  354. package/dist/shaders/symbolic-overlay.wgsl.js.map +1 -1
  355. package/dist/shaders/textured.wgsl.d.ts.map +1 -1
  356. package/dist/shaders/textured.wgsl.js +11 -9
  357. package/dist/shaders/textured.wgsl.js.map +1 -1
  358. package/dist/shadow-light-matrix.d.ts +7 -0
  359. package/dist/shadow-light-matrix.d.ts.map +1 -1
  360. package/dist/shadow-light-matrix.js +11 -0
  361. package/dist/shadow-light-matrix.js.map +1 -1
  362. package/dist/shadow-occluder-batches.d.ts +10 -0
  363. package/dist/shadow-occluder-batches.d.ts.map +1 -0
  364. package/dist/shadow-occluder-batches.js +39 -0
  365. package/dist/shadow-occluder-batches.js.map +1 -0
  366. package/dist/shadow-occluders.d.ts.map +1 -1
  367. package/dist/shadow-occluders.js +4 -0
  368. package/dist/shadow-occluders.js.map +1 -1
  369. package/dist/shadow-pass.d.ts +5 -51
  370. package/dist/shadow-pass.d.ts.map +1 -1
  371. package/dist/shadow-pass.js +49 -45
  372. package/dist/shadow-pass.js.map +1 -1
  373. package/dist/shadow-types.d.ts +39 -0
  374. package/dist/shadow-types.d.ts.map +1 -0
  375. package/dist/shadow-types.js +15 -0
  376. package/dist/shadow-types.js.map +1 -0
  377. package/dist/symbolic-overlay-geometry.d.ts +7 -0
  378. package/dist/symbolic-overlay-geometry.d.ts.map +1 -0
  379. package/dist/symbolic-overlay-geometry.js +43 -0
  380. package/dist/symbolic-overlay-geometry.js.map +1 -0
  381. package/dist/symbolic-overlay-pipelines.d.ts +24 -36
  382. package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
  383. package/dist/symbolic-overlay-pipelines.js +175 -182
  384. package/dist/symbolic-overlay-pipelines.js.map +1 -1
  385. package/dist/symbolic-text-atlas.d.ts +21 -0
  386. package/dist/symbolic-text-atlas.d.ts.map +1 -1
  387. package/dist/symbolic-text-atlas.js +29 -1
  388. package/dist/symbolic-text-atlas.js.map +1 -1
  389. package/dist/types.d.ts +20 -26
  390. package/dist/types.d.ts.map +1 -1
  391. package/dist/visual-enhancement.d.ts +30 -1
  392. package/dist/visual-enhancement.d.ts.map +1 -1
  393. package/dist/visual-enhancement.js +44 -12
  394. package/dist/visual-enhancement.js.map +1 -1
  395. package/package.json +3 -3
  396. package/dist/post-processor.d.ts +0 -60
  397. package/dist/post-processor.d.ts.map +0 -1
  398. package/dist/post-processor.js +0 -353
  399. package/dist/post-processor.js.map +0 -1
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export { appearanceSourceTriangle, expandAppearanceCorners, equivalentAppearance
11
11
  export { sameCompanionParts } from './appearance-companions.js';
12
12
  export { invertAppearancePartition, validateAppearancePartition } from './appearance-preview.js';
13
13
  import { createReferenceImageManager } from './reference-image-host.js';
14
- import { resizeRendererViewport } from './renderer-viewport.js';
14
+ import { measureDrawingBuffer, resizeRendererViewport } from './renderer-viewport.js';
15
15
  export { pickFitPolicy } from './camera-fit-policy.js';
16
16
  // `Scene` is NOT exported: an exported class republishes every method `SceneContents` above just froze.
17
17
  export { Picker } from './picker.js';
@@ -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';
@@ -79,32 +81,38 @@ import { Scene } from './scene.js';
79
81
  import { Picker } from './picker.js';
80
82
  import { reportableItemId } from './pick-resolve.js';
81
83
  import { MathUtils, viewBasis } from './math.js';
82
- import { FrustumUtils } from '@ifc-lite/spatial';
83
- import { VisualEnhancementResolver } from './visual-enhancement.js';
84
+ import { isRteAabbVisible, rteFrustum, sourceFrustumFromRte } from './rte-frustum.js';
85
+ import { VisualEnhancementResolver, livePostEffects } from './visual-enhancement.js';
84
86
  import { packClipBox } from './clip-box.js';
87
+ import { MESH_FLAGS_BYTE_OFFSET, MESH_FLAG_RTE_DRAWABLE, MESH_UNIFORM_FLOATS, MESH_UNIFORM_OFFSET, packRteCameraOrigin, packRteFragmentSpace, } from './mesh-rte-uniforms.js';
88
+ import { packMeshMaterial } from './mesh-material.js';
85
89
  import { RendererOverlays } from './renderer-overlays.js';
86
90
  import { resolveSectionPlaneFrame } from './render-section-plane.js';
87
91
  import { PickingManager } from './picking-manager.js';
88
92
  import { RaycastEngine } from './raycast-engine.js';
89
93
  import { RenderDegradationMonitor } from './render-degradation.js';
90
- import { PostProcessor } from './post-processor.js';
94
+ import { PostPassChain } from './post-pass-chain.js';
91
95
  import { InteractionEffectsGovernor } from './interaction-effects-governor.js';
92
96
  import { VisibilityEpochTracker } from './visibility-epoch.js';
93
97
  import { isEntityVisible } from './entity-visibility.js';
94
98
  import { ModelBoundsTracker } from './model-bounds-tracker.js';
95
99
  import { resolveContributionThresholdPx, projectedAabbRadiusPx, projectedInstancedRadiusPx } from './contribution-cull.js';
96
- import { EdlPass } from './edl-pass.js';
97
100
  import { SkyPass } from './sky-pass.js';
98
101
  import { skyShaderSource } from './shaders/sky.wgsl.js';
99
102
  import { resolveEnvironment } from './environment.js';
100
103
  import { ShadowPass, resolveShadowMapResolution } from './shadow-pass.js';
101
- import { fitSunLightMatrix, cameraFrustumFocusCorners } from './shadow-light-matrix.js';
102
- import { collectShadowOccluders, classifyBatchVisibility, DEFAULT_MIN_CAST_ALPHA } from './shadow-occluders.js';
104
+ import { fitSunLightMatrix, cameraFrustumFocusCorners, resolveShadowNormalBiasMetres } from './shadow-light-matrix.js';
105
+ import { collectShadowOccluders } from './shadow-occluders.js';
106
+ import { uploadInstancedRteDeltas } from './instanced-rte.js';
107
+ import { shadowOccluderBatches } from './shadow-occluder-batches.js';
108
+ import { captureRendererScreenshot } from './renderer-screenshot.js';
109
+ import { beginRendererColorFrameCapture, cancelRendererColorFrame, discardRendererColorFrameReadback, encodeRendererColorFrameCapture, requestRendererColorFrame, retryRendererColorFrame, settleRendererColorFrameCapture } from './renderer-color-readback.js';
103
110
  import { shouldRouteMeshTransparent, shouldRouteBatchTransparent, splitVisibleIdsByPromotion, DEFAULT_GHOST_ALPHA } from './overlay-routing.js';
104
111
  import { XRayAlpha, XRayEpochTracker } from './xray-alpha.js';
105
112
  import { PartialBatchRequests } from './partial-batch-requests.js';
106
113
  import { colorSaltByte, packEntityLane } from './scene-geometry.js';
107
114
  import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
115
+ import { appendPointCloudChunk as appendPointCloudChunkImpl, beginPointCloudStream as beginPointCloudStreamImpl, endPointCloudStream as endPointCloudStreamImpl, removePointCloudAsset as removePointCloudAssetImpl, } from './pointcloud/point-cloud-stream-lifecycle.js';
108
116
  import { DeviationComputer } from './deviation/deviation-computer.js';
109
117
  import { runGuardedGpuUpload, isDeviceLossThrow } from './gpu-upload-guard.js';
110
118
  import { recoverRendererDevice, rendererDeviceLostError } from './device-recovery.js';
@@ -133,15 +141,19 @@ export class Renderer {
133
141
  scene;
134
142
  picker = null;
135
143
  canvas;
144
+ // The overlay theme (#5484), kept so a pre-init `setOverlayTheme` call
145
+ // (and a later re-init) still lands — `init()` re-applies it to `pipeline`.
146
+ overlayTheme = DEFAULT_OVERLAY_THEME;
136
147
  /**
137
148
  * Section-plane gizmo, 2D section drawing/cap, and the standalone 3D line
138
149
  * + symbolic annotation overlays (issue #2425). Created here rather than in
139
- * `init()` so a pre-init `setOverlayLineColor` still lands — the GPU
150
+ * `init()` so a pre-init `setOverlayTheme` still lands — the GPU
140
151
  * objects inside stay null until `init()` calls `overlays.init()`.
141
152
  */
142
153
  overlays = new RendererOverlays({
143
154
  getModelBounds: () => this.getModelBounds(),
144
155
  expandModelBoundsWithFlatVertices: (positions, stride) => this.modelBoundsTracker.expandWithFlatVertices(positions, stride),
156
+ expandModelBoundsWithAnchoredLineVertices: (positions, origin, stride) => this.modelBoundsTracker.expandWithAnchoredVertices(positions, origin, stride),
145
157
  syncCameraSceneBounds: () => {
146
158
  if (this.modelBounds)
147
159
  this.camera.setSceneBounds(this.modelBounds);
@@ -150,9 +162,12 @@ export class Renderer {
150
162
  });
151
163
  referenceImages = createReferenceImageManager(this);
152
164
  getReferenceImages() { return this.referenceImages; }
153
- postProcessor = null;
165
+ // Ambient occlusion, separation lines and eye-dome lighting (post-pass-chain.ts),
166
+ // created on the first rendered frame.
167
+ postPasses = null;
168
+ /** Device px per CSS px in the drawing buffer; CSS-px sizes scale by it (#5383). */
169
+ pixelRatio = 1;
154
170
  interactionEffects = new InteractionEffectsGovernor();
155
- edlPass = null;
156
171
  // Procedural sky background — created lazily on the first frame that
157
172
  // enables it (most sessions never do).
158
173
  skyPass = null;
@@ -173,6 +188,7 @@ export class Renderer {
173
188
  pointCloudRenderer = null;
174
189
  pointCloudStreamEpoch = 0;
175
190
  pointCloudStreamEpochs = new Map(); // handle id → epoch; cleanup rebuilds `{ id }`, cleared per epoch
191
+ pointCloudNextHandleId = 1; // watermark carried across teardown() so a replacement never reissues an id
176
192
  /** Set true at the end of the LATEST `init()`; gates `whenReady()` and
177
193
  * `isReady()`. Revoked synchronously by `init()` and by `destroy()`, and
178
194
  * overridden (not cleared) by a device loss — see `deviceLost`, which the
@@ -381,13 +397,9 @@ export class Renderer {
381
397
  // One-shot log guard — prints Y-up clip bounds on first section-enable so
382
398
  // users can confirm the slider is operating on the intended range.
383
399
  _loggedSectionBounds = false;
384
- // Pooled per-frame buffers to avoid GC pressure from per-batch Float32Array allocations
385
- // A single 224-byte uniform buffer (56 floats) is reused for all batches/meshes within a frame
386
- // (48 floats viewProj…flags + 8 floats clipBoxMin/clipBoxMax)
387
- // 60 floats = the WGSL Uniforms struct incl. quantParams (see
388
- // pipeline.getUniformBufferSize).
389
- uniformScratch = new Float32Array(60);
390
- uniformScratchU32 = new Uint32Array(this.uniformScratch.buffer, 176, 4);
400
+ // One 336-byte buffer serves all frame batches/meshes (84 floats including RTE lanes).
401
+ uniformScratch = new Float32Array(MESH_UNIFORM_FLOATS);
402
+ uniformScratchU32 = new Uint32Array(this.uniformScratch.buffer, MESH_FLAGS_BYTE_OFFSET, 4);
391
403
  // What the last render() actually clipped, so the GPU picker can mirror it and
392
404
  // section/crop-clipped geometry stays unpickable, not just invisible. Updated
393
405
  // every render; read by pick()/pickRect(). null = nothing clipped that frame.
@@ -409,8 +421,8 @@ export class Renderer {
409
421
  * are released first. The comment below advertises a `destroy()` + `init()`
410
422
  * re-init flow, and the obvious device-loss auto-recovery is to call
411
423
  * `init()` on the live instance — which, without this, silently orphaned
412
- * two render pipelines, the picker, the post-processor, the point-cloud and
413
- * deviation pipelines, the EDL pass and the overlay layer's glyph atlas, per
424
+ * two render pipelines, the picker, the post passes, the point-cloud and
425
+ * deviation pipelines and the overlay layer's glyph atlas, per
414
426
  * recovery (#2448). Making the method self-safe is cheaper than trusting
415
427
  * every future caller to remember.
416
428
  *
@@ -473,8 +485,8 @@ export class Renderer {
473
485
  await initializingDevice.init(this.canvas);
474
486
  // A `destroy()` (or a newer `init()`) landed while we were parked on the
475
487
  // device. Everything below allocates a full GPU stack — two pipelines,
476
- // the picker, the post-processor, the point-cloud and deviation
477
- // pipelines, the EDL pass, the overlay glyph atlas — and this aborted
488
+ // the picker, the point-cloud and deviation pipelines, the overlay
489
+ // glyph atlas — and this aborted
478
490
  // path runs no second teardown, so all of it would be orphaned outright
479
491
  // (#2465). `markReady()`'s generation check is not enough on its own: it
480
492
  // withholds the readiness PUBLICATION, not the allocation. Release the
@@ -484,43 +496,32 @@ export class Renderer {
484
496
  this.device.destroy();
485
497
  return;
486
498
  }
487
- // Get canvas dimensions (use pixel dimensions if set, otherwise use CSS dimensions)
488
- // and clamp to the GPU's max 2D texture dimension so the initial pipeline allocations
489
- // can't overflow on tall/wide layouts (see render() for the per-frame clamp).
490
- const rect = this.canvas.getBoundingClientRect();
499
+ // Size the buffer to the element's device pixels, the same sizing every
500
+ // frame applies (#5383); an unlaid-out canvas keeps its attributes. Both
501
+ // are clamped to the GPU's max 2D texture dimension so the initial
502
+ // pipeline allocations can't overflow on tall/wide layouts.
491
503
  const maxDim = this.device.getMaxTextureDimension();
492
- const rawWidth = this.canvas.width || Math.max(1, Math.floor(rect.width));
493
- const rawHeight = this.canvas.height || Math.max(1, Math.floor(rect.height));
494
- const width = Math.min(rawWidth, maxDim);
495
- const height = Math.min(rawHeight, maxDim);
504
+ const measured = measureDrawingBuffer(this.canvas, maxDim);
505
+ this.pixelRatio = measured?.pixelRatio ?? 1;
506
+ const width = Math.max(1, Math.min(measured?.width ?? this.canvas.width, maxDim));
507
+ const height = Math.max(1, Math.min(measured?.height ?? this.canvas.height, maxDim));
496
508
  // Set pixel dimensions if not already set, or if we clamped them down
497
509
  if (!this.canvas.width || !this.canvas.height || this.canvas.width !== width || this.canvas.height !== height) {
498
510
  this.canvas.width = width;
499
511
  this.canvas.height = height;
500
512
  }
501
513
  this.pipeline = new RenderPipeline(this.device, width, height);
514
+ // Re-apply any theme set before this (re)creation, or set by a prior
515
+ // init() before a device-loss re-init, so it isn't lost (#5484).
516
+ this.pipeline.selectionColorUniform.update(this.overlayTheme.selection);
502
517
  this.picker = new Picker(this.device, width, height);
503
518
  this.overlays.init(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
504
519
  this.referenceImages.init(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
505
- // PostProcessor is optional — if it fails (e.g. mobile GPU lacking
506
- // depth TEXTURE_BINDING), rendering still works without post-processing.
507
- try {
508
- this.postProcessor = new PostProcessor(this.device, {
509
- enableContactShading: true,
510
- contactRadius: 1.0,
511
- contactIntensity: 0.3,
512
- }, this.pipeline.getSampleCount());
513
- }
514
- catch (e) {
515
- console.warn('[Renderer] PostProcessor init failed (post-processing disabled):', e);
516
- this.postProcessor = null;
517
- }
518
- this.pointCloudRenderer = new PointCloudRenderer(this.device.getDevice(), this.device.getFormat(), 'depth24plus-stencil8', this.pipeline.getSampleCount());
520
+ this.pointCloudRenderer = new PointCloudRenderer(this.device.getDevice(), this.device.getFormat(), 'depth24plus-stencil8', this.pipeline.getSampleCount(), this.pointCloudNextHandleId);
519
521
  // Compute pipeline for the BIM↔scan deviation heatmap. Lazily
520
522
  // owns the per-triangle BVH GPU buffers; idle until the first
521
523
  // `computeDeviations` call.
522
524
  this.deviationComputer.init(this.device.getDevice());
523
- this.edlPass = new EdlPass(this.device, this.pipeline.getSampleCount());
524
525
  this.camera.setAspect(width / height);
525
526
  // Update picking manager with initialized picker
526
527
  this.pickingManager.setPicker(this.picker);
@@ -707,6 +708,7 @@ export class Renderer {
707
708
  }
708
709
  this.deviceLossSequence++;
709
710
  this.deviceLost = true;
711
+ cancelRendererColorFrame(this);
710
712
  this.recovery.lostReferenceImages = this.referenceImages.hasImages();
711
713
  this.referenceImages.destroy();
712
714
  this.deviceLostGeneration = this.initGeneration;
@@ -766,11 +768,13 @@ export class Renderer {
766
768
  // regions really do have such a source on a HEALTHY device: the outer
767
769
  // one runs `scene.restoreAllEvicted()` for capture frames, the encode
768
770
  // one builds visibility sub-batches through
769
- // `scene.getOrCreatePartialBatch()`, and both allocate via
770
- // `createBuffer({ mappedAtCreation: true })`, which throws a plain
771
- // `RangeError` under host memory pressure — the failure
772
- // `gpu-upload-guard` documents verbatim. Latching there would kill the
773
- // viewport for a failure whose blast radius should be one frame, and
771
+ // `scene.getOrCreatePartialBatch()`, and both merge geometry into
772
+ // fresh typed arrays, whose allocation throws a plain `RangeError`
773
+ // ("Array buffer allocation failed") under host memory pressure.
774
+ // (Before #5429 the GPU upload itself added a second source, the
775
+ // mapped-at-creation `createBuffer` RangeError; uploads now go through
776
+ // `createStaticGpuBuffer`, which cannot raise it.) Latching there
777
+ // would kill the viewport for a failure whose blast radius should be one frame, and
774
778
  // would raise a false "graphics device was lost" toast plus false
775
779
  // `device_lost` telemetry on top.
776
780
  //
@@ -879,38 +883,16 @@ export class Renderer {
879
883
  * chunks will arrive (currently a no-op but kept for symmetry).
880
884
  */
881
885
  beginPointCloudStream(meta) {
882
- if (this.deviceLost)
883
- throw rendererDeviceLostError();
884
- if (!this.pointCloudRenderer) {
885
- throw new Error('Renderer not initialized. Call init() first.');
886
- }
887
- const handle = this.pointCloudRenderer.beginAsset(meta);
888
- this.pointCloudStreamEpochs.set(handle.id, this.pointCloudStreamEpoch);
889
- return handle;
890
- }
891
- currentPointCloudStreamRenderer(handle) {
892
- if (this.deviceLost || !this.pointCloudRenderer
893
- || this.pointCloudStreamEpochs.get(handle.id) !== this.pointCloudStreamEpoch)
894
- throw rendererDeviceLostError();
895
- return this.pointCloudRenderer;
886
+ return beginPointCloudStreamImpl(this, meta);
896
887
  }
897
888
  appendPointCloudChunk(handle, chunk) {
898
- this.currentPointCloudStreamRenderer(handle).appendChunk(handle, chunk);
899
- this.modelBoundsTracker.expandForPointClouds();
900
- this.camera.setSceneBounds(this.modelBounds);
901
- this.requestRender();
889
+ appendPointCloudChunkImpl(this, handle, chunk);
902
890
  }
903
891
  endPointCloudStream(handle) {
904
- this.currentPointCloudStreamRenderer(handle).endAsset(handle);
905
- this.requestRender();
892
+ endPointCloudStreamImpl(this, handle);
906
893
  }
907
894
  removePointCloudAsset(handle) {
908
- if (!this.pointCloudStreamEpochs.delete(handle.id))
909
- return;
910
- this.pointCloudRenderer?.removeAsset(handle);
911
- // Bounds may have shrunk — recompute from scratch so fit-to-view
912
- // and section-plane sliders see fresh extents.
913
- this.refreshPlacementBounds();
895
+ removePointCloudAssetImpl(this, handle);
914
896
  }
915
897
  /**
916
898
  * Reassign a streamed point-cloud's expressId after upload. Use
@@ -958,6 +940,13 @@ export class Renderer {
958
940
  this.clearCaches();
959
941
  this.refreshPlacementBounds();
960
942
  }
943
+ /** Toggle one resident streamed scan without releasing its GPU buffers. */
944
+ setPointCloudVisibility(handle, visible) {
945
+ if (!this.pointCloudRenderer?.setAssetVisible(handle, visible))
946
+ return;
947
+ this.clearCaches();
948
+ this.refreshPlacementBounds();
949
+ }
961
950
  refreshPlacementBounds() {
962
951
  this.modelBoundsTracker.recompute();
963
952
  this.camera.setSceneBounds(this.modelBounds);
@@ -1157,65 +1146,7 @@ export class Renderer {
1157
1146
  * collector drops them.
1158
1147
  */
1159
1148
  shadowOccluderBatches(options, device, hasVisibilityFiltering) {
1160
- const all = this.scene.getBatchedMeshes();
1161
- if (!hasVisibilityFiltering) {
1162
- for (const batch of all)
1163
- this.noteShadowOccluderResidency(batch);
1164
- return all;
1165
- }
1166
- const pipeline = this.pipeline;
1167
- const out = [];
1168
- for (const batch of all) {
1169
- const vis = classifyBatchVisibility(batch.expressIds, options.hiddenIds, options.isolatedIds);
1170
- if (vis.kind === 'none')
1171
- continue; // fully hidden → does not cast
1172
- if (vis.kind === 'all') {
1173
- this.noteShadowOccluderResidency(batch);
1174
- out.push(batch); // fully visible → its own buffers
1175
- continue;
1176
- }
1177
- // Partially hidden. Transparent parents don't cast (collector's alpha
1178
- // filter) — skip rather than build a wasted, divergent-key clone.
1179
- if (batch.color[3] < DEFAULT_MIN_CAST_ALPHA)
1180
- continue;
1181
- // The partial sub-batch is built from the PARENT's CPU meshData, so a
1182
- // cold parent yields nothing until it is restored. Queue that restore
1183
- // here too: the colour pass only queues it for parents inside its own
1184
- // frustum, but an up-sun occluder behind the camera still has to cast.
1185
- this.noteShadowOccluderResidency(batch);
1186
- // Opaque partial: reuse the colour pass's cached sub-batch. The key is
1187
- // visibility-content-independent; `_partialBatchEpoch` invalidates it on
1188
- // any hide/isolate or override change, so the clone is always current.
1189
- // Without a pipeline the renderer isn't drawing, so skip (the shadow
1190
- // pass won't run either); casting the whole parent would be wrong.
1191
- if (!pipeline)
1192
- continue;
1193
- const sub = this.scene.getOrCreatePartialBatch(`${batch.colorKey}:${batch.id}`, batch.colorKey, vis.visibleIds, device, pipeline, this._partialBatchEpoch);
1194
- // A cold parent yields an empty partial (its residency restore is queued
1195
- // above); skip this frame — the collector drops zero-index draws anyway,
1196
- // and the subset casts once resident.
1197
- if (sub && sub.indexCount > 0)
1198
- out.push(sub);
1199
- }
1200
- return out;
1201
- }
1202
- /**
1203
- * Keep a shadow occluder batch resident so it does not thin out silently on
1204
- * large models under the GPU residency budget (#2670 review). The depth pass
1205
- * reads these batches' buffers, but that read did not count as usage, so an
1206
- * up-sun occluder outside the colour frustum aged into an eviction candidate.
1207
- * Transparent batches never cast (the collector's alpha filter), so their
1208
- * residency is irrelevant here.
1209
- */
1210
- noteShadowOccluderResidency(batch) {
1211
- if (batch.color[3] < DEFAULT_MIN_CAST_ALPHA)
1212
- return;
1213
- if (batch.gpuResident === false) {
1214
- this.scene.requestBatchResidency(batch);
1215
- }
1216
- else {
1217
- this.scene.recordBatchDrawn(batch);
1218
- }
1149
+ return shadowOccluderBatches(this.scene, options, device, hasVisibilityFiltering, this.pipeline, this._partialBatchEpoch);
1219
1150
  }
1220
1151
  /** Guarded entry point (#4885) for the unguarded body below. */
1221
1152
  createMeshFromData(meshData) {
@@ -1234,18 +1165,17 @@ export class Renderer {
1234
1165
  const interleaved = new Float32Array(interleavedRaw);
1235
1166
  const interleavedU32 = new Uint32Array(interleavedRaw);
1236
1167
  // Build this individual mesh (selection highlight + GPU object-id picker)
1237
- // in ABSOLUTE world space so it renders with an identity model matrix.
1168
+ // in the same small local frame as its source batch.
1238
1169
  // CRITICAL: replicate the BATCH's exact two-step f32 path so the highlight
1239
1170
  // is bit-coincident with its source surface (no z-fight, no depth bias):
1240
1171
  // batch stores s = f32(local + (origin - sharedOrigin)) [merge]
1241
- // batch shader world = f32(f32(sharedOrigin) + s) [draw]
1242
- // We compute the same `world` here. When there's no shared origin yet
1243
- // (legacy / pre-batch), fall back to a plain f64 fold (local + origin).
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.
1244
1175
  const o = meshData.origin;
1245
1176
  const so = this.scene.getSharedFrameOrigin(meshData.modelIndex, meshData);
1246
1177
  const ox = o ? o[0] : 0, oy = o ? o[1] : 0, oz = o ? o[2] : 0;
1247
1178
  const fr = Math.fround;
1248
- const sox = so ? fr(so[0]) : null, soy = so ? fr(so[1]) : 0, soz = so ? fr(so[2]) : 0;
1249
1179
  const dx = so ? (ox - so[0]) : ox, dy = so ? (oy - so[1]) : oy, dz = so ? (oz - so[2]) : oz;
1250
1180
  // Quantized batches (issue #1682 phase 6) render lattice-snapped
1251
1181
  // positions: the shader's quantMin + q*step is exactly the lattice
@@ -1262,9 +1192,9 @@ export class Renderer {
1262
1192
  for (let i = 0; i < vertexCount; i++) {
1263
1193
  const base = i * 7;
1264
1194
  const posBase = i * 3;
1265
- interleaved[base] = so ? fr(sox + snap(fr(p[posBase] + dx))) : snap(p[posBase] + dx);
1266
- interleaved[base + 1] = so ? fr(soy + snap(fr(p[posBase + 1] + dy))) : snap(p[posBase + 1] + dy);
1267
- interleaved[base + 2] = so ? fr(soz + snap(fr(p[posBase + 2] + dz))) : snap(p[posBase + 2] + dz);
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]);
1268
1198
  const hasNormals = meshData.normals.length > 0;
1269
1199
  interleaved[base + 3] = hasNormals ? meshData.normals[posBase] : 0;
1270
1200
  interleaved[base + 4] = hasNormals ? meshData.normals[posBase + 1] : 0;
@@ -1294,7 +1224,9 @@ export class Renderer {
1294
1224
  usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
1295
1225
  });
1296
1226
  device.queue.writeBuffer(indexBuffer, 0, meshData.indices);
1297
- // Add to scene with identity transform (positions already in world space).
1227
+ // Keep the hydrated mesh in its decoded local frame. Folding this
1228
+ // origin back into f32 vertices used to make selection/highlight the
1229
+ // only mesh path that lost centimetres at national-grid coordinates.
1298
1230
  // Flagged `hydrated` so it can be freed when its entity leaves the
1299
1231
  // selection — these duplicate geometry already drawn by a batch and would
1300
1232
  // otherwise accumulate + double-draw (see Scene.disposeHydratedMeshesExcept).
@@ -1309,7 +1241,14 @@ export class Renderer {
1309
1241
  vertexBuffer,
1310
1242
  indexBuffer,
1311
1243
  indexCount: meshData.indices.length,
1312
- transform: MathUtils.identity(),
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],
1313
1252
  color: meshData.color,
1314
1253
  hydrated: true,
1315
1254
  });
@@ -1469,6 +1408,7 @@ export class Renderer {
1469
1408
  // emit a stream of validation errors. Stay quiet until re-init.
1470
1409
  if (this.deviceLost) {
1471
1410
  this._renderSkipCount++;
1411
+ cancelRendererColorFrame(this);
1472
1412
  return;
1473
1413
  }
1474
1414
  try {
@@ -1480,6 +1420,7 @@ export class Renderer {
1480
1420
  // as "did not fail" — see `frameContainedThrow`.
1481
1421
  if (!this.frameContainedThrow)
1482
1422
  this.consecutiveDegradedFrames = 0;
1423
+ retryRendererColorFrame(this, () => this.requestRender());
1483
1424
  }
1484
1425
  catch (error) {
1485
1426
  // Safari (26.5) reports device loss SYNCHRONOUSLY: a call against a
@@ -1495,6 +1436,7 @@ export class Renderer {
1495
1436
  // host down with us".
1496
1437
  this._renderSkipCount++;
1497
1438
  this.containFrameThrow(error, 'frame');
1439
+ cancelRendererColorFrame(this);
1498
1440
  }
1499
1441
  }
1500
1442
  /**
@@ -1506,23 +1448,16 @@ export class Renderer {
1506
1448
  this._renderSkipCount++;
1507
1449
  return;
1508
1450
  }
1509
- // Validate canvas dimensions
1510
- // Align width to 64 pixels for WebGPU texture row alignment (256 bytes / 4 bytes per pixel)
1511
- // and clamp both axes to the GPU's max 2D texture dimension. Some hosts (e.g. tall iframes
1512
- // on high-DPR displays) can produce canvas dimensions that exceed 8192 and would otherwise
1513
- // make every depth/colour texture allocation a validation error.
1514
- const rect = this.canvas.getBoundingClientRect();
1515
- const maxDim = this.device.getMaxTextureDimension();
1516
- const rawWidth = Math.max(1, Math.floor(rect.width));
1517
- const widthAligned = Math.max(64, Math.floor(rawWidth / 64) * 64);
1518
- const width = Math.min(widthAligned, Math.floor(maxDim / 64) * 64);
1519
- const rawHeight = Math.max(1, Math.floor(rect.height));
1520
- const height = Math.min(rawHeight, maxDim);
1521
- // Skip rendering if canvas is too small
1522
- if (width < 64 || height < 10) {
1451
+ // Drawing buffer = the element's device-pixel size (capped ratio, clamped
1452
+ // to the GPU's max texture dimension); see computeDrawingBufferSize (#5383).
1453
+ const measured = measureDrawingBuffer(this.canvas, this.device.getMaxTextureDimension());
1454
+ // Skip rendering while the canvas is collapsed or too small.
1455
+ if (!measured || measured.height < 10) {
1523
1456
  this._renderSkipCount++;
1524
1457
  return;
1525
1458
  }
1459
+ const { width, height } = measured;
1460
+ this.pixelRatio = measured.pixelRatio;
1526
1461
  // Update canvas pixel dimensions if needed
1527
1462
  const dimensionsChanged = this.canvas.width !== width || this.canvas.height !== height;
1528
1463
  if (dimensionsChanged) {
@@ -1546,6 +1481,13 @@ export class Renderer {
1546
1481
  }
1547
1482
  const device = this.device.getDevice();
1548
1483
  const viewProj = this.camera.getViewProjMatrix().m;
1484
+ const relativeToEyeFrame = this.camera.getRelativeToEyeFrame();
1485
+ // Culling must share the translation-free RTE frame used by every GPU
1486
+ // pass. `sourceFrustum` is the f64 plane equivalent for the immutable
1487
+ // source-space BVH; render-time boxes stay eye-relative below.
1488
+ const rteCullFrustum = rteFrustum(relativeToEyeFrame);
1489
+ const rteCullCamera = relativeToEyeFrame.getCameraWorld();
1490
+ const sourceCullFrustum = sourceFrustumFromRte(rteCullFrustum, rteCullCamera);
1549
1491
  // Frame stats (issue #1682): geometry draw calls + per-frame cull
1550
1492
  // outcomes, snapshotted into _lastFrameStats before queue.submit.
1551
1493
  let frameDrawCalls = 0;
@@ -1585,11 +1527,8 @@ export class Renderer {
1585
1527
  const edgeIntensity = Math.min(3.0, Math.max(0.0, visualEnhancement.edgeContrast.intensity));
1586
1528
  const edgeEnabledU32 = edgeEnabled ? 1 : 0;
1587
1529
  const edgeIntensityMilliU32 = Math.round(edgeIntensity * 1000);
1588
- const contactEnabled = effectsLive && visualEnhancement.enabled && visualEnhancement.contactShading.quality !== 'off';
1589
- const separationEnabled = effectsLive && visualEnhancement.enabled
1590
- && visualEnhancement.separationLines.enabled
1591
- && visualEnhancement.separationLines.quality !== 'off';
1592
- const needsObjectIdPass = contactEnabled || separationEnabled;
1530
+ // Only the edge pass reads the object-id attachment after the pass.
1531
+ const needsObjectIdPass = livePostEffects(visualEnhancement, effectsLive).edges;
1593
1532
  // Check if visibility filtering is active
1594
1533
  const hasHiddenFilter = options.hiddenIds && options.hiddenIds.size > 0;
1595
1534
  const hasIsolatedFilter = options.isolatedIds !== null && options.isolatedIds !== undefined;
@@ -1697,8 +1636,7 @@ export class Renderer {
1697
1636
  // Frustum culling (if enabled and spatial index available)
1698
1637
  if (options.enableFrustumCulling && options.spatialIndex) {
1699
1638
  try {
1700
- const frustum = FrustumUtils.fromViewProjMatrix(viewProj);
1701
- const visibleIds = new Set(options.spatialIndex.queryFrustum(frustum));
1639
+ const visibleIds = new Set(options.spatialIndex.queryFrustum(sourceCullFrustum));
1702
1640
  meshes = meshes.filter(mesh => visibleIds.has(mesh.expressId));
1703
1641
  }
1704
1642
  catch (error) {
@@ -1739,12 +1677,15 @@ export class Renderer {
1739
1677
  }
1740
1678
  return; // Skip this frame, context will be reconfigured next frame
1741
1679
  }
1680
+ let colorCapture = null;
1681
+ let colorReadback = null;
1742
1682
  try {
1743
1683
  const clearColor = options.clearColor
1744
1684
  ? (Array.isArray(options.clearColor)
1745
1685
  ? { r: options.clearColor[0], g: options.clearColor[1], b: options.clearColor[2], a: options.clearColor[3] }
1746
1686
  : options.clearColor)
1747
1687
  : { r: 0.1, g: 0.1, b: 0.1, a: 1 };
1688
+ colorCapture = beginRendererColorFrameCapture(this, currentTexture, this.canvas.width, this.canvas.height, this.device.getFormat());
1748
1689
  const textureView = currentTexture.createView();
1749
1690
  const objectIdView = this.pipeline.getObjectIdTextureView();
1750
1691
  // Separate meshes into opaque and transparent
@@ -1818,6 +1759,8 @@ export class Renderer {
1818
1759
  for (const mesh of allMeshes) {
1819
1760
  if (mesh.uniformBuffer) {
1820
1761
  meshBuf.set(viewProj, 0);
1762
+ relativeToEyeFrame.packUniforms(meshBuf, MESH_UNIFORM_OFFSET.rteViewProj);
1763
+ packRteCameraOrigin(relativeToEyeFrame, meshBuf);
1821
1764
  meshBuf.set(mesh.transform.m, 16);
1822
1765
  // Check if mesh is selected (single or multi-selection)
1823
1766
  // For multi-model support: also check modelIndex if provided
@@ -1830,10 +1773,7 @@ export class Renderer {
1830
1773
  meshBuf[34] = mesh.color[2];
1831
1774
  // Selected meshes always keep their own alpha so highlights stay opaque
1832
1775
  meshBuf[35] = isSelected ? mesh.color[3] : alphaForMesh(mesh.expressId, mesh.color[3]);
1833
- meshBuf[36] = mesh.material?.metallic ?? 0.0;
1834
- meshBuf[37] = mesh.material?.roughness ?? 0.6;
1835
- meshBuf[38] = 0;
1836
- meshBuf[39] = 0;
1776
+ packMeshMaterial(meshBuf, mesh.color[3], mesh.material);
1837
1777
  // Section plane data (offset 40-43)
1838
1778
  if (sectionPlaneData) {
1839
1779
  meshBuf[40] = sectionPlaneData.normal[0];
@@ -1858,6 +1798,13 @@ export class Renderer {
1858
1798
  clipBit;
1859
1799
  meshFlags[2] = edgeEnabledU32;
1860
1800
  meshFlags[3] = edgeIntensityMilliU32;
1801
+ // Individual meshes retain their origin and share the RTE
1802
+ // fragment contract with colour batches.
1803
+ packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, meshBuf);
1804
+ const meshOrigin = mesh.rteOrigin
1805
+ ?? [mesh.transform.m[12], mesh.transform.m[13], mesh.transform.m[14]];
1806
+ relativeToEyeFrame.packDrawableOrigin(meshOrigin, meshBuf, MESH_UNIFORM_OFFSET.drawableDelta);
1807
+ meshFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
1861
1808
  device.queue.writeBuffer(mesh.uniformBuffer, 0, meshBuf);
1862
1809
  }
1863
1810
  }
@@ -1887,8 +1834,16 @@ export class Renderer {
1887
1834
  else {
1888
1835
  this.shadowPass.setResolution(resolution);
1889
1836
  }
1890
- const boundsMin = [bounds.min.x, bounds.min.y, bounds.min.z];
1891
- const boundsMax = [bounds.max.x, bounds.max.y, bounds.max.z];
1837
+ // Fit shadows in the same eye-relative frame as vertices.
1838
+ const shadowEye = relativeToEyeFrame.getCameraWorld();
1839
+ const sourceBoundsMin = [bounds.min.x, bounds.min.y, bounds.min.z];
1840
+ const sourceBoundsMax = [bounds.max.x, bounds.max.y, bounds.max.z];
1841
+ const boundsMin = [
1842
+ bounds.min.x - shadowEye[0], bounds.min.y - shadowEye[1], bounds.min.z - shadowEye[2],
1843
+ ];
1844
+ const boundsMax = [
1845
+ bounds.max.x - shadowEye[0], bounds.max.y - shadowEye[1], bounds.max.z - shadowEye[2],
1846
+ ];
1892
1847
  // Lateral shadow fit. AT REST: fit to the camera frustum
1893
1848
  // clipped to the model (maintainer #1) so a small building on
1894
1849
  // a large site keeps sharp shadows instead of spending the
@@ -1911,9 +1866,13 @@ export class Renderer {
1911
1866
  aspect: this.canvas.height > 0 ? this.canvas.width / this.canvas.height : 1,
1912
1867
  ortho: this.camera.getProjectionMode() === 'orthographic',
1913
1868
  orthoHalfHeight: this.camera.getOrthoSize(),
1914
- boundsMin,
1915
- boundsMax,
1916
- }) ?? undefined;
1869
+ boundsMin: sourceBoundsMin,
1870
+ boundsMax: sourceBoundsMax,
1871
+ })?.map((corner) => ({
1872
+ x: corner.x - shadowEye[0],
1873
+ y: corner.y - shadowEye[1],
1874
+ z: corner.z - shadowEye[2],
1875
+ }));
1917
1876
  }
1918
1877
  const sun = resolveEnvironment(options.environment).sunDirection;
1919
1878
  const fit = fitSunLightMatrix({ sunDirection: sun, boundsMin, boundsMax, focusCorners });
@@ -1948,7 +1907,7 @@ export class Renderer {
1948
1907
  box: options.clipBox?.enabled
1949
1908
  ? { min: options.clipBox.min, max: options.clipBox.max }
1950
1909
  : null,
1951
- });
1910
+ }, { cameraWorld: shadowEye });
1952
1911
  // Shadow uniform: light matrix + sampling params. The kernel
1953
1912
  // width follows the sun's angular size (physical, ~0.53°
1954
1913
  // like Blender's Sun lamp). Bias scales with the kernel: a
@@ -1958,7 +1917,7 @@ export class Renderer {
1958
1917
  const texelWorld = (2 * fit.orthoHalfWidth) / resolution;
1959
1918
  const sunAngleDeg = shadowOpts.sunAngleDeg ?? 0.53;
1960
1919
  const pcfRadius = Math.min(Math.max(sunAngleDeg * 3.0, 0.75), 8.0);
1961
- const normalBias = texelWorld * (2.0 + pcfRadius);
1920
+ const normalBias = resolveShadowNormalBiasMetres(texelWorld, pcfRadius);
1962
1921
  const s = this.shadowScratch;
1963
1922
  s.set(fit.lightViewProj.m, 0);
1964
1923
  s[16] = 1 / resolution; // texelSize
@@ -2094,7 +2053,7 @@ export class Renderer {
2094
2053
  if (allBatchedMeshes.length > 0 || this.scene.getTexturedMeshes().length > 0 || this.scene.getInstancedTemplates().length > 0) {
2095
2054
  // Frustum culling for batched meshes - skip entire batches outside the camera view
2096
2055
  // This is the primary performance optimization for large models (200K+ meshes)
2097
- const frustum = FrustumUtils.fromViewProjMatrix(viewProj);
2056
+ const frustum = rteCullFrustum;
2098
2057
  // Contribution culling (issue #1682): skip batches whose world
2099
2058
  // AABB projects below a pixel threshold. Disabled unless the
2100
2059
  // caller opts in via options.contributionCull; the threshold is
@@ -2121,7 +2080,7 @@ export class Renderer {
2121
2080
  mode: this.camera.getProjectionMode(),
2122
2081
  fovYRadians: this.camera.getFOV(),
2123
2082
  orthoHalfHeight: this.camera.getOrthoSize(),
2124
- viewportHeightPx: this.canvas.height,
2083
+ viewportHeightPx: this.canvas.height / this.pixelRatio, // CSS px, like the thresholds
2125
2084
  };
2126
2085
  }
2127
2086
  // Per-batch visibility (only meaningful while filtering is active).
@@ -2176,7 +2135,7 @@ export class Renderer {
2176
2135
  // Frustum culling: skip batches entirely outside the camera view
2177
2136
  if (batch.bounds) {
2178
2137
  const batchAABB = { min: batch.bounds.min, max: batch.bounds.max };
2179
- if (!FrustumUtils.isAABBVisible(frustum, batchAABB)) {
2138
+ if (!isRteAabbVisible(frustum, batchAABB, rteCullCamera)) {
2180
2139
  frameBatchesFrustumCulled++;
2181
2140
  continue; // Entire batch is off-screen
2182
2141
  }
@@ -2272,6 +2231,8 @@ export class Renderer {
2272
2231
  const tpl = this.uniformScratch;
2273
2232
  const tplFlags = this.uniformScratchU32;
2274
2233
  tpl.set(viewProj, 0);
2234
+ relativeToEyeFrame.packUniforms(tpl, MESH_UNIFORM_OFFSET.rteViewProj);
2235
+ packRteCameraOrigin(relativeToEyeFrame, tpl);
2275
2236
  // Identity model matrix (positions already in world space)
2276
2237
  tpl[16] = 1;
2277
2238
  tpl[17] = 0;
@@ -2289,12 +2250,9 @@ export class Renderer {
2289
2250
  tpl[29] = 0;
2290
2251
  tpl[30] = 0;
2291
2252
  tpl[31] = 1;
2292
- // Color placeholder — overwritten per batch
2293
- // tpl[32..35] set per batch
2294
- tpl[36] = 0.0; // metallic
2295
- tpl[37] = 0.6; // roughness
2296
- tpl[38] = 0;
2297
- tpl[39] = 0; // padding
2253
+ // Colour + material row (tpl[32..39]) are patched per draw; the
2254
+ // instanced passes keep this opaque default.
2255
+ packMeshMaterial(tpl);
2298
2256
  if (sectionPlaneData) {
2299
2257
  tpl[40] = sectionPlaneData.normal[0];
2300
2258
  tpl[41] = sectionPlaneData.normal[1];
@@ -2322,15 +2280,22 @@ export class Renderer {
2322
2280
  tplClipBit;
2323
2281
  tplFlags[2] = edgeEnabledU32;
2324
2282
  tplFlags[3] = edgeIntensityMilliU32;
2283
+ // Flat/quantized/textured batches enter WGSL in the single
2284
+ // camera-relative frame. Their fragment clip inputs must use
2285
+ // that same frame; mixing a local vertex with a 5,000 km f32
2286
+ // world plane loses centimetre cuts before the comparison.
2287
+ packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2325
2288
  // Helper function to render a batch — patches color into the shared template
2326
2289
  const renderBatch = (batch) => {
2327
2290
  if (!batch.bindGroup || !batch.uniformBuffer)
2328
2291
  return;
2329
- // Patch only the per-batch color (4 floats at offset 32)
2292
+ // Patch the per-batch colour, and the material its AUTHORED
2293
+ // alpha implies: an X-Ray fade is not glass (#5386).
2330
2294
  tpl[32] = batch.color[0];
2331
2295
  tpl[33] = batch.color[1];
2332
2296
  tpl[34] = batch.color[2];
2333
2297
  tpl[35] = alphaForBatch(batch, batch.color[3]);
2298
+ packMeshMaterial(tpl, batch.color[3]);
2334
2299
  // Per-batch local frame: the batch's vertices are stored
2335
2300
  // RELATIVE to batch.origin (f32-small), so set the model
2336
2301
  // matrix translation column to origin (world = origin + pos).
@@ -2341,6 +2306,11 @@ export class Renderer {
2341
2306
  tpl[28] = o ? o[0] : 0;
2342
2307
  tpl[29] = o ? o[1] : 0;
2343
2308
  tpl[30] = o ? o[2] : 0;
2309
+ // The regular model matrix remains for absolute-space
2310
+ // fragment work (section/shadow); vertex projection uses
2311
+ // this f64-subtracted high/low origin instead.
2312
+ relativeToEyeFrame.packDrawableOrigin(o ?? [0, 0, 0], tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2313
+ tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2344
2314
  // Quantized dequantization params (issue #1682 phase 6);
2345
2315
  // zeroed for f32 batches (their pipelines ignore them).
2346
2316
  const qz = batch.quantized;
@@ -2420,7 +2390,7 @@ export class Renderer {
2420
2390
  const kept = [];
2421
2391
  for (const it of instancedTemplates) {
2422
2392
  if (it.bounds) {
2423
- if (!FrustumUtils.isAABBVisible(frustum, it.bounds)) {
2393
+ if (!isRteAabbVisible(frustum, it.bounds, rteCullCamera)) {
2424
2394
  frameInstancedFrustumCulled++;
2425
2395
  continue;
2426
2396
  }
@@ -2438,11 +2408,12 @@ export class Renderer {
2438
2408
  visibleInstanced = kept;
2439
2409
  }
2440
2410
  if (visibleInstanced.length > 0) {
2411
+ uploadInstancedRteDeltas(device, visibleInstanced, relativeToEyeFrame.getCameraWorld());
2441
2412
  // Opaque instanced pass. flags.x bit 2 marks "instanced pass" so the
2442
2413
  // shader routes per-instance opacity: opaque (or selected) occurrences
2443
2414
  // draw here; translucent ones (lens/x-ray/compare overrides) are
2444
2415
  // discarded and drawn in the transparent sub-pass below.
2445
- this.pipeline.writeRawUniforms(tpl, 0x4);
2416
+ this.pipeline.writeRawUniforms(tpl, MESH_FLAG_RTE_DRAWABLE | 0x4);
2446
2417
  pass.setPipeline(this.pipeline.getInstancedPipeline());
2447
2418
  pass.setBindGroup(0, this.pipeline.getBindGroup());
2448
2419
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
@@ -2502,10 +2473,22 @@ export class Renderer {
2502
2473
  tpl[28] = tm.origin[0];
2503
2474
  tpl[29] = tm.origin[1];
2504
2475
  tpl[30] = tm.origin[2];
2476
+ relativeToEyeFrame.packDrawableOrigin(tm.origin, tpl, MESH_UNIFORM_OFFSET.drawableDelta);
2477
+ tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2505
2478
  tpl[32] = txOverride ? txOverride[0] : tm.color[0];
2506
2479
  tpl[33] = txOverride ? txOverride[1] : tm.color[1];
2507
2480
  tpl[34] = txOverride ? txOverride[2] : tm.color[2];
2508
2481
  tpl[35] = txAlpha;
2482
+ // Same authored-alpha + material contract as the flat/batched
2483
+ // paths (mesh-material.ts) — a call with neither argument
2484
+ // silently gave every textured metal or translucent mesh the
2485
+ // opaque dielectric default (#5623 review). The pipeline
2486
+ // itself is still opaque/depth-write only (no blend state,
2487
+ // see `txAlpha` above), so a translucent authored alpha still
2488
+ // never actually blends here — only the glass/roughness
2489
+ // CHOICE in the shader follows the same rule as everywhere
2490
+ // else, for the same reason `tm.material` does.
2491
+ packMeshMaterial(tpl, tm.color[3], tm.material);
2509
2492
  device.queue.writeBuffer(tm.uniformBuffer, 0, tpl);
2510
2493
  pass.setBindGroup(0, tm.bindGroup);
2511
2494
  pass.setVertexBuffer(0, tm.vertexBuffer);
@@ -2568,7 +2551,7 @@ export class Renderer {
2568
2551
  // flags.x bit 1 = overlay: tells the shader to preserve baseColor.a
2569
2552
  // (the overlay pipeline now has src-alpha blending so low-alpha ghost
2570
2553
  // tints composite correctly against the opaque pass) AND skip the
2571
- // glass-fresnel branch (which is meant for real glass materials and
2554
+ // specular term (glass reflections are meant for real glass and
2572
2555
  // would whiten low-alpha colour overrides at grazing angles).
2573
2556
  const overrideBatches = this.scene.getOverrideBatches();
2574
2557
  if (overrideBatches.length > 0) {
@@ -2648,7 +2631,7 @@ export class Renderer {
2648
2631
  if (visibleInstanced.length > 0 &&
2649
2632
  this.scene.hasTransparentInstances() &&
2650
2633
  instancedTransparentPipeline !== null) {
2651
- this.pipeline.writeRawUniforms(tpl, 0x4 | 0x8);
2634
+ this.pipeline.writeRawUniforms(tpl, MESH_FLAG_RTE_DRAWABLE | 0x4 | 0x8);
2652
2635
  pass.setPipeline(instancedTransparentPipeline);
2653
2636
  pass.setBindGroup(0, this.pipeline.getBindGroup());
2654
2637
  pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
@@ -2682,10 +2665,7 @@ export class Renderer {
2682
2665
  tpl[33] = mesh.color[1];
2683
2666
  tpl[34] = mesh.color[2];
2684
2667
  tpl[35] = alphaForMesh(mesh.expressId, mesh.color[3]);
2685
- tpl[36] = mesh.material?.metallic ?? 0.0;
2686
- tpl[37] = mesh.material?.roughness ?? 0.6;
2687
- tpl[38] = 0;
2688
- tpl[39] = 0;
2668
+ packMeshMaterial(tpl, mesh.color[3], mesh.material);
2689
2669
  if (sectionPlaneData) {
2690
2670
  tpl[40] = sectionPlaneData.normal[0];
2691
2671
  tpl[41] = sectionPlaneData.normal[1];
@@ -2705,6 +2685,10 @@ export class Renderer {
2705
2685
  tplClipBit;
2706
2686
  tplFlags[2] = edgeEnabledU32;
2707
2687
  tplFlags[3] = edgeIntensityMilliU32;
2688
+ packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2689
+ 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);
2691
+ tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2708
2692
  device.queue.writeBuffer(mesh.uniformBuffer, 0, tpl);
2709
2693
  pass.setBindGroup(0, mesh.bindGroup);
2710
2694
  pass.setVertexBuffer(0, mesh.vertexBuffer);
@@ -2742,10 +2726,7 @@ export class Renderer {
2742
2726
  tpl[33] = mesh.color[1];
2743
2727
  tpl[34] = mesh.color[2];
2744
2728
  tpl[35] = mesh.color[3];
2745
- tpl[36] = mesh.material?.metallic ?? 0.0;
2746
- tpl[37] = mesh.material?.roughness ?? 0.6;
2747
- tpl[38] = 0;
2748
- tpl[39] = 0;
2729
+ packMeshMaterial(tpl, mesh.color[3], mesh.material);
2749
2730
  if (sectionPlaneData) {
2750
2731
  tpl[40] = sectionPlaneData.normal[0];
2751
2732
  tpl[41] = sectionPlaneData.normal[1];
@@ -2765,6 +2746,10 @@ export class Renderer {
2765
2746
  tplClipBit;
2766
2747
  tplFlags[2] = edgeEnabledU32;
2767
2748
  tplFlags[3] = edgeIntensityMilliU32;
2749
+ packRteFragmentSpace(relativeToEyeFrame, sectionPlaneData, options.clipBox, tpl);
2750
+ 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);
2752
+ tplFlags[0] |= MESH_FLAG_RTE_DRAWABLE;
2768
2753
  device.queue.writeBuffer(mesh.uniformBuffer, 0, tpl);
2769
2754
  pass.setPipeline(this.pipeline.getSelectionPipeline());
2770
2755
  pass.setBindGroup(0, mesh.bindGroup);
@@ -2812,65 +2797,55 @@ export class Renderer {
2812
2797
  // viewport size to convert pixel sizes into clip-space offsets.
2813
2798
  if (this.pointCloudRenderer && this.pointCloudRenderer.hasAssets()) {
2814
2799
  this.pointCloudRenderer.draw(pass, {
2815
- viewProj,
2800
+ viewProj: relativeToEyeFrame.getViewProjection().m,
2801
+ relativeToEyeFrame,
2816
2802
  sectionPlane: sectionPlaneData
2817
2803
  ? { ...sectionPlaneData, flipped: options.sectionPlane?.flipped === true }
2818
2804
  : null,
2819
- viewport: { width: this.canvas.width, height: this.canvas.height },
2805
+ clipBox: options.clipBox,
2806
+ viewport: { width: this.canvas.width / this.pixelRatio, height: this.canvas.height / this.pixelRatio },
2820
2807
  });
2821
2808
  }
2822
2809
  // Section-plane gizmo, 2D section cap and every standalone 3D
2823
2810
  // overlay (annotation / alignment / grid / DXF / clash / symbolic
2824
2811
  // text). One draw call into the pass — see RendererOverlays.draw().
2825
- this.referenceImages.draw(pass, viewProj);
2812
+ this.referenceImages.draw(pass, viewProj, relativeToEyeFrame.getViewProjection().m, relativeToEyeFrame.getCameraWorld());
2826
2813
  this.overlays.draw(pass, {
2827
2814
  options,
2828
2815
  viewProj,
2829
2816
  modelBounds: this.getModelBounds(),
2830
2817
  camera: this.camera,
2831
- canvasWidth: this.canvas.width,
2832
- canvasHeight: this.canvas.height,
2818
+ canvasWidth: this.canvas.width / this.pixelRatio, // CSS px: glyph sizes are CSS px
2819
+ canvasHeight: this.canvas.height / this.pixelRatio, pixelRatio: this.pixelRatio,
2820
+ relativeToEyeFrame,
2821
+ rteViewProj: relativeToEyeFrame.getViewProjection().m,
2822
+ rteCamera: relativeToEyeFrame.getCameraWorld(),
2833
2823
  });
2834
2824
  pass.end();
2835
- const canRunPostPass = (contactEnabled || separationEnabled)
2836
- && this.postProcessor !== null;
2837
- if (canRunPostPass && this.postProcessor) {
2838
- this.postProcessor.updateOptions({
2839
- enableContactShading: contactEnabled,
2840
- contactRadius: visualEnhancement.contactShading.radius,
2841
- contactIntensity: visualEnhancement.contactShading.intensity,
2842
- });
2843
- this.postProcessor.apply(encoder, {
2844
- targetView: textureView,
2845
- // Depth-only view required because depth24plus-stencil8
2846
- // cannot be sampled as texture_depth_* with aspect 'all'.
2847
- depthView: this.pipeline.getDepthOnlyTextureView(),
2848
- objectIdView: this.pipeline.getObjectIdTextureView(),
2849
- contactQuality: contactEnabled && visualEnhancement.contactShading.quality === 'high' ? 'high' : 'low',
2850
- radius: Math.min(3.0, Math.max(1.0, visualEnhancement.contactShading.radius)),
2851
- intensity: contactEnabled ? Math.min(1.0, Math.max(0.0, visualEnhancement.contactShading.intensity)) : 0.0,
2852
- separationQuality: visualEnhancement.separationLines.quality === 'high' ? 'high' : 'low',
2853
- separationRadius: Math.min(2.0, Math.max(1.0, visualEnhancement.separationLines.radius)),
2854
- separationIntensity: separationEnabled ? Math.min(1.0, Math.max(0.0, visualEnhancement.separationLines.intensity)) : 0.0,
2855
- enableSeparationLines: separationEnabled,
2856
- });
2857
- }
2858
- // Eye-Dome Lighting — runs AFTER contact/separation so it darkens
2859
- // every layer uniformly. Cheap (~9 depth taps), only active when
2860
- // there are point clouds in the scene and the user has enabled it.
2861
- if (this.edlPass
2862
- && this.edlOptions.enabled
2863
- && this.pointCloudRenderer?.hasAssets()) {
2864
- this.edlPass.apply(encoder, {
2865
- targetView: textureView,
2866
- depthView: this.pipeline.getDepthOnlyTextureView(),
2867
- }, {
2868
- strength: this.edlOptions.strength,
2869
- radiusPx: this.edlOptions.radiusPx,
2870
- highQuality: this.edlOptions.highQuality,
2871
- });
2872
- }
2825
+ // Created lazily like the sky/shadow passes; each pass inside is too.
2826
+ this.postPasses ??= new PostPassChain(this.device, this.pipeline.getSampleCount());
2827
+ this.postPasses.encode({
2828
+ encoder,
2829
+ targetView: textureView,
2830
+ width: this.canvas.width,
2831
+ height: this.canvas.height,
2832
+ // Depth-only: depth24plus-stencil8 cannot be sampled as texture_depth_* with aspect 'all'.
2833
+ depthView: this.pipeline.getDepthOnlyTextureView(),
2834
+ objectIdView,
2835
+ projection: this.camera.getProjMatrix(),
2836
+ enhancement: visualEnhancement,
2837
+ effectsLive,
2838
+ pixelRatio: this.pixelRatio,
2839
+ // EDL only when point clouds are loaded and the user enabled it.
2840
+ edl: this.edlOptions.enabled && this.pointCloudRenderer?.hasAssets() ? this.edlOptions : null,
2841
+ });
2842
+ colorReadback = colorCapture && encodeRendererColorFrameCapture(device, encoder, colorCapture);
2873
2843
  device.queue.submit([encoder.finish()]);
2844
+ if (colorReadback && colorCapture) {
2845
+ settleRendererColorFrameCapture(this, colorCapture, colorReadback);
2846
+ colorCapture = null;
2847
+ colorReadback = null;
2848
+ }
2874
2849
  this._lastFrameStats = {
2875
2850
  drawCalls: frameDrawCalls,
2876
2851
  batchesDrawn: frameBatchesDrawn,
@@ -2894,6 +2869,7 @@ export class Renderer {
2894
2869
  }
2895
2870
  }
2896
2871
  catch (error) {
2872
+ discardRendererColorFrameReadback(colorReadback);
2897
2873
  // Balance the validation scope if we threw before popping it above —
2898
2874
  // an unpopped scope would capture every later frame's errors silently.
2899
2875
  // drainErrorScope logs a pop rejection (device loss) rather than
@@ -2913,7 +2889,9 @@ export class Renderer {
2913
2889
  // typed-array views — plus one 5-argument call in
2914
2890
  // `point-cloud-uniforms.ts` whose offset and size are compile-time
2915
2891
  // constants matching its scratch array — so the spec's
2916
- // `OperationError` preconditions are unreachable; the one
2892
+ // `OperationError` preconditions are unreachable (the partial-batch
2893
+ // upload's `createStaticGpuBuffer` also pads its whole-view payload
2894
+ // to the 4-byte multiple the spec demands, #5429); the one
2917
2895
  // `copyExternalImageToTexture` copies the glyph atlas's own
2918
2896
  // never-externally-drawn canvas at its full fixed size, so neither
2919
2897
  // `SecurityError` nor a zero-size `OperationError` can arise; and
@@ -2922,10 +2900,11 @@ export class Renderer {
2922
2900
  // draws, `finish`, `submit`, `createBindGroup`) reports failure as
2923
2901
  // an asynchronous `GPUValidationError` through the error scope, not
2924
2902
  // as a throw. The region's real healthy-device failure is
2925
- // `getOrCreatePartialBatch`'s `createBuffer({ mappedAtCreation:
2926
- // true })`, and that throws a `RangeError` — which is exactly why
2903
+ // `getOrCreatePartialBatch`'s CPU-side geometry merge running out of
2904
+ // host memory, and that throws a `RangeError` — which is exactly why
2927
2905
  // the discriminator keys on the TYPE and not on "a frame threw".
2928
2906
  this.containFrameThrow(error, 'encode');
2907
+ cancelRendererColorFrame(this);
2929
2908
  }
2930
2909
  }
2931
2910
  /**
@@ -3106,13 +3085,21 @@ export class Renderer {
3106
3085
  this.overlays.clearSection2DOverlay();
3107
3086
  }
3108
3087
  /**
3109
- * Set the colour of the overlay lines (annotation / alignment / grid) and the
3110
- * section-cut outline (RGBA, 0..1). Defaults to opaque black; theme it to keep
3111
- * lines legible on a dark canvas. The matching label colour is per-text via
3088
+ * Set the one colour vocabulary every overlay draws in (#5484): the selection
3089
+ * highlight, the section-plane preview (one accent for every axis), every
3090
+ * overlay line channel (section-cut outline, IfcAnnotation, alignment,
3091
+ * IfcGrid, DXF / LandXML) and the clash pair / overlap tints. Call this on
3092
+ * theme change and once after `init()`; the underlying GPU uniforms are
3093
+ * written only here, never per frame. Supersedes `setOverlayLineColor`
3094
+ * (deleted — the app never called it with a themed colour). See
3095
+ * `OverlayTheme` for which fields want linear-light RGBA and which want
3096
+ * sRGB-direct RGBA. The matching label colour is per-text via
3112
3097
  * `SymbolicTextInput.color` on `uploadAnnotationTexts3D`.
3113
3098
  */
3114
- setOverlayLineColor(color) {
3115
- this.overlays.setOverlayLineColor(color);
3099
+ setOverlayTheme(theme) {
3100
+ this.overlayTheme = theme;
3101
+ this.pipeline?.selectionColorUniform.update(theme.selection);
3102
+ this.overlays.setTheme(theme);
3116
3103
  }
3117
3104
  /**
3118
3105
  * Set one standalone 3D line overlay, or clear it by passing `null`.
@@ -3125,7 +3112,7 @@ export class Renderer {
3125
3112
  *
3126
3113
  * Every channel is an independent buffer with its own visibility, so
3127
3114
  * setting one leaves the other three untouched. All four share the colour
3128
- * set by {@link setOverlayLineColor}; label colour is per-text via
3115
+ * set by `Renderer.setOverlayTheme`'s `overlayLine` field; label colour is per-text via
3129
3116
  * `SymbolicTextInput.color` on `uploadAnnotationTexts3D`.
3130
3117
  *
3131
3118
  * The channels differ in exactly one way — whether they grow the scene
@@ -3145,8 +3132,10 @@ export class Renderer {
3145
3132
  /**
3146
3133
  * Show (or clear) the clash-overlap box: the wireframe AABB of a focused
3147
3134
  * clash, drawn in `color` so the overlap region reads as a distinct third
3148
- * colour next to the two glowing clash elements (#1277). Pass `null` to
3149
- * clear. `min`/`max` are world-space corners (clash works in world frame).
3135
+ * colour next to the two glowing clash elements (#1277). Omit `color` to
3136
+ * draw it in the overlay theme's `clashOverlap` and keep following it
3137
+ * across `setOverlayTheme` calls (#5490). Pass `null` to clear.
3138
+ * `min`/`max` are world-space corners (clash works in world frame).
3150
3139
  */
3151
3140
  setClashOverlapBox(box) {
3152
3141
  this.overlays.setClashOverlapBox(box);
@@ -3155,8 +3144,10 @@ export class Renderer {
3155
3144
  * Draw the focused clash's CONTACT geometry as 3D line segments — the real
3156
3145
  * shared-face polygon outlines / intersection lines, not the AABB box.
3157
3146
  * `vertices` is a flat line-list (x,y,z per endpoint, 2 endpoints per
3158
- * segment) in world frame. Pass `null` to clear. Shares the clash-box line
3159
- * buffer, so only one of this / setClashOverlapBox is shown at a time.
3147
+ * segment) in world frame. Omit `color` to follow the overlay theme's
3148
+ * `clashOverlap`, as for `setClashOverlapBox`. Pass `null` to clear. Shares
3149
+ * the clash-box line buffer, so only one of this / setClashOverlapBox is
3150
+ * shown at a time.
3160
3151
  */
3161
3152
  setClashContactLines(lines) {
3162
3153
  this.overlays.setClashContactLines(lines);
@@ -3168,7 +3159,8 @@ export class Renderer {
3168
3159
  * BIMcollab Zoom / Solibri presentation). Pass `null` to clear. Independent
3169
3160
  * of `setClashOverlapBox` / `setClashContactLines`: the caller decides
3170
3161
  * which one is current for a given clash (solid when the kernel resolved
3171
- * one, box/lines as the fallback when it didn't).
3162
+ * one, box/lines as the fallback when it didn't). Omit `color` to follow
3163
+ * the overlay theme's `clashOverlap`, as for `setClashOverlapBox`.
3172
3164
  */
3173
3165
  setClashIntersectionSolid(solid) {
3174
3166
  this.overlays.setClashIntersectionSolid(solid);
@@ -3252,30 +3244,9 @@ export class Renderer {
3252
3244
  }
3253
3245
  return this.device.getDevice();
3254
3246
  }
3255
- /**
3256
- * Capture a screenshot of the current view
3257
- * Waits for GPU work to complete and captures exactly what's displayed
3258
- * @returns PNG data URL or null if capture failed
3259
- */
3260
- async captureScreenshot() {
3261
- if (!this.device.isInitialized()) {
3262
- console.warn('[Renderer] Cannot capture screenshot: not initialized');
3263
- return null;
3264
- }
3265
- try {
3266
- // Wait for any pending GPU work to complete before capturing
3267
- // This ensures we capture the fully rendered frame
3268
- const device = this.device.getDevice();
3269
- await device.queue.onSubmittedWorkDone();
3270
- // Capture exactly what's displayed on the canvas
3271
- const dataUrl = this.canvas.toDataURL('image/png');
3272
- return dataUrl;
3273
- }
3274
- catch (error) {
3275
- console.error('[Renderer] Screenshot capture failed:', error);
3276
- return null;
3277
- }
3278
- }
3247
+ captureScreenshot() { return captureRendererScreenshot(this.device, this.canvas); }
3248
+ /** Capture the next submitted frame's bounded color pixels, not compositor state. */
3249
+ captureColorFrame() { return requestRendererColorFrame(this, !this.destroyed && !this.deviceLost && this.device.isInitialized(), () => this.requestRender()); }
3279
3250
  /**
3280
3251
  * Destroy the renderer and release all GPU resources.
3281
3252
  *
@@ -3310,6 +3281,7 @@ export class Renderer {
3310
3281
  destroy() {
3311
3282
  this.initGeneration++;
3312
3283
  this.destroyed = true;
3284
+ cancelRendererColorFrame(this);
3313
3285
  this.teardown();
3314
3286
  this.rejectReadyWaiters(rendererDestroyedError());
3315
3287
  }
@@ -3323,6 +3295,7 @@ export class Renderer {
3323
3295
  // go back to waiting. Set first: every release below is synchronous, but
3324
3296
  // the flag is what a caller holding a live reference actually reads.
3325
3297
  this.ready = false;
3298
+ cancelRendererColorFrame(this);
3326
3299
  // Scene mesh GPU buffers
3327
3300
  if (clearScene) {
3328
3301
  this.scene.clear();
@@ -3339,11 +3312,8 @@ export class Renderer {
3339
3312
  this.picker?.destroy();
3340
3313
  this.picker = null;
3341
3314
  this.pickingManager.setPicker(null);
3342
- // Post-processor uniform buffer
3343
- this.postProcessor?.destroy();
3344
- this.postProcessor = null;
3345
- this.edlPass?.destroy();
3346
- this.edlPass = null;
3315
+ this.postPasses?.destroy();
3316
+ this.postPasses = null;
3347
3317
  this.skyPass?.destroy();
3348
3318
  this.skyPass = null;
3349
3319
  this.shadowPass?.destroy();
@@ -3353,6 +3323,7 @@ export class Renderer {
3353
3323
  this.overlays.destroy();
3354
3324
  this.referenceImages.destroy();
3355
3325
  // Point cloud GPU resources
3326
+ this.pointCloudNextHandleId = this.pointCloudRenderer?.handleIds?.current() ?? this.pointCloudNextHandleId;
3356
3327
  this.pointCloudStreamEpoch++;
3357
3328
  this.pointCloudStreamEpochs.clear();
3358
3329
  this.pointCloudRenderer?.clear();