@ifc-lite/renderer 3.1.0 → 4.2.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 (429) hide show
  1. package/README.md +22 -0
  2. package/dist/aabb-edges.d.ts +10 -0
  3. package/dist/aabb-edges.d.ts.map +1 -1
  4. package/dist/aabb-edges.js +13 -0
  5. package/dist/aabb-edges.js.map +1 -1
  6. package/dist/ao-params.d.ts +85 -0
  7. package/dist/ao-params.d.ts.map +1 -0
  8. package/dist/ao-params.js +109 -0
  9. package/dist/ao-params.js.map +1 -0
  10. package/dist/ao-pass.d.ts +47 -0
  11. package/dist/ao-pass.d.ts.map +1 -0
  12. package/dist/ao-pass.js +157 -0
  13. package/dist/ao-pass.js.map +1 -0
  14. package/dist/appearance-preview.d.ts +2 -0
  15. package/dist/appearance-preview.d.ts.map +1 -1
  16. package/dist/appearance-preview.js +4 -0
  17. package/dist/appearance-preview.js.map +1 -1
  18. package/dist/camera-animation-immediate.d.ts +5 -0
  19. package/dist/camera-animation-immediate.d.ts.map +1 -0
  20. package/dist/camera-animation-immediate.js +16 -0
  21. package/dist/camera-animation-immediate.js.map +1 -0
  22. package/dist/camera-animation.d.ts +3 -9
  23. package/dist/camera-animation.d.ts.map +1 -1
  24. package/dist/camera-animation.js +14 -9
  25. package/dist/camera-animation.js.map +1 -1
  26. package/dist/camera-matrices.d.ts +3 -0
  27. package/dist/camera-matrices.d.ts.map +1 -1
  28. package/dist/camera-matrices.js +12 -1
  29. package/dist/camera-matrices.js.map +1 -1
  30. package/dist/camera-orientation.d.ts +62 -0
  31. package/dist/camera-orientation.d.ts.map +1 -0
  32. package/dist/camera-orientation.js +155 -0
  33. package/dist/camera-orientation.js.map +1 -0
  34. package/dist/camera-projection.d.ts +3 -1
  35. package/dist/camera-projection.d.ts.map +1 -1
  36. package/dist/camera-projection.js +26 -20
  37. package/dist/camera-projection.js.map +1 -1
  38. package/dist/camera-surface-zoom.d.ts +32 -0
  39. package/dist/camera-surface-zoom.d.ts.map +1 -0
  40. package/dist/camera-surface-zoom.js +37 -0
  41. package/dist/camera-surface-zoom.js.map +1 -0
  42. package/dist/camera.d.ts +32 -61
  43. package/dist/camera.d.ts.map +1 -1
  44. package/dist/camera.js +71 -167
  45. package/dist/camera.js.map +1 -1
  46. package/dist/clash-solid-pipeline.d.ts +9 -3
  47. package/dist/clash-solid-pipeline.d.ts.map +1 -1
  48. package/dist/clash-solid-pipeline.js +31 -8
  49. package/dist/clash-solid-pipeline.js.map +1 -1
  50. package/dist/constants.d.ts +1 -1
  51. package/dist/constants.d.ts.map +1 -1
  52. package/dist/constants.js +4 -3
  53. package/dist/constants.js.map +1 -1
  54. package/dist/depth-reconstruct.d.ts +43 -0
  55. package/dist/depth-reconstruct.d.ts.map +1 -0
  56. package/dist/depth-reconstruct.js +35 -0
  57. package/dist/depth-reconstruct.js.map +1 -0
  58. package/dist/device-recovery.d.ts +60 -0
  59. package/dist/device-recovery.d.ts.map +1 -0
  60. package/dist/device-recovery.js +153 -0
  61. package/dist/device-recovery.js.map +1 -0
  62. package/dist/device.d.ts +2 -0
  63. package/dist/device.d.ts.map +1 -1
  64. package/dist/device.js +13 -1
  65. package/dist/device.js.map +1 -1
  66. package/dist/environment.d.ts +26 -15
  67. package/dist/environment.d.ts.map +1 -1
  68. package/dist/environment.js +24 -13
  69. package/dist/environment.js.map +1 -1
  70. package/dist/federation-registry.d.ts +16 -0
  71. package/dist/federation-registry.d.ts.map +1 -1
  72. package/dist/federation-registry.js +80 -9
  73. package/dist/federation-registry.js.map +1 -1
  74. package/dist/gpu-static-upload.d.ts +39 -0
  75. package/dist/gpu-static-upload.d.ts.map +1 -0
  76. package/dist/gpu-static-upload.js +70 -0
  77. package/dist/gpu-static-upload.js.map +1 -0
  78. package/dist/gpu-upload-guard.d.ts +95 -0
  79. package/dist/gpu-upload-guard.d.ts.map +1 -0
  80. package/dist/gpu-upload-guard.js +88 -0
  81. package/dist/gpu-upload-guard.js.map +1 -0
  82. package/dist/index.d.ts +71 -41
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +340 -375
  85. package/dist/index.js.map +1 -1
  86. package/dist/instanced-override-color.d.ts +8 -0
  87. package/dist/instanced-override-color.d.ts.map +1 -1
  88. package/dist/instanced-override-color.js +10 -0
  89. package/dist/instanced-override-color.js.map +1 -1
  90. package/dist/instanced-render.d.ts +22 -2
  91. package/dist/instanced-render.d.ts.map +1 -1
  92. package/dist/instanced-render.js +47 -10
  93. package/dist/instanced-render.js.map +1 -1
  94. package/dist/instanced-rte.d.ts +17 -0
  95. package/dist/instanced-rte.d.ts.map +1 -0
  96. package/dist/instanced-rte.js +32 -0
  97. package/dist/instanced-rte.js.map +1 -0
  98. package/dist/merged-mesh-extract.d.ts.map +1 -1
  99. package/dist/merged-mesh-extract.js +5 -0
  100. package/dist/merged-mesh-extract.js.map +1 -1
  101. package/dist/mesh-rte-uniforms.d.ts +46 -0
  102. package/dist/mesh-rte-uniforms.d.ts.map +1 -0
  103. package/dist/mesh-rte-uniforms.js +59 -0
  104. package/dist/mesh-rte-uniforms.js.map +1 -0
  105. package/dist/model-bounds-tracker.d.ts +7 -0
  106. package/dist/model-bounds-tracker.d.ts.map +1 -1
  107. package/dist/model-bounds-tracker.js +16 -5
  108. package/dist/model-bounds-tracker.js.map +1 -1
  109. package/dist/model-translation.d.ts +40 -2
  110. package/dist/model-translation.d.ts.map +1 -1
  111. package/dist/model-translation.js +152 -17
  112. package/dist/model-translation.js.map +1 -1
  113. package/dist/pick-rte-snapshot.d.ts +7 -0
  114. package/dist/pick-rte-snapshot.d.ts.map +1 -0
  115. package/dist/pick-rte-snapshot.js +15 -0
  116. package/dist/pick-rte-snapshot.js.map +1 -0
  117. package/dist/pick-types.d.ts +17 -0
  118. package/dist/pick-types.d.ts.map +1 -0
  119. package/dist/pick-types.js +5 -0
  120. package/dist/pick-types.js.map +1 -0
  121. package/dist/pick-uniforms.d.ts +6 -0
  122. package/dist/pick-uniforms.d.ts.map +1 -1
  123. package/dist/pick-uniforms.js +22 -18
  124. package/dist/pick-uniforms.js.map +1 -1
  125. package/dist/pick-world-position.d.ts +18 -0
  126. package/dist/pick-world-position.d.ts.map +1 -0
  127. package/dist/pick-world-position.js +21 -0
  128. package/dist/pick-world-position.js.map +1 -0
  129. package/dist/picker-readbacks.d.ts +6 -0
  130. package/dist/picker-readbacks.d.ts.map +1 -0
  131. package/dist/picker-readbacks.js +20 -0
  132. package/dist/picker-readbacks.js.map +1 -0
  133. package/dist/picker-rte-uniforms.d.ts +6 -0
  134. package/dist/picker-rte-uniforms.d.ts.map +1 -0
  135. package/dist/picker-rte-uniforms.js +41 -0
  136. package/dist/picker-rte-uniforms.js.map +1 -0
  137. package/dist/picker.d.ts +6 -7
  138. package/dist/picker.d.ts.map +1 -1
  139. package/dist/picker.js +88 -115
  140. package/dist/picker.js.map +1 -1
  141. package/dist/picking-manager.d.ts +10 -1
  142. package/dist/picking-manager.d.ts.map +1 -1
  143. package/dist/picking-manager.js +57 -21
  144. package/dist/picking-manager.js.map +1 -1
  145. package/dist/pipeline.d.ts +1 -7
  146. package/dist/pipeline.d.ts.map +1 -1
  147. package/dist/pipeline.js +17 -20
  148. package/dist/pipeline.js.map +1 -1
  149. package/dist/point-picker.d.ts +5 -1
  150. package/dist/point-picker.d.ts.map +1 -1
  151. package/dist/point-picker.js +49 -9
  152. package/dist/point-picker.js.map +1 -1
  153. package/dist/pointcloud/point-cloud-handle-ids.d.ts +22 -0
  154. package/dist/pointcloud/point-cloud-handle-ids.d.ts.map +1 -0
  155. package/dist/pointcloud/point-cloud-handle-ids.js +31 -0
  156. package/dist/pointcloud/point-cloud-handle-ids.js.map +1 -0
  157. package/dist/pointcloud/point-cloud-node.d.ts +14 -6
  158. package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
  159. package/dist/pointcloud/point-cloud-node.js.map +1 -1
  160. package/dist/pointcloud/point-cloud-pick-sources.d.ts +47 -0
  161. package/dist/pointcloud/point-cloud-pick-sources.d.ts.map +1 -0
  162. package/dist/pointcloud/point-cloud-pick-sources.js +32 -0
  163. package/dist/pointcloud/point-cloud-pick-sources.js.map +1 -0
  164. package/dist/pointcloud/point-cloud-placement.d.ts.map +1 -1
  165. package/dist/pointcloud/point-cloud-placement.js +20 -5
  166. package/dist/pointcloud/point-cloud-placement.js.map +1 -1
  167. package/dist/pointcloud/point-cloud-ray-transform.d.ts +78 -4
  168. package/dist/pointcloud/point-cloud-ray-transform.d.ts.map +1 -1
  169. package/dist/pointcloud/point-cloud-ray-transform.js +31 -84
  170. package/dist/pointcloud/point-cloud-ray-transform.js.map +1 -1
  171. package/dist/pointcloud/point-cloud-renderer.d.ts +21 -19
  172. package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
  173. package/dist/pointcloud/point-cloud-renderer.js +46 -52
  174. package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
  175. package/dist/pointcloud/point-cloud-stream-lifecycle.d.ts +44 -0
  176. package/dist/pointcloud/point-cloud-stream-lifecycle.d.ts.map +1 -0
  177. package/dist/pointcloud/point-cloud-stream-lifecycle.js +44 -0
  178. package/dist/pointcloud/point-cloud-stream-lifecycle.js.map +1 -0
  179. package/dist/pointcloud/point-cloud-uniforms.d.ts +5 -0
  180. package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
  181. package/dist/pointcloud/point-cloud-uniforms.js +55 -44
  182. package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
  183. package/dist/pointcloud/point-cloud-visibility.d.ts +10 -0
  184. package/dist/pointcloud/point-cloud-visibility.d.ts.map +1 -0
  185. package/dist/pointcloud/point-cloud-visibility.js +18 -0
  186. package/dist/pointcloud/point-cloud-visibility.js.map +1 -0
  187. package/dist/pointcloud/point-pipeline.d.ts +4 -9
  188. package/dist/pointcloud/point-pipeline.d.ts.map +1 -1
  189. package/dist/pointcloud/point-pipeline.js +4 -9
  190. package/dist/pointcloud/point-pipeline.js.map +1 -1
  191. package/dist/pointcloud/point-shader.wgsl.d.ts +1 -21
  192. package/dist/pointcloud/point-shader.wgsl.d.ts.map +1 -1
  193. package/dist/pointcloud/point-shader.wgsl.js +25 -5
  194. package/dist/pointcloud/point-shader.wgsl.js.map +1 -1
  195. package/dist/post-pass-chain.d.ts +38 -0
  196. package/dist/post-pass-chain.d.ts.map +1 -0
  197. package/dist/post-pass-chain.js +99 -0
  198. package/dist/post-pass-chain.js.map +1 -0
  199. package/dist/post-processor.d.ts +12 -26
  200. package/dist/post-processor.d.ts.map +1 -1
  201. package/dist/post-processor.js +14 -72
  202. package/dist/post-processor.js.map +1 -1
  203. package/dist/raycast-engine.d.ts +7 -5
  204. package/dist/raycast-engine.d.ts.map +1 -1
  205. package/dist/raycast-engine.js +17 -20
  206. package/dist/raycast-engine.js.map +1 -1
  207. package/dist/raycast-point-cloud-query.d.ts +1 -1
  208. package/dist/raycast-point-cloud-query.d.ts.map +1 -1
  209. package/dist/reference-image-pipeline.d.ts +1 -1
  210. package/dist/reference-image-pipeline.d.ts.map +1 -1
  211. package/dist/reference-image-pipeline.js +25 -7
  212. package/dist/reference-image-pipeline.js.map +1 -1
  213. package/dist/reference-images.d.ts +3 -1
  214. package/dist/reference-images.d.ts.map +1 -1
  215. package/dist/reference-images.js +15 -5
  216. package/dist/reference-images.js.map +1 -1
  217. package/dist/relative-to-eye.d.ts +146 -0
  218. package/dist/relative-to-eye.d.ts.map +1 -0
  219. package/dist/relative-to-eye.js +326 -0
  220. package/dist/relative-to-eye.js.map +1 -0
  221. package/dist/render-section-draw.d.ts +11 -0
  222. package/dist/render-section-draw.d.ts.map +1 -1
  223. package/dist/render-section-draw.js +6 -3
  224. package/dist/render-section-draw.js.map +1 -1
  225. package/dist/renderer-color-readback.d.ts +50 -0
  226. package/dist/renderer-color-readback.d.ts.map +1 -0
  227. package/dist/renderer-color-readback.js +145 -0
  228. package/dist/renderer-color-readback.js.map +1 -0
  229. package/dist/renderer-overlays.d.ts +15 -3
  230. package/dist/renderer-overlays.d.ts.map +1 -1
  231. package/dist/renderer-overlays.js +46 -20
  232. package/dist/renderer-overlays.js.map +1 -1
  233. package/dist/renderer-screenshot.d.ts +4 -0
  234. package/dist/renderer-screenshot.d.ts.map +1 -0
  235. package/dist/renderer-screenshot.js +17 -0
  236. package/dist/renderer-screenshot.js.map +1 -0
  237. package/dist/renderer-symbolic-overlays.d.ts +5 -2
  238. package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
  239. package/dist/renderer-symbolic-overlays.js +36 -13
  240. package/dist/renderer-symbolic-overlays.js.map +1 -1
  241. package/dist/renderer-viewport.d.ts +45 -0
  242. package/dist/renderer-viewport.d.ts.map +1 -1
  243. package/dist/renderer-viewport.js +44 -0
  244. package/dist/renderer-viewport.js.map +1 -1
  245. package/dist/rte-clip-space.d.ts +12 -0
  246. package/dist/rte-clip-space.d.ts.map +1 -0
  247. package/dist/rte-clip-space.js +22 -0
  248. package/dist/rte-clip-space.js.map +1 -0
  249. package/dist/rte-frustum.d.ts +24 -0
  250. package/dist/rte-frustum.d.ts.map +1 -0
  251. package/dist/rte-frustum.js +45 -0
  252. package/dist/rte-frustum.js.map +1 -0
  253. package/dist/scene-appearance-preview.d.ts +2 -0
  254. package/dist/scene-appearance-preview.d.ts.map +1 -1
  255. package/dist/scene-appearance-preview.js +9 -0
  256. package/dist/scene-appearance-preview.js.map +1 -1
  257. package/dist/scene-authored-owner.d.ts +10 -0
  258. package/dist/scene-authored-owner.d.ts.map +1 -1
  259. package/dist/scene-authored-owner.js +44 -5
  260. package/dist/scene-authored-owner.js.map +1 -1
  261. package/dist/scene-batch-rebuild.d.ts +18 -0
  262. package/dist/scene-batch-rebuild.d.ts.map +1 -0
  263. package/dist/scene-batch-rebuild.js +49 -0
  264. package/dist/scene-batch-rebuild.js.map +1 -0
  265. package/dist/scene-batch-upload.d.ts +4 -1
  266. package/dist/scene-batch-upload.d.ts.map +1 -1
  267. package/dist/scene-batch-upload.js +31 -38
  268. package/dist/scene-batch-upload.js.map +1 -1
  269. package/dist/scene-bucket-routing.d.ts +21 -0
  270. package/dist/scene-bucket-routing.d.ts.map +1 -0
  271. package/dist/scene-bucket-routing.js +44 -0
  272. package/dist/scene-bucket-routing.js.map +1 -0
  273. package/dist/scene-contents.d.ts +8 -0
  274. package/dist/scene-contents.d.ts.map +1 -1
  275. package/dist/scene-derived-mesh-provenance.d.ts +13 -0
  276. package/dist/scene-derived-mesh-provenance.d.ts.map +1 -0
  277. package/dist/scene-derived-mesh-provenance.js +30 -0
  278. package/dist/scene-derived-mesh-provenance.js.map +1 -0
  279. package/dist/scene-device-recovery.d.ts +103 -0
  280. package/dist/scene-device-recovery.d.ts.map +1 -0
  281. package/dist/scene-device-recovery.js +295 -0
  282. package/dist/scene-device-recovery.js.map +1 -0
  283. package/dist/scene-finalize-regroup.d.ts +54 -0
  284. package/dist/scene-finalize-regroup.d.ts.map +1 -0
  285. package/dist/scene-finalize-regroup.js +136 -0
  286. package/dist/scene-finalize-regroup.js.map +1 -0
  287. package/dist/scene-geometry.d.ts +1 -0
  288. package/dist/scene-geometry.d.ts.map +1 -1
  289. package/dist/scene-geometry.js +3 -23
  290. package/dist/scene-geometry.js.map +1 -1
  291. package/dist/scene-instance-bounds.d.ts +12 -0
  292. package/dist/scene-instance-bounds.d.ts.map +1 -0
  293. package/dist/scene-instance-bounds.js +48 -0
  294. package/dist/scene-instance-bounds.js.map +1 -0
  295. package/dist/scene-instance-materialization.d.ts +4 -1
  296. package/dist/scene-instance-materialization.d.ts.map +1 -1
  297. package/dist/scene-instance-materialization.js +21 -6
  298. package/dist/scene-instance-materialization.js.map +1 -1
  299. package/dist/scene-instance-translation.d.ts +17 -0
  300. package/dist/scene-instance-translation.d.ts.map +1 -0
  301. package/dist/scene-instance-translation.js +50 -0
  302. package/dist/scene-instance-translation.js.map +1 -0
  303. package/dist/scene-instance-types.d.ts +6 -0
  304. package/dist/scene-instance-types.d.ts.map +1 -1
  305. package/dist/scene-model-translation.d.ts +13 -1
  306. package/dist/scene-model-translation.d.ts.map +1 -1
  307. package/dist/scene-model-translation.js +23 -1
  308. package/dist/scene-model-translation.js.map +1 -1
  309. package/dist/scene-precision.d.ts +16 -0
  310. package/dist/scene-precision.d.ts.map +1 -0
  311. package/dist/scene-precision.js +73 -0
  312. package/dist/scene-precision.js.map +1 -0
  313. package/dist/scene-rect-select.d.ts +11 -1
  314. package/dist/scene-rect-select.d.ts.map +1 -1
  315. package/dist/scene-rect-select.js +7 -4
  316. package/dist/scene-rect-select.js.map +1 -1
  317. package/dist/scene.d.ts +42 -39
  318. package/dist/scene.d.ts.map +1 -1
  319. package/dist/scene.js +258 -375
  320. package/dist/scene.js.map +1 -1
  321. package/dist/section-2d-lift.d.ts +10 -2
  322. package/dist/section-2d-lift.d.ts.map +1 -1
  323. package/dist/section-2d-lift.js +14 -6
  324. package/dist/section-2d-lift.js.map +1 -1
  325. package/dist/section-2d-line-buffer.d.ts +12 -4
  326. package/dist/section-2d-line-buffer.d.ts.map +1 -1
  327. package/dist/section-2d-line-buffer.js +59 -33
  328. package/dist/section-2d-line-buffer.js.map +1 -1
  329. package/dist/section-2d-overlay.d.ts +18 -72
  330. package/dist/section-2d-overlay.d.ts.map +1 -1
  331. package/dist/section-2d-overlay.js +72 -103
  332. package/dist/section-2d-overlay.js.map +1 -1
  333. package/dist/section-2d-types.d.ts +38 -0
  334. package/dist/section-2d-types.d.ts.map +1 -0
  335. package/dist/section-2d-types.js +6 -0
  336. package/dist/section-2d-types.js.map +1 -0
  337. package/dist/section-cap-pipelines.d.ts +6 -0
  338. package/dist/section-cap-pipelines.d.ts.map +1 -0
  339. package/dist/section-cap-pipelines.js +87 -0
  340. package/dist/section-cap-pipelines.js.map +1 -0
  341. package/dist/section-plane-geometry.d.ts +24 -0
  342. package/dist/section-plane-geometry.d.ts.map +1 -0
  343. package/dist/section-plane-geometry.js +93 -0
  344. package/dist/section-plane-geometry.js.map +1 -0
  345. package/dist/section-plane.d.ts +23 -24
  346. package/dist/section-plane.d.ts.map +1 -1
  347. package/dist/section-plane.js +58 -174
  348. package/dist/section-plane.js.map +1 -1
  349. package/dist/shaders/ao.wgsl.d.ts +2 -0
  350. package/dist/shaders/ao.wgsl.d.ts.map +1 -0
  351. package/dist/shaders/ao.wgsl.js +234 -0
  352. package/dist/shaders/ao.wgsl.js.map +1 -0
  353. package/dist/shaders/color-transfer.wgsl.d.ts +26 -0
  354. package/dist/shaders/color-transfer.wgsl.d.ts.map +1 -0
  355. package/dist/shaders/color-transfer.wgsl.js +53 -0
  356. package/dist/shaders/color-transfer.wgsl.js.map +1 -0
  357. package/dist/shaders/depth-reconstruct.wgsl.d.ts +16 -0
  358. package/dist/shaders/depth-reconstruct.wgsl.d.ts.map +1 -0
  359. package/dist/shaders/depth-reconstruct.wgsl.js +69 -0
  360. package/dist/shaders/depth-reconstruct.wgsl.js.map +1 -0
  361. package/dist/shaders/main-rte-shadow.wgsl.d.ts +4 -0
  362. package/dist/shaders/main-rte-shadow.wgsl.d.ts.map +1 -0
  363. package/dist/shaders/main-rte-shadow.wgsl.js +54 -0
  364. package/dist/shaders/main-rte-shadow.wgsl.js.map +1 -0
  365. package/dist/shaders/main.wgsl.d.ts +1 -6
  366. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  367. package/dist/shaders/main.wgsl.js +110 -159
  368. package/dist/shaders/main.wgsl.js.map +1 -1
  369. package/dist/shaders/relative-to-eye.wgsl.d.ts +12 -0
  370. package/dist/shaders/relative-to-eye.wgsl.d.ts.map +1 -0
  371. package/dist/shaders/relative-to-eye.wgsl.js +40 -0
  372. package/dist/shaders/relative-to-eye.wgsl.js.map +1 -0
  373. package/dist/shaders/section-2d-overlay.wgsl.d.ts +28 -12
  374. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
  375. package/dist/shaders/section-2d-overlay.wgsl.js +40 -13
  376. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
  377. package/dist/shaders/shadow.wgsl.d.ts +1 -1
  378. package/dist/shaders/shadow.wgsl.d.ts.map +1 -1
  379. package/dist/shaders/shadow.wgsl.js +25 -7
  380. package/dist/shaders/shadow.wgsl.js.map +1 -1
  381. package/dist/shaders/sky.wgsl.d.ts +1 -11
  382. package/dist/shaders/sky.wgsl.d.ts.map +1 -1
  383. package/dist/shaders/sky.wgsl.js +17 -19
  384. package/dist/shaders/sky.wgsl.js.map +1 -1
  385. package/dist/shaders/symbolic-overlay.wgsl.d.ts +2 -9
  386. package/dist/shaders/symbolic-overlay.wgsl.d.ts.map +1 -1
  387. package/dist/shaders/symbolic-overlay.wgsl.js +88 -17
  388. package/dist/shaders/symbolic-overlay.wgsl.js.map +1 -1
  389. package/dist/shaders/textured.wgsl.d.ts.map +1 -1
  390. package/dist/shaders/textured.wgsl.js +11 -9
  391. package/dist/shaders/textured.wgsl.js.map +1 -1
  392. package/dist/shadow-light-matrix.d.ts +7 -0
  393. package/dist/shadow-light-matrix.d.ts.map +1 -1
  394. package/dist/shadow-light-matrix.js +11 -0
  395. package/dist/shadow-light-matrix.js.map +1 -1
  396. package/dist/shadow-occluder-batches.d.ts +10 -0
  397. package/dist/shadow-occluder-batches.d.ts.map +1 -0
  398. package/dist/shadow-occluder-batches.js +39 -0
  399. package/dist/shadow-occluder-batches.js.map +1 -0
  400. package/dist/shadow-occluders.d.ts.map +1 -1
  401. package/dist/shadow-occluders.js +4 -0
  402. package/dist/shadow-occluders.js.map +1 -1
  403. package/dist/shadow-pass.d.ts +5 -51
  404. package/dist/shadow-pass.d.ts.map +1 -1
  405. package/dist/shadow-pass.js +49 -45
  406. package/dist/shadow-pass.js.map +1 -1
  407. package/dist/shadow-types.d.ts +39 -0
  408. package/dist/shadow-types.d.ts.map +1 -0
  409. package/dist/shadow-types.js +15 -0
  410. package/dist/shadow-types.js.map +1 -0
  411. package/dist/symbolic-overlay-geometry.d.ts +7 -0
  412. package/dist/symbolic-overlay-geometry.d.ts.map +1 -0
  413. package/dist/symbolic-overlay-geometry.js +43 -0
  414. package/dist/symbolic-overlay-geometry.js.map +1 -0
  415. package/dist/symbolic-overlay-pipelines.d.ts +24 -36
  416. package/dist/symbolic-overlay-pipelines.d.ts.map +1 -1
  417. package/dist/symbolic-overlay-pipelines.js +175 -182
  418. package/dist/symbolic-overlay-pipelines.js.map +1 -1
  419. package/dist/symbolic-text-atlas.d.ts +21 -0
  420. package/dist/symbolic-text-atlas.d.ts.map +1 -1
  421. package/dist/symbolic-text-atlas.js +29 -1
  422. package/dist/symbolic-text-atlas.js.map +1 -1
  423. package/dist/types.d.ts +20 -26
  424. package/dist/types.d.ts.map +1 -1
  425. package/dist/visual-enhancement.d.ts +23 -1
  426. package/dist/visual-enhancement.d.ts.map +1 -1
  427. package/dist/visual-enhancement.js +38 -12
  428. package/dist/visual-enhancement.js.map +1 -1
  429. package/package.json +3 -3
package/dist/scene.js CHANGED
@@ -4,9 +4,10 @@
4
4
  import { materializeInstances } from './scene-instance-materialization.js';
5
5
  import { InstanceSuppression } from './scene-instance-suppression.js';
6
6
  import { createSceneBatch } from './scene-batch-upload.js';
7
- import { createSceneAppearancePreview } from './scene-appearance-preview.js';
7
+ import { createStaticGpuBuffer } from './gpu-static-upload.js';
8
+ import { createSceneAppearancePreview, rebindSceneAppearanceAccess } from './scene-appearance-preview.js';
8
9
  import { AppearanceBuckets } from './scene-appearance-buckets.js';
9
- import { prepareSceneAuthoredOwner } from './scene-authored-owner.js';
10
+ import { AuthoredPreparationRegistry, prepareSceneAuthoredOwner } from './scene-authored-owner.js';
10
11
  import { interleaveTexturedVertices } from './textured-vertices.js';
11
12
  import { RgbaTexturePool } from './rgba-texture-pool.js';
12
13
  import { splitMeshForStreaming } from './scene-stream-split.js';
@@ -14,9 +15,10 @@ import { hostsOtherEntities } from './mesh-entity-hosting.js';
14
15
  import { BATCH_CONSTANTS } from './constants.js';
15
16
  import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, rayIntersectsBox, } from './scene-raycaster.js';
16
17
  import { selectBoundingBoxesInRect } from './scene-rect-select.js';
17
- import { splitMeshDataForBufferLimit, cachedWorldAabb, worldAabbFromPieces, destroyGpuResources } from './scene-geometry.js';
18
+ import { splitMeshDataForBufferLimit, cachedWorldAabb, worldAabbFromPieces, destroyGpuResources, topologySafeBatchOrigin } from './scene-geometry.js';
19
+ import { resolvePrecisionBucket } from './scene-bucket-routing.js';
18
20
  import { sumResidentGpuBytes } from './render-stats.js';
19
- import { composeInstancedOverrideColor } from './instanced-override-color.js';
21
+ import { composeInstancedOverrideColor, writeOriginalInstancedColors } from './instanced-override-color.js';
20
22
  import { bucketBaseKeyFor } from './chunk-grid.js';
21
23
  import { inheritedQuantization, groupOverridePieces, cloneOverrides, overlaysInvalidatedBy } from './scene-derived-batches.js';
22
24
  import { VisibilityEpochTracker } from './visibility-epoch.js';
@@ -24,11 +26,16 @@ import { isEntityVisible } from './entity-visibility.js';
24
26
  import { planInstancedGhosting } from './instanced-ghost-plan.js';
25
27
  import { selectEvictions } from './residency.js';
26
28
  import { OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
27
- import { translateSceneModel, releaseInstanceVertices, refreshTexturedBounds } from './scene-model-translation.js';
29
+ import { translateSceneModel, rotateSceneModelInstances, releaseInstanceVertices, refreshTexturedBounds } from './scene-model-translation.js';
28
30
  import { ModelTranslations } from './model-translation.js';
29
- import { extractEntityFromMergedMesh } from './merged-mesh-extract.js';
31
+ import { unionInstancedWorldAabb as unionInstanceBounds } from './scene-instance-bounds.js';
32
+ import { DerivedMeshProvenance } from './scene-derived-mesh-provenance.js';
33
+ import { rebuildSceneBatches } from './scene-batch-rebuild.js';
34
+ import { regroupStreamedBuckets } from './scene-finalize-regroup.js';
30
35
  import { dropAllPartialCaches as dropAllPartialCachesIn, dropPartialCacheForBatch as dropPartialCacheForBatchIn, retireUnusedAlphaSlots as retireUnusedAlphaSlotsIn, } from './partial-batch-cache.js';
31
36
  import { prepareInstancedRender, foldOccurrenceWorldBox, INSTANCE_STRIDE_BYTES, INSTANCE_COLOR_OFFSET, INSTANCE_FLAGS_OFFSET, INSTANCE_FLAG_SELECTED, INSTANCE_FLAG_HIDDEN, } from './instanced-render.js';
37
+ import { translateInstanceRecord } from './scene-instance-translation.js';
38
+ import { discardSceneGpuResourcesForRecovery, prepareSceneDeviceRecovery, repartitionHydratedRecoveryBucket, restoreSceneGpuResourcesAfterRecovery } from './scene-device-recovery.js';
32
39
  const EMPTY_INSTANCED_TEMPLATES = [];
33
40
  /**
34
41
  * Pure helper: compute the exclusive end index of the next flushPending()
@@ -84,6 +91,7 @@ export class Scene {
84
91
  batchedMeshes = []; // flat render array (rebuilt from buckets)
85
92
  buckets = new Map(); // bucketKey -> consolidated bucket state
86
93
  meshDataBucket = new Map(); // reverse lookup: MeshData -> owning bucket
94
+ derivedMeshProvenance = new DerivedMeshProvenance();
87
95
  modelTranslations = new ModelTranslations();
88
96
  meshDataMap = new Map(); // Map expressId -> MeshData[] (for lazy buffer creation, accumulates multiple pieces)
89
97
  boundingBoxes = new Map(); // Map expressId -> bounding box (computed lazily)
@@ -95,7 +103,9 @@ export class Scene {
95
103
  rgbaTexturePool = new RgbaTexturePool();
96
104
  appearanceController;
97
105
  appearanceBuckets;
106
+ appearanceAccessState;
98
107
  authoredGeneration = 0;
108
+ authoredPreparations = new AuthoredPreparationRegistry();
99
109
  appearanceAccess(device, pipeline) {
100
110
  return {
101
111
  meshes: () => this.texturedMeshes, data: this.meshDataMap,
@@ -135,24 +145,23 @@ export class Scene {
135
145
  },
136
146
  };
137
147
  }
148
+ bindAppearanceAccess(device, pipeline) { return this.appearanceAccessState = rebindSceneAppearanceAccess(this.appearanceAccessState, this.appearanceAccess(device, pipeline)); }
138
149
  sharedAppearanceBuckets(access) {
139
150
  return this.appearanceBuckets ??= new AppearanceBuckets(access.buckets, id => this.meshDataMap.get(id));
140
151
  }
141
152
  appearancePreview(device, pipeline) {
142
- const access = this.appearanceAccess(device, pipeline);
153
+ const access = this.bindAppearanceAccess(device, pipeline);
143
154
  return this.appearanceController ??= createSceneAppearancePreview(access, this.sharedAppearanceBuckets(access));
144
155
  }
145
- /** Place canonical native appearance source geometry in this model's live frame. */
146
156
  placeAppearanceSource(mesh) { return this.modelTranslations.placeMesh(mesh); }
147
- /** Retain model-local source coordinates when publishing a placed appearance mesh. */
148
157
  appearanceSourceMesh(mesh) { return this.modelTranslations.sourceFromPlaced(mesh); }
149
158
  /** Stage one new IFC owner with all its coloured or textured geometry parts. */
150
159
  prepareAuthoredOwner(parts, device, pipeline) {
151
- const access = this.appearanceAccess(device, pipeline), generation = this.authoredGeneration;
152
- return prepareSceneAuthoredOwner(access, this.sharedAppearanceBuckets(access), parts, () => {
160
+ const access = this.bindAppearanceAccess(device, pipeline), generation = this.authoredGeneration;
161
+ return this.authoredPreparations.track(prepareSceneAuthoredOwner(access, this.sharedAppearanceBuckets(access), parts, () => {
153
162
  if (generation !== this.authoredGeneration)
154
163
  throw new Error('The scene changed while preparing the object.');
155
- });
164
+ }));
156
165
  }
157
166
  /** Compatibility entry point for an image-backed single-part owner. */
158
167
  prepareTexturedOwner(mesh, device, pipeline) {
@@ -295,18 +304,20 @@ export class Scene {
295
304
  // Temporary fragment batches created during streaming for immediate rendering.
296
305
  // Destroyed and replaced by proper merged batches in finalizeStreaming().
297
306
  streamingFragments = [];
307
+ // Buckets that received streamed meshes since the last finalize — the only
308
+ // ones a finalize re-groups and rebuilds (#5358).
309
+ streamedBucketKeys = new Set();
298
310
  // ─── Mesh command queue ────────────────────────────────────────────
299
311
  // Decouples React state updates from GPU work. Callers push meshes
300
312
  // via queueMeshes() (instant, no GPU), and the animation loop drains
301
313
  // the queue via flushPending() with a per-frame time budget.
302
314
  meshQueue = [];
303
315
  meshQueueReadIndex = 0;
304
- // ─── GPU-resident mode ──────────────────────────────────────────────
305
- // After releaseGeometryData(), JS-side typed arrays are freed.
306
- // Only lightweight metadata is retained for operations that don't need
307
- // raw vertex data (bounding boxes, color key lookups, expressId sets).
308
316
  geometryReleased = false;
309
317
  ephemeralStreamingMode = false;
318
+ prepareDeviceRecovery() { return prepareSceneDeviceRecovery(this); }
319
+ discardGpuResourcesForRecovery() { discardSceneGpuResourcesForRecovery(this); }
320
+ restoreGpuResourcesAfterRecovery(device, pipeline) { restoreSceneGpuResourcesAfterRecovery(this, device, pipeline); }
310
321
  /**
311
322
  * Add mesh to scene
312
323
  */
@@ -329,8 +340,10 @@ export class Scene {
329
340
  /** The shared local-frame origin all batches relativize against (null until
330
341
  * the first batch is built). Per-mesh highlight/picker VBOs replicate the
331
342
  * batch's exact f32 path against this so they render bit-coincident. */
332
- getSharedFrameOrigin(modelIndex = 0) {
333
- return this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex) ?? null;
343
+ getSharedFrameOrigin(modelIndex = 0, meshData) {
344
+ const placed = meshData ? this.derivedMeshProvenance.placedSourceFor(meshData, this.modelTranslations) : undefined;
345
+ const bucket = placed ? this.meshDataBucket.get(placed) : undefined;
346
+ return bucket?.batchedMesh?.origin ?? bucket?.frameOrigin ?? this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex) ?? null;
334
347
  }
335
348
  /**
336
349
  * Enable/disable spatial chunk bucketing (issue #1682 phase 2). When set,
@@ -428,6 +441,7 @@ export class Scene {
428
441
  }
429
442
  const rebuilt = this.createBatchedMesh(bucket.meshData, bucket.meshData[0].color, device, pipeline, key);
430
443
  bucket.batchedMesh = rebuilt;
444
+ bucket.frameOrigin = rebuilt.origin;
431
445
  const idx = this.batchedMeshes.indexOf(old);
432
446
  if (idx >= 0)
433
447
  this.batchedMeshes[idx] = rebuilt;
@@ -474,7 +488,8 @@ export class Scene {
474
488
  * lattice snap in createMeshFromData (an f32 batch must NOT snap). Same rule
475
489
  * as overlay/partial batches; global flag while unbucketed. */
476
490
  isMeshQuantized(meshData) {
477
- return inheritedQuantization(this.quantizedBatchesEnabled, this.meshDataBucket.get(meshData)?.batchedMesh) !== 'off';
491
+ const source = this.derivedMeshProvenance.placedSourceFor(meshData, this.modelTranslations);
492
+ return inheritedQuantization(this.quantizedBatchesEnabled, this.meshDataBucket.get(source)?.batchedMesh) !== 'off';
478
493
  }
479
494
  /** Set (or clear) the HOST budget in bytes for bucket CPU geometry. */
480
495
  setHostResidencyBudget(bytes) {
@@ -629,10 +644,10 @@ export class Scene {
629
644
  this.meshDataBucket.set(m, bucket);
630
645
  this.addMeshData(m);
631
646
  }
632
- this.coldBuckets.delete(key);
633
647
  if (members.length > 0) {
634
- // Warm now — re-queue so the next restore tick rebuilds the GPU batch.
635
- this.residencyRestoreQueue.add(key);
648
+ this.coldBuckets.delete(key);
649
+ for (const restoredKey of repartitionHydratedRecoveryBucket(this, key, bucket, shell))
650
+ this.residencyRestoreQueue.add(restoredKey);
636
651
  }
637
652
  else {
638
653
  console.warn(`[Scene] cold restore for ${key} found no members — bucket stays a shell`);
@@ -664,6 +679,7 @@ export class Scene {
664
679
  continue;
665
680
  const rebuilt = this.createBatchedMesh(bucket.meshData, bucket.meshData[0].color, device, pipeline, bucket.key);
666
681
  bucket.batchedMesh = rebuilt;
682
+ bucket.frameOrigin = rebuilt.origin;
667
683
  const idx = this.batchedMeshes.indexOf(old);
668
684
  if (idx >= 0)
669
685
  this.batchedMeshes[idx] = rebuilt;
@@ -863,7 +879,7 @@ export class Scene {
863
879
  // this expressId so selection highlighting is per-entity, not the
864
880
  // entire merged batch.
865
881
  if (single.entityIds) {
866
- return extractEntityFromMergedMesh(single, expressId);
882
+ return this.derivedMeshProvenance.extract(single, expressId);
867
883
  }
868
884
  return single;
869
885
  }
@@ -873,7 +889,7 @@ export class Scene {
873
889
  const extracted = [];
874
890
  for (const piece of pieces) {
875
891
  if (piece.entityIds) {
876
- const ex = extractEntityFromMergedMesh(piece, expressId);
892
+ const ex = this.derivedMeshProvenance.extract(piece, expressId);
877
893
  if (ex)
878
894
  extracted.push(ex);
879
895
  }
@@ -888,6 +904,18 @@ export class Scene {
888
904
  pieces = extracted;
889
905
  // Fall through to the normal multi-piece merge below
890
906
  }
907
+ // A cross-bucket singular result keeps the historical representative merge
908
+ // (all triangles, no provenance); precise callers use the pieces accessor.
909
+ const firstOrigin = pieces[0].origin;
910
+ const firstSource = this.derivedMeshProvenance.placedSourceFor(pieces[0], this.modelTranslations);
911
+ const firstBucket = this.meshDataBucket.get(firstSource);
912
+ const sameBucket = pieces.every(piece => this.meshDataBucket.get(this.derivedMeshProvenance.placedSourceFor(piece, this.modelTranslations)) === firstBucket);
913
+ const sameFrame = pieces.every(piece => piece.origin?.[0] === firstOrigin?.[0]
914
+ && piece.origin?.[1] === firstOrigin?.[1] && piece.origin?.[2] === firstOrigin?.[2]);
915
+ const precisionPlaceable = sameBucket || (!firstBucket && sameFrame);
916
+ const mergedOrigin = precisionPlaceable
917
+ ? firstBucket?.batchedMesh?.origin ?? firstBucket?.frameOrigin ?? firstOrigin ?? [0, 0, 0]
918
+ : undefined;
891
919
  // Check if all pieces have the same color (within tolerance)
892
920
  // This handles multi-material elements like windows (frame vs glass)
893
921
  const firstColor = pieces[0].color;
@@ -921,8 +949,17 @@ export class Scene {
921
949
  let idxOffset = 0;
922
950
  let vertexOffset = 0;
923
951
  for (const piece of pieces) {
924
- // Copy positions and normals
925
- mergedPositions.set(piece.positions, posOffset);
952
+ if (mergedOrigin) {
953
+ const ox = (piece.origin?.[0] ?? 0) - mergedOrigin[0], oy = (piece.origin?.[1] ?? 0) - mergedOrigin[1], oz = (piece.origin?.[2] ?? 0) - mergedOrigin[2];
954
+ for (let i = 0; i < piece.positions.length; i += 3) {
955
+ mergedPositions[posOffset + i] = piece.positions[i] + ox;
956
+ mergedPositions[posOffset + i + 1] = piece.positions[i + 1] + oy;
957
+ mergedPositions[posOffset + i + 2] = piece.positions[i + 2] + oz;
958
+ }
959
+ }
960
+ else {
961
+ mergedPositions.set(piece.positions, posOffset);
962
+ }
926
963
  mergedNormals.set(piece.normals, posOffset);
927
964
  // Copy indices with offset
928
965
  for (let i = 0; i < piece.indices.length; i++) {
@@ -933,7 +970,7 @@ export class Scene {
933
970
  vertexOffset += piece.positions.length / 3;
934
971
  }
935
972
  // Return merged MeshData (all pieces have same color)
936
- return {
973
+ const merged = {
937
974
  expressId,
938
975
  modelIndex: pieces[0].modelIndex, // Preserve modelIndex for multi-model support
939
976
  positions: mergedPositions,
@@ -941,7 +978,15 @@ export class Scene {
941
978
  indices: mergedIndices,
942
979
  color: firstColor,
943
980
  ifcType: pieces[0].ifcType,
981
+ ...(mergedOrigin ? { origin: mergedOrigin } : {}),
944
982
  };
983
+ // The common frame is explicit above. Keep a source only when every part
984
+ // belongs to the same live bucket; an arbitrary source would give a merged
985
+ // result the wrong quantization/frame after a re-batch.
986
+ if (firstBucket && precisionPlaceable) {
987
+ this.derivedMeshProvenance.remember(merged, firstSource);
988
+ }
989
+ return merged;
945
990
  }
946
991
  /**
947
992
  * Check if MeshData exists for an expressId
@@ -1013,7 +1058,7 @@ export class Scene {
1013
1058
  const extracted = [];
1014
1059
  for (const piece of pieces) {
1015
1060
  if (piece.entityIds) {
1016
- const ex = extractEntityFromMergedMesh(piece, expressId);
1061
+ const ex = this.derivedMeshProvenance.extract(piece, expressId);
1017
1062
  if (ex)
1018
1063
  extracted.push(ex);
1019
1064
  }
@@ -1046,13 +1091,23 @@ export class Scene {
1046
1091
  * STREAMING OPTIMIZATION: During streaming, creates lightweight "fragment"
1047
1092
  * batches from ONLY the new meshes instead of re-merging all accumulated
1048
1093
  * data. This reduces streaming from O(N²) to O(N). Call finalizeStreaming()
1049
- * when streaming completes to do one O(N) full merge.
1094
+ * when streaming completes to merge the streamed buckets (only those, #5358).
1050
1095
  */
1051
1096
  appendToBatches(meshDataArray, device, pipeline, isStreaming = false) {
1097
+ meshDataArray = meshDataArray.map((mesh) => this.modelTranslations.placeMesh(mesh));
1098
+ // Validate every input before cancelling appearance work, publishing a
1099
+ // bucket, or touching GPU state. A single impossible f32 frame must leave
1100
+ // an already-valid scene usable and allow the next safe append.
1101
+ for (const meshData of meshDataArray) {
1102
+ const modelIndex = meshData.modelIndex ?? 0;
1103
+ const shared = this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex);
1104
+ if (!topologySafeBatchOrigin([meshData], undefined, shared)) {
1105
+ throw new Error('Unable to resolve a topology-safe GPU frame for mesh geometry.');
1106
+ }
1107
+ }
1052
1108
  if (this.appearanceController)
1053
1109
  for (const part of meshDataArray)
1054
1110
  this.appearanceController.cancelFor(part.expressId);
1055
- meshDataArray = meshDataArray.map((mesh) => this.modelTranslations.placeMesh(mesh));
1056
1111
  // Cache max buffer size on first call
1057
1112
  if (this.cachedMaxBufferSize === 0) {
1058
1113
  this.cachedMaxBufferSize = this.getMaxBufferSize(device);
@@ -1081,6 +1136,9 @@ export class Scene {
1081
1136
  for (const meshData of renderable) {
1082
1137
  const baseKey = this.bucketBaseKey(meshData);
1083
1138
  const bucketKey = this.resolveActiveBucket(baseKey, meshData);
1139
+ const resolvedBucket = this.buckets.get(bucketKey);
1140
+ if (resolvedBucket)
1141
+ this.meshDataBucket.set(meshData, resolvedBucket);
1084
1142
  if (retainStreamingGeometry || !isStreaming) {
1085
1143
  // Accumulate mesh data in the bucket when we need later rebatching or
1086
1144
  // CPU-side lookups. Huge-file mode intentionally skips this to keep JS
@@ -1095,16 +1153,17 @@ export class Scene {
1095
1153
  this.meshDataBucket.set(meshData, bucket);
1096
1154
  // Also store individual mesh data for visibility filtering
1097
1155
  this.addMeshData(meshData);
1098
- // Track pending keys for non-streaming rebuild only
1099
- if (!isStreaming) {
1156
+ // Non-streaming rebuilds now; streamed buckets wait for finalize.
1157
+ if (isStreaming)
1158
+ this.streamedBucketKeys.add(bucketKey);
1159
+ else
1100
1160
  this.pendingBatchKeys.add(bucketKey);
1101
- }
1102
1161
  }
1103
1162
  }
1104
1163
  if (isStreaming) {
1105
1164
  // STREAMING: Create small fragment batches from ONLY the new meshes.
1106
1165
  // Avoids the O(N²) cost of re-merging all accumulated data every batch.
1107
- // finalizeStreaming() destroys fragments and does one O(N) full merge.
1166
+ // finalizeStreaming() destroys fragments and merges the streamed buckets.
1108
1167
  // `renderable` excludes textured meshes (drawn via the textured pipeline).
1109
1168
  this.createStreamingFragments(renderable, device, pipeline);
1110
1169
  return;
@@ -1112,48 +1171,15 @@ export class Scene {
1112
1171
  // NON-STREAMING: Rebuild full batches immediately
1113
1172
  this.rebuildPendingBatches(device, pipeline);
1114
1173
  }
1115
- /**
1116
- * Rebuild all pending batches (call this after streaming completes)
1117
- *
1118
- * Each bucket key already maps to data that fits within the GPU buffer
1119
- * limit (enforced at accumulation time by resolveActiveBucket), so no
1120
- * splitting is needed here — just create one batch per key.
1121
- */
1174
+ /** Rebuild pending buckets after streaming or a geometry mutation. */
1122
1175
  rebuildPendingBatches(device, pipeline) {
1123
1176
  if (this.pendingBatchKeys.size === 0)
1124
1177
  return;
1125
- const rebuilt = [];
1126
- for (const key of this.pendingBatchKeys) {
1127
- const bucket = this.buckets.get(key);
1128
- const previousQuantized = bucket?.batchedMesh?.quantized !== undefined;
1129
- // Destroy old GPU batch if it exists
1130
- if (bucket?.batchedMesh) {
1131
- // Slot keys embed the batch id and the replacement gets a fresh one, so
1132
- // drop this batch's cached sub-batch clones first or they are stranded
1133
- // with live GPU buffers. Every other batch-destroying path already
1134
- // clears the cache (eviction per batch; finalize/release/clear wholesale).
1135
- this.dropPartialCacheForBatch(bucket.batchedMesh);
1136
- destroyGpuResources(bucket.batchedMesh);
1137
- bucket.batchedMesh = null;
1138
- }
1139
- if (!bucket || bucket.meshData.length === 0) {
1140
- // Bucket is empty — clean up
1141
- this.buckets.delete(key);
1142
- continue;
1143
- }
1144
- // Create new batch with all accumulated meshes for this bucket
1145
- const color = bucket.meshData[0].color;
1146
- const batchedMesh = this.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
1147
- bucket.batchedMesh = batchedMesh;
1148
- rebuilt.push({ bucket, previousQuantized });
1149
- }
1150
- // Rebuild the flat render array from all buckets (148 max batches — not perf critical)
1151
- this.batchedMeshes = [];
1152
- for (const bucket of this.buckets.values()) {
1153
- if (bucket.batchedMesh) {
1154
- this.batchedMeshes.push(bucket.batchedMesh);
1155
- }
1156
- }
1178
+ const rebuilt = rebuildSceneBatches({ pendingKeys: this.pendingBatchKeys, buckets: this.buckets,
1179
+ create: (meshes, color, target, renderPipeline, key) => this.createBatchedMesh(meshes, color, target, renderPipeline, key),
1180
+ dropPartial: batch => this.dropPartialCacheForBatch(batch),
1181
+ }, device, pipeline);
1182
+ this.batchedMeshes = [...this.buckets.values()].flatMap(bucket => bucket.batchedMesh ? [bucket.batchedMesh] : []);
1157
1183
  this.pendingBatchKeys.clear();
1158
1184
  // Overlays follow their depth writers only when one actually changed under
1159
1185
  // them (flip / bucket move, #4832); finalize re-applies once itself.
@@ -1486,22 +1512,17 @@ export class Scene {
1486
1512
  const cpu = this.instancedTemplateCpu[occ.templateIndex];
1487
1513
  if (!cpu)
1488
1514
  continue;
1489
- // Column-major mat4: the translation column is floats 12,13,14, i.e. bytes
1490
- // +48/+52/+56 of the occurrence's record (matches unionInstancedWorldAabb).
1491
- const dv = new DataView(cpu.instanceData);
1492
1515
  const b = occ.byteOffset;
1493
- const tx = dv.getFloat32(b + 48, true) + dx;
1494
- const ty = dv.getFloat32(b + 52, true) + dy;
1495
- const tz = dv.getFloat32(b + 56, true) + dz;
1496
- dv.setFloat32(b + 48, tx, true);
1497
- dv.setFloat32(b + 52, ty, true);
1498
- dv.setFloat32(b + 56, tz, true);
1516
+ const translated = translateInstanceRecord(cpu, b, [dx, dy, dz]);
1499
1517
  // Push only the 12 translation bytes to the GPU buffer (in place). Guarded
1500
1518
  // on the cached device so CPU-only tests still exercise the matrix math.
1501
1519
  if (device) {
1502
1520
  const gpu = this.instancedTemplates[occ.templateIndex]?.instanceBuffer;
1503
- if (gpu)
1504
- device.queue.writeBuffer(gpu, b + 48, new Float32Array([tx, ty, tz]));
1521
+ if (gpu) {
1522
+ device.queue.writeBuffer(gpu, b + 48, translated.translation);
1523
+ if (translated.legacyAnchors)
1524
+ device.queue.writeBuffer(gpu, b + 88, translated.legacyAnchors);
1525
+ }
1505
1526
  }
1506
1527
  moved = true;
1507
1528
  }
@@ -1559,21 +1580,40 @@ export class Scene {
1559
1580
  }
1560
1581
  this.meshes = kept;
1561
1582
  }
1562
- /** Absolute workspace translation, renderer Y-up metres (#4226). */
1563
- getModelTranslation(modelIndex) { return this.modelTranslations.get(modelIndex); }
1564
- setModelTranslation(modelIndex, translation) {
1583
+ /** Shared `TranslationScene` view for `setModelTranslation`/`setModelRotation`
1584
+ * (#4890) — the placement helpers in scene-model-translation.ts read scene
1585
+ * state through this narrow interface instead of the full `Scene`. */
1586
+ translationScope() {
1565
1587
  const batches = this.finalizeInProgress ? [...new Set([...this.batchedMeshes,
1566
1588
  ...[...this.buckets.values()].flatMap((bucket) => bucket.batchedMesh ? [bucket.batchedMesh] : [])])] : this.batchedMeshes;
1567
- const changed = translateSceneModel({ translations: this.modelTranslations, pieces: this.meshDataMap,
1589
+ return { translations: this.modelTranslations, pieces: this.meshDataMap,
1568
1590
  bounds: this.boundingBoxes, batches, meshes: this.meshes, overrides: this.overrideBatches, textured: this.texturedMeshes,
1569
1591
  templates: this.instancedTemplates, cpu: this.instancedTemplateCpu, occurrences: this.instancedEntityMap,
1570
1592
  device: this.instancedDevice, evictHighlight: (id) => this.evictHighlightMeshes(id, true),
1571
1593
  clearPartial: () => this.dropAllPartialCaches(),
1572
- unionBounds: (id, view, offset, min, max) => this.unionInstancedWorldAabb(id, view, offset, ...min, ...max),
1573
- }, modelIndex, translation);
1594
+ unionBounds: (id, view, offset, min, max) => this.unionInstancedWorldAabb(id, view, offset, ...min, ...max) };
1595
+ }
1596
+ dropOrphanedSuppressedBounds() {
1574
1597
  for (const id of this.instanceSuppression.suppressedIds())
1575
1598
  if (!this.meshDataMap.has(id))
1576
1599
  this.boundingBoxes.delete(id);
1600
+ }
1601
+ /** Absolute workspace translation, renderer Y-up metres (#4226). */
1602
+ getModelTranslation(modelIndex) { return this.modelTranslations.get(modelIndex); }
1603
+ setModelTranslation(modelIndex, translation) {
1604
+ const changed = translateSceneModel(this.translationScope(), modelIndex, translation);
1605
+ this.dropOrphanedSuppressedBounds();
1606
+ return changed;
1607
+ }
1608
+ /** Absolute workspace yaw, renderer Y-up radians about the render-frame
1609
+ * pivot `(pivot[0], *, pivot[2])` — the GPU-instanced half of a whole-model
1610
+ * rotation (#4890); `angle === 0` clears it. Flat/authored/batched geometry
1611
+ * is rotated by the viewer's bake, not here. */
1612
+ getModelRotation(modelIndex) { return this.modelTranslations.getYaw(modelIndex); }
1613
+ setModelRotation(modelIndex, angle, pivot) {
1614
+ const yaw = angle === 0 ? null : { angle, px: pivot[0], pz: pivot[2] };
1615
+ const changed = rotateSceneModelInstances(this.translationScope(), modelIndex, yaw);
1616
+ this.dropOrphanedSuppressedBounds();
1577
1617
  return changed;
1578
1618
  }
1579
1619
  /** Bulk variant of `translateMeshesForEntity`. */
@@ -1736,7 +1776,7 @@ export class Scene {
1736
1776
  // yielding the frame back at the TOP of the loop. The mesh-count-only chunker
1737
1777
  // could merge a 512-mesh chunk of high-poly meshes (e.g. 899 Velux roof
1738
1778
  // windows at 7624 tris each → ~3.9M tris) in ONE indivisible mergeGeometry +
1739
- // mappedAtCreation buffer copy — hundreds of ms that parked the main thread
1779
+ // GPU buffer upload copy — hundreds of ms that parked the main thread
1740
1780
  // past the 16s stream watchdog. Capping each appendToBatches by index volume
1741
1781
  // keeps every synchronous slice ≈ one bounded fragment merge (~12-15ms).
1742
1782
  const MAX_MESHES_PER_FLUSH = 4096;
@@ -1801,7 +1841,7 @@ export class Scene {
1801
1841
  // applies to cells exactly like it does to buckets.
1802
1842
  const colorGroups = new Map();
1803
1843
  for (const meshData of meshDataArray) {
1804
- const key = this.bucketBaseKey(meshData);
1844
+ const key = this.meshDataBucket.get(meshData)?.key ?? this.bucketBaseKey(meshData);
1805
1845
  for (const fragment of this.splitMeshForStreaming(meshData)) {
1806
1846
  let group = colorGroups.get(key);
1807
1847
  if (!group) {
@@ -1812,11 +1852,11 @@ export class Scene {
1812
1852
  }
1813
1853
  }
1814
1854
  // Create one fragment batch per color group (with buffer limit splitting)
1815
- for (const [, group] of colorGroups) {
1855
+ for (const [key, group] of colorGroups) {
1816
1856
  const chunks = this.splitMeshDataForBufferLimit(group, this.cachedMaxBufferSize);
1817
1857
  for (const chunk of chunks) {
1818
1858
  const color = chunk[0].color;
1819
- const fragment = this.createBatchedMesh(chunk, color, device, pipeline);
1859
+ const fragment = this.createBatchedMesh(chunk, color, device, pipeline, undefined, undefined, this.buckets.get(key)?.frameOrigin);
1820
1860
  this.batchedMeshes.push(fragment);
1821
1861
  this.streamingFragments.push(fragment);
1822
1862
  }
@@ -1826,15 +1866,14 @@ export class Scene {
1826
1866
  return splitMeshForStreaming(meshData, Scene.STREAMING_FRAGMENT_MAX_INDICES, Scene.STREAMING_FRAGMENT_MAX_VERTEX_BYTES);
1827
1867
  }
1828
1868
  /**
1829
- * Finalize streaming: destroy temporary fragment batches and do one full
1830
- * O(N) merge of all accumulated mesh data into proper batches.
1869
+ * Finalize streaming: destroy the temporary fragment batches and merge the
1870
+ * meshes streamed since the last finalize into proper batches.
1831
1871
  * Call this when streaming completes instead of rebuildPendingBatches().
1832
1872
  *
1833
- * IMPORTANT: During streaming, external code (applyColorUpdatesToMeshes)
1834
- * may mutate meshData.color in-place for deferred style/material colors.
1835
- * This means the bucket keys (computed at insertion time from the ORIGINAL
1836
- * color) no longer match the meshes' current colors. We must re-group all
1837
- * meshData by their CURRENT color to produce correct batches.
1873
+ * Only the buckets that received streamed meshes are re-grouped and rebuilt
1874
+ * (plus any key already pending); every other model's batches stay as they
1875
+ * are (#5358). See `regroupStreamedBuckets` for why the streamed meshes are
1876
+ * re-grouped by their CURRENT colour.
1838
1877
  */
1839
1878
  finalizeStreaming(device, pipeline) {
1840
1879
  if (this.streamingFragments.length === 0)
@@ -1847,6 +1886,27 @@ export class Scene {
1847
1886
  this.finalizeInProgress = false;
1848
1887
  }
1849
1888
  }
1889
+ regroupStreamed() {
1890
+ return regroupStreamedBuckets({
1891
+ buckets: this.buckets,
1892
+ meshDataBucket: this.meshDataBucket,
1893
+ activeBucketKey: this.activeBucketKey,
1894
+ coldBuckets: this.coldBuckets,
1895
+ pendingBatchKeys: this.pendingBatchKeys,
1896
+ streamedBucketKeys: this.streamedBucketKeys,
1897
+ bucketBaseKey: (md) => this.bucketBaseKey(md),
1898
+ resolveActiveBucket: (base, md) => this.resolveActiveBucket(base, md),
1899
+ createBucket: (key) => ({ key, meshData: [], batchedMesh: null, vertexBytes: 0 }),
1900
+ });
1901
+ }
1902
+ /** Free batches the finalize swap replaced, with their per-batch caches. */
1903
+ retireFinalizedBatches(batches) {
1904
+ for (const batch of batches) {
1905
+ this.dropPartialCacheForBatch(batch);
1906
+ this.lastDrawnFrame.delete(batch.id);
1907
+ destroyGpuResources(batch);
1908
+ }
1909
+ }
1850
1910
  finalizeStreamingInner(device, pipeline) {
1851
1911
  // Save references to old fragments/batches — keep them rendering
1852
1912
  // until the new proper batches are fully built (no visual gap).
@@ -1854,103 +1914,47 @@ export class Scene {
1854
1914
  const oldBatches = this.batchedMeshes;
1855
1915
  const fragmentSet = new Set(oldFragments);
1856
1916
  const oldBatchSet = new Set(oldBatches);
1857
- // Steps 1-4 detach the old drawables (streamingFragments = [],
1858
- // batchedMeshes = []) BEFORE the replacement GPU buffers exist. If a
1859
- // createBuffer fails part-way through, callers that CONTAIN the throw to
1860
- // keep the canvas alive would otherwise be left rendering a half-built —
1861
- // often empty — scene, turning a crash into a silently blank model.
1917
+ // The re-group detaches the streamed buckets BEFORE the replacement GPU
1918
+ // buffers exist. If a createBuffer fails part-way through, callers that
1919
+ // CONTAIN the throw to keep the canvas alive would otherwise be left
1920
+ // rendering a half-built scene, turning a crash into a silently blank model.
1921
+ let regroup = null;
1862
1922
  let rebuilt = false;
1863
1923
  try {
1864
1924
  this.streamingFragments = [];
1865
- // 1. Collect ALL accumulated meshData before clearing state.
1866
- // Cold buckets (issue #1682 phase 3b) hold NO meshData — their
1867
- // geometry lives on disk — so they are carried through the rebuild as
1868
- // sealed shells instead of being re-grouped (re-grouping would
1869
- // silently drop them).
1870
- const allMeshData = [];
1871
- const carriedCold = [];
1872
- for (const [key, bucket] of this.buckets) {
1873
- if (this.coldBuckets.has(key) && bucket.meshData.length === 0 && bucket.batchedMesh) {
1874
- carriedCold.push([key, bucket]);
1875
- continue;
1876
- }
1877
- for (const md of bucket.meshData)
1878
- allMeshData.push(md);
1879
- }
1880
- // 2. Clear all bucket/batch state for a clean rebuild
1881
- // NOTE: batchedMeshes keeps the OLD array reference — the renderer
1882
- // continues to draw from it until we swap in the new array below.
1883
- this.buckets.clear();
1884
- this.meshDataBucket = new Map();
1885
- this.activeBucketKey.clear();
1886
- this.lastDrawnFrame.clear();
1887
- this.residencyRestoreQueue.clear();
1888
- this.pendingBatchKeys.clear();
1889
- // Re-seat the carried cold shells in the fresh bucket map (their GPU
1890
- // shells re-enter the flat array via rebuildPendingBatches below).
1891
- for (const [key, bucket] of carriedCold)
1892
- this.buckets.set(key, bucket);
1893
- // 3. Re-group ALL meshData by their CURRENT color (and grid cell).
1894
- // meshData.color may have been mutated in-place since the mesh was
1895
- // first bucketed, so the original bucket key is stale. Re-grouping
1896
- // by current color ensures batches render with correct colors.
1897
- for (const meshData of allMeshData) {
1898
- const baseKey = this.bucketBaseKey(meshData);
1899
- const bucketKey = this.resolveActiveBucket(baseKey, meshData);
1900
- let bucket = this.buckets.get(bucketKey);
1901
- if (!bucket) {
1902
- bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
1903
- this.buckets.set(bucketKey, bucket);
1904
- }
1905
- bucket.meshData.push(meshData);
1906
- this.meshDataBucket.set(meshData, bucket);
1907
- this.pendingBatchKeys.add(bucketKey);
1908
- }
1909
- // 4. Build new proper batches into a fresh array
1925
+ regroup = this.regroupStreamed();
1926
+ // Build into a fresh array; rebuildPendingBatches republishes every
1927
+ // bucket's batch, untouched ones included.
1910
1928
  this.batchedMeshes = [];
1911
1929
  this.rebuildPendingBatches(device, pipeline);
1912
1930
  rebuilt = true;
1913
- // Cached partial (filtered-visibility) batches are keyed by their SOURCE
1914
- // batch, so they only go stale once the replacement batches are live.
1915
- // Dropping them up in step 2 destroyed their GPU resources BEFORE the
1916
- // rebuild could fail, and the rollback cannot bring them back — an active
1917
- // hide/isolate view lost its visible subset and had to recreate it
1918
- // against the very device that just failed. On the failure path they now
1919
- // survive, still matching the restored batches.
1920
- this.dropAllPartialCaches();
1921
1931
  }
1922
1932
  finally {
1923
1933
  if (!rebuilt) {
1924
- // Free ONLY what this attempt created. Carried cold shells are aliased
1925
- // into BOTH the old and the new array, so anything that was already
1926
- // live before the rebuild must be left alone — destroying it would
1927
- // leave the restored arrays pointing at dead buffers.
1934
+ // Free ONLY what this attempt created: anything live before the
1935
+ // rebuild is what the restored arrays point back at.
1928
1936
  for (const created of this.batchedMeshes) {
1929
1937
  if (!oldBatchSet.has(created) && !fragmentSet.has(created)) {
1930
1938
  destroyGpuResources(created);
1931
1939
  }
1932
1940
  }
1933
- // Step 5 never ran, so every old fragment/batch is still a live GPU
1934
- // resource: putting the arrays back restores exactly what was on
1935
- // screen.
1941
+ regroup?.rollback();
1936
1942
  this.streamingFragments = oldFragments;
1937
1943
  this.batchedMeshes = oldBatches;
1938
1944
  }
1939
1945
  }
1940
- // 5. NOW destroy old fragment/batch GPU resources (new batches are live)
1941
- for (const fragment of oldFragments)
1942
- destroyGpuResources(fragment);
1943
- for (const batch of oldBatches) {
1944
- if (!fragmentSet.has(batch))
1945
- destroyGpuResources(batch);
1946
- }
1946
+ // NOW free the fragments and the dissolved buckets' batches (the
1947
+ // replacements are live). Batches of rebuilt surviving buckets were
1948
+ // already retired by rebuildPendingBatches.
1949
+ this.retireFinalizedBatches(oldFragments);
1950
+ this.retireFinalizedBatches(regroup?.retired ?? []);
1947
1951
  this.reapplyColorOverrides(device, pipeline);
1948
1952
  }
1949
1953
  /**
1950
1954
  * Time-sliced version of finalizeStreaming.
1951
- * Re-groups mesh data and rebuilds GPU batches in small chunks,
1952
- * yielding to the event loop between chunks so orbit/pan stays responsive.
1953
- * Streaming fragments continue rendering until each new batch replaces them.
1955
+ * Rebuilds the streamed buckets' batches in small chunks, yielding to the
1956
+ * event loop between chunks so orbit/pan stays responsive. The fragments
1957
+ * keep rendering until the rebuilt batches are swapped in.
1954
1958
  *
1955
1959
  * @param device GPU device
1956
1960
  * @param pipeline Render pipeline
@@ -1982,40 +1986,32 @@ export class Scene {
1982
1986
  // spec). Every entry point that can fail — the synchronous preamble AND
1983
1987
  // each chunked continuation — must therefore run under its own try/catch
1984
1988
  // that explicitly calls `reject`, mirroring finalizeStreamingInner's
1985
- // try/finally contract: restore oldFragments/oldBatches, free only what
1986
- // this attempt created, defer dropAllPartialCaches() until success, and
1987
- // always clear finalizeInProgress.
1989
+ // try/finally contract: restore oldFragments/oldBatches and the bucket
1990
+ // map, free only what this attempt created, and always clear
1991
+ // finalizeInProgress.
1988
1992
  return new Promise((resolve, reject) => {
1989
- const newBatches = [];
1990
- // Every batch this attempt creates, paired with the bucket that now owns
1991
- // it and the value it displaced. `newBatches` alone is not enough to roll
1992
- // back: processChunk publishes each batch into `bucket.batchedMesh`, so
1993
- // freeing it without repairing the owner leaves the bucket map pointing
1994
- // at destroyed GPU resources (use-after-free on the next bucket-driven
1995
- // access). `previous` is null for the freshly built buckets and, for a
1996
- // carried COLD bucket a re-grouped meshData landed in, the shell that the
1997
- // restored `batchedMeshes` still holds — which must be put back, not
1998
- // nulled.
1999
1993
  const createdOwned = [];
2000
- let carriedCold = [];
1994
+ // Pending keys whose bucket ended up empty: deleted (and their batch
1995
+ // retired) at the swap, exactly as rebuildPendingBatches would.
1996
+ const emptied = [];
1997
+ let regroup = null;
2001
1998
  let pendingKeys = [];
2002
1999
  let keyIdx = 0;
2003
2000
  function rollback() {
2004
- // Free ONLY what this attempt created — carried cold shells and
2005
- // anything already live before the rebuild must be left alone (they
2006
- // are what the restored arrays point back at). Iterating the owned
2007
- // pairs rather than `newBatches` also skips the carried cold shells
2008
- // appended just before the swap, which this attempt did not create.
2009
- for (const { bucket, previous, batch } of createdOwned) {
2001
+ for (const { bucket, previous, previousFrameOrigin, batch } of createdOwned) {
2010
2002
  // Repair the owner BEFORE the free, so no bucket is ever observable
2011
- // holding a destroyed batch.
2012
- if (bucket.batchedMesh === batch)
2003
+ // holding a destroyed batch; its frame origin (what a later
2004
+ // createBatchedMesh seeds from) must describe the restored batch.
2005
+ if (bucket.batchedMesh === batch) {
2013
2006
  bucket.batchedMesh = previous;
2007
+ bucket.frameOrigin = previousFrameOrigin;
2008
+ }
2014
2009
  if (!oldBatchSet.has(batch) && !fragmentSet.has(batch)) {
2015
2010
  destroyGpuResources(batch);
2016
2011
  }
2017
2012
  }
2018
- scene.streamingFragments = oldFragments;
2013
+ regroup?.rollback();
2014
+ scene.streamingFragments = [...oldFragments, ...scene.streamingFragments]; // + any streamed in meanwhile
2019
2015
  scene.batchedMeshes = oldBatches;
2020
2016
  scene.finalizeInProgress = false;
2021
2017
  }
@@ -2026,43 +2022,41 @@ export class Scene {
2026
2022
  const key = pendingKeys[keyIdx++];
2027
2023
  const bucket = scene.buckets.get(key);
2028
2024
  if (!bucket || bucket.meshData.length === 0) {
2029
- scene.buckets.delete(key);
2025
+ emptied.push(key);
2030
2026
  continue;
2031
2027
  }
2032
2028
  const color = bucket.meshData[0].color;
2033
2029
  const previous = bucket.batchedMesh;
2030
+ const previousFrameOrigin = bucket.frameOrigin;
2034
2031
  const batchedMesh = scene.createBatchedMesh(bucket.meshData, color, device, pipeline, key);
2035
2032
  bucket.batchedMesh = batchedMesh;
2036
- createdOwned.push({ bucket, previous, batch: batchedMesh });
2037
- newBatches.push(batchedMesh);
2033
+ bucket.frameOrigin = batchedMesh.origin;
2034
+ createdOwned.push({ bucket, previous, previousFrameOrigin, batch: batchedMesh });
2038
2035
  // Check time budget — yield if exceeded
2039
2036
  if (performance.now() - chunkStart >= budgetMs) {
2040
2037
  setTimeout(processChunk, 0);
2041
2038
  return;
2042
2039
  }
2043
2040
  }
2044
- // Carried cold shells stay drawable-when-restored: keep them in the
2045
- // flat array (their buffers are already destroyed; the draw loop
2046
- // skips gpuResident === false and the restore path revives them).
2047
- for (const [, bucket] of carriedCold) {
2048
- if (bucket.batchedMesh)
2049
- newBatches.push(bucket.batchedMesh);
2050
- }
2051
- // All batches built — atomic swap so renderer never sees an empty array
2052
- scene.batchedMeshes = newBatches;
2053
- // Cached partial (filtered-visibility) batches are keyed by their
2054
- // SOURCE batch, so they only go stale once the replacement batches
2055
- // are live — dropping them any earlier destroys GPU resources a
2056
- // mid-rebuild failure could never get back (same rationale as
2057
- // finalizeStreamingInner).
2058
- scene.dropAllPartialCaches();
2059
- // Destroy old fragment/batch GPU resources
2060
- for (const fragment of oldFragments)
2061
- destroyGpuResources(fragment);
2062
- for (const batch of oldBatches) {
2063
- if (!fragmentSet.has(batch))
2064
- destroyGpuResources(batch);
2041
+ const retired = [...oldFragments, ...(regroup?.retired ?? [])];
2042
+ for (const { previous } of createdOwned)
2043
+ if (previous)
2044
+ retired.push(previous);
2045
+ for (const key of emptied) {
2046
+ const batch = scene.buckets.get(key)?.batchedMesh;
2047
+ if (batch)
2048
+ retired.push(batch);
2049
+ scene.buckets.delete(key);
2065
2050
  }
2051
+ // Atomic swap so the renderer never sees an empty array: every
2052
+ // bucket's live batch (untouched models included, evicted shells
2053
+ // too — the draw loop skips gpuResident === false and the restore
2054
+ // path revives them) plus any fragment streamed in meanwhile.
2055
+ scene.batchedMeshes = [
2056
+ ...[...scene.buckets.values()].flatMap((b) => (b.batchedMesh ? [b.batchedMesh] : [])),
2057
+ ...scene.streamingFragments,
2058
+ ];
2059
+ scene.retireFinalizedBatches(retired);
2066
2060
  scene.finalizeInProgress = false;
2067
2061
  scene.reapplyColorOverrides(device, pipeline); // never throws: the swap above is committed
2068
2062
  resolve();
@@ -2073,45 +2067,9 @@ export class Scene {
2073
2067
  }
2074
2068
  }
2075
2069
  try {
2076
- // --- Synchronous preamble (fast O(N) bookkeeping) ---
2070
+ // --- Synchronous preamble: re-group the streamed meshes only ---
2077
2071
  scene.streamingFragments = [];
2078
- // 1. Collect ALL accumulated meshData (cold buckets carried as sealed
2079
- // shells — see the sync finalize for the rationale)
2080
- const allMeshData = [];
2081
- for (const [key, bucket] of scene.buckets) {
2082
- if (scene.coldBuckets.has(key) && bucket.meshData.length === 0 && bucket.batchedMesh) {
2083
- carriedCold.push([key, bucket]);
2084
- continue;
2085
- }
2086
- for (const md of bucket.meshData)
2087
- allMeshData.push(md);
2088
- }
2089
- // 2. Clear bucket/batch state. dropAllPartialCaches() is deliberately
2090
- // NOT called here — see the success path above.
2091
- scene.buckets.clear();
2092
- scene.meshDataBucket = new Map();
2093
- scene.activeBucketKey.clear();
2094
- scene.lastDrawnFrame.clear();
2095
- scene.residencyRestoreQueue.clear();
2096
- scene.pendingBatchKeys.clear();
2097
- // Re-seat the carried cold shells in the fresh bucket map.
2098
- for (const [key, bucket] of carriedCold)
2099
- scene.buckets.set(key, bucket);
2100
- // 3. Re-group meshData by current color (and grid cell) — fast
2101
- for (const meshData of allMeshData) {
2102
- const baseKey = scene.bucketBaseKey(meshData);
2103
- const bucketKey = scene.resolveActiveBucket(baseKey, meshData);
2104
- let bucket = scene.buckets.get(bucketKey);
2105
- if (!bucket) {
2106
- bucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
2107
- scene.buckets.set(bucketKey, bucket);
2108
- }
2109
- bucket.meshData.push(meshData);
2110
- scene.meshDataBucket.set(meshData, bucket);
2111
- scene.pendingBatchKeys.add(bucketKey);
2112
- }
2113
- // Build new batches into a temporary array so the old batchedMeshes
2114
- // (streaming fragments) keep rendering until the swap is complete.
2072
+ regroup = scene.regroupStreamed();
2115
2073
  pendingKeys = Array.from(scene.pendingBatchKeys);
2116
2074
  scene.pendingBatchKeys.clear();
2117
2075
  }
@@ -2152,6 +2110,7 @@ export class Scene {
2152
2110
  this.coldBuckets.clear();
2153
2111
  this.dirtyBuckets.clear();
2154
2112
  this.pendingBatchKeys.clear();
2113
+ this.streamedBucketKeys.clear();
2155
2114
  this.dropAllPartialCaches();
2156
2115
  this.geometryReleased = true;
2157
2116
  this.ephemeralStreamingMode = false;
@@ -2172,7 +2131,6 @@ export class Scene {
2172
2131
  * Call this after finalizeStreaming() when all color updates have been applied.
2173
2132
  */
2174
2133
  releaseGeometryData() {
2175
- this.authoredGeneration++;
2176
2134
  if (this.geometryReleased)
2177
2135
  return;
2178
2136
  if (this.instanceSuppression.retained) {
@@ -2186,6 +2144,8 @@ export class Scene {
2186
2144
  `Call finalizeStreaming()/rebuildPendingBatches() first.`);
2187
2145
  return;
2188
2146
  }
2147
+ this.authoredGeneration++;
2148
+ this.authoredPreparations.invalidate();
2189
2149
  this.appearanceController?.forget();
2190
2150
  // 1. Precompute and cache ALL entity bounding boxes before releasing data.
2191
2151
  // Same rule as `finishEphemeralStreaming`: an entity with no usable vertex
@@ -2328,14 +2288,17 @@ export class Scene {
2328
2288
  * participate in the main buckets map).
2329
2289
  * @param quantization - 'auto' for bucket batches; derived batches pass `inheritedQuantization(source)` (#4832).
2330
2290
  */
2331
- createBatchedMesh(meshes, color, device, pipeline, bucketKey, quantization = this.quantizedBatchesEnabled ? 'auto' : 'off') {
2291
+ createBatchedMesh(meshes, color, device, pipeline, bucketKey, quantization = this.quantizedBatchesEnabled ? 'auto' : 'off', frameOrigin) {
2332
2292
  // Keep main's model-local frame and translation registration while staging
2333
2293
  // every GPU allocation before publishing Scene state.
2334
2294
  const modelIndex = meshes[0]?.modelIndex ?? 0;
2335
2295
  const offset = this.modelTranslations.get(modelIndex);
2296
+ const origin = topologySafeBatchOrigin(meshes, frameOrigin ?? this.meshDataBucket.get(meshes[0])?.batchedMesh?.origin, this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex), this.meshDataBucket.get(meshes[0])?.frameOrigin);
2297
+ if (!origin)
2298
+ throw new Error('Unable to resolve a topology-safe GPU frame for mesh geometry.');
2336
2299
  const result = createSceneBatch(meshes, color, device, pipeline, {
2337
2300
  id: this.nextBatchId, colorKey: bucketKey ?? this.colorKey(color),
2338
- origin: this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex),
2301
+ origin,
2339
2302
  quantized: quantization, lod: this.lodBuildsEnabled,
2340
2303
  }, bucketKey);
2341
2304
  this.nextBatchId++;
@@ -2365,38 +2328,15 @@ export class Scene {
2365
2328
  * Returns the bucket key to use (may be the base key or a suffixed key).
2366
2329
  */
2367
2330
  resolveActiveBucket(baseColorKey, meshData) {
2368
- let bucketKey = this.activeBucketKey.get(baseColorKey) ?? baseColorKey;
2369
- const bucket = this.buckets.get(bucketKey);
2370
- const currentBytes = bucket?.vertexBytes ?? 0;
2371
- const meshBytes = (meshData.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
2372
- // A COLD bucket is sealed (its content lives on disk and its shell's
2373
- // expressIds are the restore contract) — route new arrivals (e.g. a
2374
- // federated add landing in the same cell+colour) to an overflow
2375
- // sub-bucket instead of corrupting the sealed one.
2376
- if (bucket && this.coldBuckets.has(bucketKey)) {
2377
- bucketKey = `${baseColorKey}#${this.nextSplitId++}`;
2378
- this.activeBucketKey.set(baseColorKey, bucketKey);
2379
- let target = this.buckets.get(bucketKey);
2380
- if (!target) {
2381
- target = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
2382
- this.buckets.set(bucketKey, target);
2383
- }
2384
- target.vertexBytes += meshBytes;
2385
- return bucketKey;
2386
- }
2387
- if (currentBytes > 0 && currentBytes + meshBytes > this.cachedMaxBufferSize) {
2388
- // Overflow — create a new sub-bucket
2389
- bucketKey = `${baseColorKey}#${this.nextSplitId++}`;
2390
- this.activeBucketKey.set(baseColorKey, bucketKey);
2391
- }
2392
- // Update size tracking on the bucket (create if needed)
2393
- let targetBucket = this.buckets.get(bucketKey);
2394
- if (!targetBucket) {
2395
- targetBucket = { key: bucketKey, meshData: [], batchedMesh: null, vertexBytes: 0 };
2396
- this.buckets.set(bucketKey, targetBucket);
2397
- }
2398
- targetBucket.vertexBytes += meshBytes;
2399
- return bucketKey;
2331
+ const modelIndex = meshData.modelIndex ?? 0;
2332
+ return resolvePrecisionBucket({
2333
+ buckets: this.buckets,
2334
+ activeKeys: this.activeBucketKey,
2335
+ coldKeys: this.coldBuckets,
2336
+ maxBufferSize: this.cachedMaxBufferSize,
2337
+ sharedOrigin: this.modelTranslations.frameOrigin(this.sharedFrameOrigins.get(modelIndex) ?? null, modelIndex),
2338
+ nextSplitKey: () => `${baseColorKey}#${this.nextSplitId++}`,
2339
+ }, baseColorKey, meshData);
2400
2340
  }
2401
2341
  /**
2402
2342
  * Extract the base color key from a bucket key (strips "#N" suffix if present).
@@ -2524,7 +2464,7 @@ export class Scene {
2524
2464
  // Drawn INSTEAD of its source batch, so it inherits the source's f32/quantized
2525
2465
  // decision (#4832): the overlay built from the same source must match its depth.
2526
2466
  const color = visibleMeshData[0].color;
2527
- const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline, undefined, inheritedQuantization(this.quantizedBatchesEnabled, bucket?.batchedMesh));
2467
+ const partialBatch = this.createBatchedMesh(visibleMeshData, color, device, pipeline, undefined, inheritedQuantization(this.quantizedBatchesEnabled, bucket?.batchedMesh), bucket?.batchedMesh?.origin);
2528
2468
  // Cache it
2529
2469
  this.partialBatchCache.set(cacheKey, partialBatch);
2530
2470
  this.partialBatchCacheKeys.set(sourceBatchKey, cacheKey);
@@ -2574,7 +2514,7 @@ export class Scene {
2574
2514
  this.overlaySources.set(piece, sourceKey);
2575
2515
  const quantization = inheritedQuantization(this.quantizedBatchesEnabled, sourceBatch);
2576
2516
  for (const chunk of this.splitMeshDataForBufferLimit(meshData, maxBufferSize)) {
2577
- this.overrideBatches.push(this.createBatchedMesh(chunk, color, device, pipeline, undefined, quantization));
2517
+ this.overrideBatches.push(this.createBatchedMesh(chunk, color, device, pipeline, undefined, quantization, sourceBatch?.origin));
2578
2518
  }
2579
2519
  }
2580
2520
  }
@@ -2873,31 +2813,13 @@ export class Scene {
2873
2813
  vf[o + 5] = i * 3 + 2 < t.normals.length ? t.normals[i * 3 + 2] : 0;
2874
2814
  // vf[o + 6] (entityId lane) stays 0.
2875
2815
  }
2876
- const vertexBuffer = device.createBuffer({
2877
- size: vtx.byteLength,
2878
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
2879
- mappedAtCreation: true,
2880
- });
2881
- new Uint8Array(vertexBuffer.getMappedRange()).set(new Uint8Array(vtx));
2882
- vertexBuffer.unmap();
2883
- const indexBuffer = device.createBuffer({
2884
- size: t.indices.byteLength,
2885
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
2886
- mappedAtCreation: true,
2887
- });
2888
- new Uint32Array(indexBuffer.getMappedRange()).set(t.indices);
2889
- indexBuffer.unmap();
2816
+ const vertexBuffer = createStaticGpuBuffer(device, vtx, GPUBufferUsage.VERTEX);
2817
+ const indexBuffer = createStaticGpuBuffer(device, t.indices, GPUBufferUsage.INDEX);
2890
2818
  // instanceBuffer is already the interleaved mat4 + entityId + rgba block
2891
2819
  // (INSTANCE_STRIDE_BYTES per occurrence) from prepareInstancedRender.
2892
- this.modelTranslations.placeInstances(t.instanceBuffer, modelIndex, INSTANCE_STRIDE_BYTES);
2820
+ this.modelTranslations.placeInstances(t.instanceBuffer, modelIndex, INSTANCE_STRIDE_BYTES, t.canonicalAnchors, t.canonicalMatrixTranslations);
2893
2821
  const instSize = t.instanceCount * INSTANCE_STRIDE_BYTES;
2894
- const instanceBuffer = device.createBuffer({
2895
- size: instSize,
2896
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
2897
- mappedAtCreation: true,
2898
- });
2899
- new Uint8Array(instanceBuffer.getMappedRange()).set(new Uint8Array(t.instanceBuffer, 0, instSize));
2900
- instanceBuffer.unmap();
2822
+ const instanceBuffer = createStaticGpuBuffer(device, new Uint8Array(t.instanceBuffer, 0, instSize), GPUBufferUsage.VERTEX);
2901
2823
  // Always append: slots are stable identities, never recycled.
2902
2824
  const templateIndex = this.instancedTemplates.length;
2903
2825
  const template = {
@@ -2907,6 +2829,7 @@ export class Scene {
2907
2829
  indexCount: t.indices.length,
2908
2830
  instanceBuffer,
2909
2831
  instanceCount: t.instanceCount,
2832
+ canonicalAnchors: t.canonicalAnchors,
2910
2833
  bounds: null,
2911
2834
  maxOccRadius: 0,
2912
2835
  selectedCount: 0,
@@ -2940,6 +2863,8 @@ export class Scene {
2940
2863
  normals: t.normals,
2941
2864
  indices: t.indices,
2942
2865
  instanceData: t.instanceBuffer,
2866
+ canonicalAnchors: t.canonicalAnchors,
2867
+ canonicalMatrixTranslations: t.canonicalMatrixTranslations,
2943
2868
  localMin: [lmnx, lmny, lmnz],
2944
2869
  localMax: [lmxx, lmxy, lmxz],
2945
2870
  };
@@ -3010,51 +2935,10 @@ export class Scene {
3010
2935
  this.instancedVisibilityDirty = true;
3011
2936
  this.instancedGhostDirty = true;
3012
2937
  }
3013
- /** Transform a template's local AABB by an occurrence's column-major mat4 (read
3014
- * from the packed instance record at `matOffset`) and union the world box into
3015
- * boundingBoxes[eid]. Returns the occurrence's world box so the caller can also
3016
- * fold it into the template's cull metadata. */
3017
2938
  unionInstancedWorldAabb(eid, dv, matOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz) {
3018
- const m0 = dv.getFloat32(matOffset + 0, true), m1 = dv.getFloat32(matOffset + 4, true), m2 = dv.getFloat32(matOffset + 8, true);
3019
- const m4 = dv.getFloat32(matOffset + 16, true), m5 = dv.getFloat32(matOffset + 20, true), m6 = dv.getFloat32(matOffset + 24, true);
3020
- const m8 = dv.getFloat32(matOffset + 32, true), m9 = dv.getFloat32(matOffset + 36, true), m10 = dv.getFloat32(matOffset + 40, true);
3021
- const m12 = dv.getFloat32(matOffset + 48, true), m13 = dv.getFloat32(matOffset + 52, true), m14 = dv.getFloat32(matOffset + 56, true);
3022
- let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
3023
- for (let c = 0; c < 8; c++) {
3024
- const x = (c & 1) ? lmxx : lmnx, y = (c & 2) ? lmxy : lmny, z = (c & 4) ? lmxz : lmnz;
3025
- const wx = m0 * x + m4 * y + m8 * z + m12;
3026
- const wy = m1 * x + m5 * y + m9 * z + m13;
3027
- const wz = m2 * x + m6 * y + m10 * z + m14;
3028
- if (wx < minX)
3029
- minX = wx;
3030
- if (wy < minY)
3031
- minY = wy;
3032
- if (wz < minZ)
3033
- minZ = wz;
3034
- if (wx > maxX)
3035
- maxX = wx;
3036
- if (wy > maxY)
3037
- maxY = wy;
3038
- if (wz > maxZ)
3039
- maxZ = wz;
3040
- }
3041
- const existing = this.boundingBoxes.get(eid);
3042
- if (existing) {
3043
- existing.min.x = Math.min(existing.min.x, minX);
3044
- existing.min.y = Math.min(existing.min.y, minY);
3045
- existing.min.z = Math.min(existing.min.z, minZ);
3046
- existing.max.x = Math.max(existing.max.x, maxX);
3047
- existing.max.y = Math.max(existing.max.y, maxY);
3048
- existing.max.z = Math.max(existing.max.z, maxZ);
3049
- }
3050
- else {
3051
- this.boundingBoxes.set(eid, { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } });
3052
- }
3053
- return { minX, minY, minZ, maxX, maxY, maxZ };
2939
+ return unionInstanceBounds(this.boundingBoxes, eid, dv, matOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz);
3054
2940
  }
3055
- /** True if `expressId` is a GPU-instanced occurrence (lives only in the instanced
3056
- * shard, not the flat meshDataMap). CPU consumers use this to decide whether to
3057
- * fall back to the instanced accessors below. */
2941
+ /** True when `expressId` has a GPU-instanced occurrence. */
3058
2942
  isInstancedEntity(expressId) {
3059
2943
  return this.instancedEntityMap.has(expressId);
3060
2944
  }
@@ -3317,22 +3201,17 @@ export class Scene {
3317
3201
  this.restoreInstanceColor(device, eid);
3318
3202
  }
3319
3203
  for (const eid of toFade) {
3320
- const base = this.instancedOverrideColors?.get(eid) ?? this.originalInstanceColor(eid);
3321
- if (!base)
3322
- continue;
3323
- this.writeInstanceColor(device, eid, [base[0], base[1], base[2], ghostAlpha]);
3204
+ const override = this.instancedOverrideColors?.get(eid);
3205
+ if (override)
3206
+ this.writeInstanceColor(device, eid, [override[0], override[1], override[2], ghostAlpha]);
3207
+ else
3208
+ this.writeOriginalInstanceColors(device, eid, ghostAlpha);
3324
3209
  }
3325
3210
  this.instancedGhosted = next;
3326
3211
  this.lastGhostAlpha = ghostAlpha;
3327
3212
  this.instancedGhostDirty = false;
3328
3213
  this.instancedGhostTransparent = next.size > 0 && ghostAlpha < OPAQUE_ALPHA_CUTOFF;
3329
3214
  }
3330
- /** The colour an occurrence was uploaded with, before any override or ghost. */
3331
- originalInstanceColor(eid) {
3332
- const locs = this.instancedEntityMap.get(eid);
3333
- const first = locs?.[0];
3334
- return first ? first.originalColor : null;
3335
- }
3336
3215
  /** Write the combined flag lane (selected | hidden) for every occurrence of `eid`
3337
3216
  * (shares one u32 lane at INSTANCE_FLAGS_OFFSET, so both bits are folded here).
3338
3217
  * #4382: for the item-filtered eid, selected narrows per-occurrence to
@@ -3374,6 +3253,7 @@ export class Scene {
3374
3253
  device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_COLOR_OFFSET, new Float32Array(loc.originalColor));
3375
3254
  }
3376
3255
  }
3256
+ writeOriginalInstanceColors(device, eid, alpha) { writeOriginalInstancedColors(device, this.instancedEntityMap.get(eid) ?? [], this.instancedTemplates, INSTANCE_COLOR_OFFSET, alpha); }
3377
3257
  /**
3378
3258
  * Build a textured mesh (#961): interleave position+normal+entityId+uv into one
3379
3259
  * vertex buffer, upload the decoded RGBA8 texture, create a sampler honouring
@@ -3558,6 +3438,7 @@ export class Scene {
3558
3438
  */
3559
3439
  clearFlatGeometry() {
3560
3440
  this.authoredGeneration++;
3441
+ this.authoredPreparations.invalidate();
3561
3442
  this.instanceSuppression.restore();
3562
3443
  this.appearanceController?.forget();
3563
3444
  this.clearFlatBuffers();
@@ -3566,6 +3447,7 @@ export class Scene {
3566
3447
  * owners keep their original-instance history. Full reset remains separate. */
3567
3448
  clearFlatGeometryForRebuild(geometry, models, sourceGeometry = geometry) {
3568
3449
  this.authoredGeneration++;
3450
+ this.authoredPreparations.invalidate();
3569
3451
  const retained = this.appearanceController?.prepareRebuild(sourceGeometry, models) ?? new Set();
3570
3452
  const discarded = this.appearanceController?.discardedForRebuild(retained) ?? [];
3571
3453
  // A discarded converted owner must not resurrect its obsolete type instance.
@@ -3623,6 +3505,7 @@ export class Scene {
3623
3505
  this.dirtyBuckets.clear();
3624
3506
  this.cachedMaxBufferSize = 0;
3625
3507
  this.pendingBatchKeys.clear();
3508
+ this.streamedBucketKeys.clear();
3626
3509
  this.meshQueue = [];
3627
3510
  this.meshQueueReadIndex = 0;
3628
3511
  this.geometryReleased = false;
@@ -3921,7 +3804,7 @@ export class Scene {
3921
3804
  * covered: their world AABBs are registered in `boundingBoxes` when the
3922
3805
  * instanced shard is built.
3923
3806
  */
3924
- selectRect(x0, y0, x1, y1, viewportWidth, viewportHeight, viewProj, hiddenIds, isolatedIds, clip) {
3807
+ selectRect(x0, y0, x1, y1, viewportWidth, viewportHeight, viewProj, hiddenIds, isolatedIds, clip, rte) {
3925
3808
  // After release the cache is already the complete set; before it, boxes are
3926
3809
  // computed lazily, so make sure every entity that still has mesh data has
3927
3810
  // one. Same authoritative id set pick() uses, so colour-fused fillers whose
@@ -3939,7 +3822,7 @@ export class Scene {
3939
3822
  this.getEntityBoundingBox(expressId);
3940
3823
  }
3941
3824
  }
3942
- return selectBoundingBoxesInRect(this.boundingBoxes, viewProj, { x0, y0, x1, y1 }, viewportWidth, viewportHeight, hiddenIds, isolatedIds, clip);
3825
+ return selectBoundingBoxesInRect(this.boundingBoxes, viewProj, { x0, y0, x1, y1 }, viewportWidth, viewportHeight, hiddenIds, isolatedIds, clip, rte);
3943
3826
  }
3944
3827
  }
3945
3828
  //# sourceMappingURL=scene.js.map