@fps-games/engine 0.0.1-beta.2 → 0.0.1-beta.21

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 (333) hide show
  1. package/dist/asset-resource.d.ts +45 -0
  2. package/dist/asset-resource.d.ts.map +1 -0
  3. package/dist/asset-resource.js +147 -0
  4. package/dist/asset-resource.js.map +1 -0
  5. package/dist/babylon/camera.d.ts +25 -0
  6. package/dist/babylon/camera.d.ts.map +1 -0
  7. package/dist/babylon/camera.js +149 -0
  8. package/dist/babylon/camera.js.map +1 -0
  9. package/dist/babylon/frame-pump.d.ts +27 -0
  10. package/dist/babylon/frame-pump.d.ts.map +1 -0
  11. package/dist/babylon/frame-pump.js +90 -0
  12. package/dist/babylon/frame-pump.js.map +1 -0
  13. package/dist/babylon/game-world-plugin-lifecycle.d.ts +26 -0
  14. package/dist/babylon/game-world-plugin-lifecycle.d.ts.map +1 -0
  15. package/dist/babylon/game-world-plugin-lifecycle.js +257 -0
  16. package/dist/babylon/game-world-plugin-lifecycle.js.map +1 -0
  17. package/dist/babylon/lighting-controller.d.ts.map +1 -1
  18. package/dist/babylon/lighting-controller.js +162 -16
  19. package/dist/babylon/lighting-controller.js.map +1 -1
  20. package/dist/babylon/plugin-contract.d.ts +15 -0
  21. package/dist/babylon/plugin-contract.d.ts.map +1 -0
  22. package/dist/babylon/plugin-contract.js +10 -0
  23. package/dist/babylon/plugin-contract.js.map +1 -0
  24. package/dist/babylon/renderer/asset-container-instantiation.d.ts +39 -0
  25. package/dist/babylon/renderer/asset-container-instantiation.d.ts.map +1 -0
  26. package/dist/babylon/renderer/asset-container-instantiation.js +287 -0
  27. package/dist/babylon/renderer/asset-container-instantiation.js.map +1 -0
  28. package/dist/babylon/renderer/brdf-texture-settlement.d.ts +7 -0
  29. package/dist/babylon/renderer/brdf-texture-settlement.d.ts.map +1 -0
  30. package/dist/babylon/renderer/brdf-texture-settlement.js +29 -0
  31. package/dist/babylon/renderer/brdf-texture-settlement.js.map +1 -0
  32. package/dist/babylon/renderer/environment/environment-texture-controller.d.ts +7 -0
  33. package/dist/babylon/renderer/environment/environment-texture-controller.d.ts.map +1 -0
  34. package/dist/babylon/renderer/environment/environment-texture-controller.js +358 -0
  35. package/dist/babylon/renderer/environment/environment-texture-controller.js.map +1 -0
  36. package/dist/babylon/renderer/environment/index.d.ts +4 -0
  37. package/dist/babylon/renderer/environment/index.d.ts.map +1 -0
  38. package/dist/babylon/renderer/environment/index.js +4 -0
  39. package/dist/babylon/renderer/environment/index.js.map +1 -0
  40. package/dist/babylon/renderer/environment/profile.d.ts +3 -0
  41. package/dist/babylon/renderer/environment/profile.d.ts.map +1 -0
  42. package/dist/babylon/renderer/environment/profile.js +27 -0
  43. package/dist/babylon/renderer/environment/profile.js.map +1 -0
  44. package/dist/babylon/renderer/environment/types.d.ts +24 -0
  45. package/dist/babylon/renderer/environment/types.d.ts.map +1 -0
  46. package/dist/babylon/renderer/environment/types.js +6 -0
  47. package/dist/babylon/renderer/environment/types.js.map +1 -0
  48. package/dist/babylon/renderer/index.d.ts +26 -0
  49. package/dist/babylon/renderer/index.d.ts.map +1 -0
  50. package/dist/babylon/renderer/index.js +26 -0
  51. package/dist/babylon/renderer/index.js.map +1 -0
  52. package/dist/babylon/renderer/lighting/directional-light-angles.d.ts +17 -0
  53. package/dist/babylon/renderer/lighting/directional-light-angles.d.ts.map +1 -0
  54. package/dist/babylon/renderer/lighting/directional-light-angles.js +67 -0
  55. package/dist/babylon/renderer/lighting/directional-light-angles.js.map +1 -0
  56. package/dist/babylon/renderer/material-owner-candidates.d.ts +21 -0
  57. package/dist/babylon/renderer/material-owner-candidates.d.ts.map +1 -0
  58. package/dist/babylon/renderer/material-owner-candidates.js +217 -0
  59. package/dist/babylon/renderer/material-owner-candidates.js.map +1 -0
  60. package/dist/babylon/renderer/materials/index.d.ts +3 -0
  61. package/dist/babylon/renderer/materials/index.d.ts.map +1 -0
  62. package/dist/babylon/renderer/materials/index.js +3 -0
  63. package/dist/babylon/renderer/materials/index.js.map +1 -0
  64. package/dist/babylon/renderer/materials/material-runtime.d.ts +29 -0
  65. package/dist/babylon/renderer/materials/material-runtime.d.ts.map +1 -0
  66. package/dist/babylon/renderer/materials/material-runtime.js +660 -0
  67. package/dist/babylon/renderer/materials/material-runtime.js.map +1 -0
  68. package/dist/babylon/renderer/materials/material-slot-owner.d.ts +42 -0
  69. package/dist/babylon/renderer/materials/material-slot-owner.d.ts.map +1 -0
  70. package/dist/babylon/renderer/materials/material-slot-owner.js +318 -0
  71. package/dist/babylon/renderer/materials/material-slot-owner.js.map +1 -0
  72. package/dist/babylon/renderer/outlines/babylon-default/apply.d.ts +4 -0
  73. package/dist/babylon/renderer/outlines/babylon-default/apply.d.ts.map +1 -0
  74. package/dist/babylon/renderer/outlines/babylon-default/apply.js +45 -0
  75. package/dist/babylon/renderer/outlines/babylon-default/apply.js.map +1 -0
  76. package/dist/babylon/renderer/outlines/babylon-default/index.d.ts +3 -0
  77. package/dist/babylon/renderer/outlines/babylon-default/index.d.ts.map +1 -0
  78. package/dist/babylon/renderer/outlines/babylon-default/index.js +2 -0
  79. package/dist/babylon/renderer/outlines/babylon-default/index.js.map +1 -0
  80. package/dist/babylon/renderer/outlines/babylon-default/types.d.ts +7 -0
  81. package/dist/babylon/renderer/outlines/babylon-default/types.d.ts.map +1 -0
  82. package/dist/babylon/renderer/outlines/babylon-default/types.js +2 -0
  83. package/dist/babylon/renderer/outlines/babylon-default/types.js.map +1 -0
  84. package/dist/babylon/renderer/outlines/index.d.ts +4 -0
  85. package/dist/babylon/renderer/outlines/index.d.ts.map +1 -0
  86. package/dist/babylon/renderer/outlines/index.js +3 -0
  87. package/dist/babylon/renderer/outlines/index.js.map +1 -0
  88. package/dist/babylon/renderer/outlines/runtime-override.d.ts +36 -0
  89. package/dist/babylon/renderer/outlines/runtime-override.d.ts.map +1 -0
  90. package/dist/babylon/renderer/outlines/runtime-override.js +131 -0
  91. package/dist/babylon/renderer/outlines/runtime-override.js.map +1 -0
  92. package/dist/babylon/renderer/outlines/types.d.ts +7 -0
  93. package/dist/babylon/renderer/outlines/types.d.ts.map +1 -0
  94. package/dist/babylon/renderer/outlines/types.js +2 -0
  95. package/dist/babylon/renderer/outlines/types.js.map +1 -0
  96. package/dist/babylon/renderer/placement/index.d.ts +2 -0
  97. package/dist/babylon/renderer/placement/index.d.ts.map +1 -0
  98. package/dist/babylon/renderer/placement/index.js +2 -0
  99. package/dist/babylon/renderer/placement/index.js.map +1 -0
  100. package/dist/babylon/renderer/placement/runtime-drop-position.d.ts +24 -0
  101. package/dist/babylon/renderer/placement/runtime-drop-position.d.ts.map +1 -0
  102. package/dist/babylon/renderer/placement/runtime-drop-position.js +63 -0
  103. package/dist/babylon/renderer/placement/runtime-drop-position.js.map +1 -0
  104. package/dist/babylon/renderer/post-process/babylon-default-pipeline.d.ts +7 -0
  105. package/dist/babylon/renderer/post-process/babylon-default-pipeline.d.ts.map +1 -0
  106. package/dist/babylon/renderer/post-process/babylon-default-pipeline.js +301 -0
  107. package/dist/babylon/renderer/post-process/babylon-default-pipeline.js.map +1 -0
  108. package/dist/babylon/renderer/post-process/index.d.ts +5 -0
  109. package/dist/babylon/renderer/post-process/index.d.ts.map +1 -0
  110. package/dist/babylon/renderer/post-process/index.js +5 -0
  111. package/dist/babylon/renderer/post-process/index.js.map +1 -0
  112. package/dist/babylon/renderer/post-process/profile.d.ts +5 -0
  113. package/dist/babylon/renderer/post-process/profile.d.ts.map +1 -0
  114. package/dist/babylon/renderer/post-process/profile.js +155 -0
  115. package/dist/babylon/renderer/post-process/profile.js.map +1 -0
  116. package/dist/babylon/renderer/post-process/types.d.ts +81 -0
  117. package/dist/babylon/renderer/post-process/types.d.ts.map +1 -0
  118. package/dist/babylon/renderer/post-process/types.js +24 -0
  119. package/dist/babylon/renderer/post-process/types.js.map +1 -0
  120. package/dist/babylon/renderer/post-process/volume-stack.d.ts +4 -0
  121. package/dist/babylon/renderer/post-process/volume-stack.d.ts.map +1 -0
  122. package/dist/babylon/renderer/post-process/volume-stack.js +105 -0
  123. package/dist/babylon/renderer/post-process/volume-stack.js.map +1 -0
  124. package/dist/babylon/renderer/rendering/image-node.d.ts +35 -0
  125. package/dist/babylon/renderer/rendering/image-node.d.ts.map +1 -0
  126. package/dist/babylon/renderer/rendering/image-node.js +39 -0
  127. package/dist/babylon/renderer/rendering/image-node.js.map +1 -0
  128. package/dist/babylon/renderer/rendering/index.d.ts +3 -0
  129. package/dist/babylon/renderer/rendering/index.d.ts.map +1 -0
  130. package/dist/babylon/renderer/rendering/index.js +3 -0
  131. package/dist/babylon/renderer/rendering/index.js.map +1 -0
  132. package/dist/babylon/renderer/rendering/node-rendering.d.ts +19 -0
  133. package/dist/babylon/renderer/rendering/node-rendering.d.ts.map +1 -0
  134. package/dist/babylon/renderer/rendering/node-rendering.js +66 -0
  135. package/dist/babylon/renderer/rendering/node-rendering.js.map +1 -0
  136. package/dist/babylon/renderer/shadows/blob-shadow-shaders.d.ts +5 -0
  137. package/dist/babylon/renderer/shadows/blob-shadow-shaders.d.ts.map +1 -0
  138. package/dist/babylon/renderer/shadows/blob-shadow-shaders.js +46 -0
  139. package/dist/babylon/renderer/shadows/blob-shadow-shaders.js.map +1 -0
  140. package/dist/babylon/renderer/shadows/blob-shadow-system.d.ts +5 -0
  141. package/dist/babylon/renderer/shadows/blob-shadow-system.d.ts.map +1 -0
  142. package/dist/babylon/renderer/shadows/blob-shadow-system.js +402 -0
  143. package/dist/babylon/renderer/shadows/blob-shadow-system.js.map +1 -0
  144. package/dist/babylon/renderer/shadows/dynamic-shadow-system.d.ts +87 -0
  145. package/dist/babylon/renderer/shadows/dynamic-shadow-system.d.ts.map +1 -0
  146. package/dist/babylon/renderer/shadows/dynamic-shadow-system.js +663 -0
  147. package/dist/babylon/renderer/shadows/dynamic-shadow-system.js.map +1 -0
  148. package/dist/babylon/renderer/shadows/planar-shadow-shaders.d.ts +7 -0
  149. package/dist/babylon/renderer/shadows/planar-shadow-shaders.d.ts.map +1 -0
  150. package/dist/babylon/renderer/shadows/planar-shadow-shaders.js +316 -0
  151. package/dist/babylon/renderer/shadows/planar-shadow-shaders.js.map +1 -0
  152. package/dist/babylon/renderer/shadows/planar-shadow-system.d.ts +5 -0
  153. package/dist/babylon/renderer/shadows/planar-shadow-system.d.ts.map +1 -0
  154. package/dist/babylon/renderer/shadows/planar-shadow-system.js +1562 -0
  155. package/dist/babylon/renderer/shadows/planar-shadow-system.js.map +1 -0
  156. package/dist/babylon/renderer/shadows/shadow-generator-shaders.d.ts +14 -0
  157. package/dist/babylon/renderer/shadows/shadow-generator-shaders.d.ts.map +1 -0
  158. package/dist/babylon/renderer/shadows/shadow-generator-shaders.js +14 -0
  159. package/dist/babylon/renderer/shadows/shadow-generator-shaders.js.map +1 -0
  160. package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.d.ts +69 -0
  161. package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.d.ts.map +1 -0
  162. package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.js +847 -0
  163. package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.js.map +1 -0
  164. package/dist/babylon/renderer/shadows/shadow-map-experiment-system.d.ts +148 -0
  165. package/dist/babylon/renderer/shadows/shadow-map-experiment-system.d.ts.map +1 -0
  166. package/dist/babylon/renderer/shadows/shadow-map-experiment-system.js +4535 -0
  167. package/dist/babylon/renderer/shadows/shadow-map-experiment-system.js.map +1 -0
  168. package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.d.ts +4 -0
  169. package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.d.ts.map +1 -0
  170. package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.js +645 -0
  171. package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.js.map +1 -0
  172. package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.d.ts +61 -0
  173. package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.d.ts.map +1 -0
  174. package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.js +382 -0
  175. package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.js.map +1 -0
  176. package/dist/babylon/renderer/shadows/static-projected-shadow-bake.d.ts +47 -0
  177. package/dist/babylon/renderer/shadows/static-projected-shadow-bake.d.ts.map +1 -0
  178. package/dist/babylon/renderer/shadows/static-projected-shadow-bake.js +285 -0
  179. package/dist/babylon/renderer/shadows/static-projected-shadow-bake.js.map +1 -0
  180. package/dist/babylon/renderer/shadows/static-projected-shadow-baker.d.ts +88 -0
  181. package/dist/babylon/renderer/shadows/static-projected-shadow-baker.d.ts.map +1 -0
  182. package/dist/babylon/renderer/shadows/static-projected-shadow-baker.js +830 -0
  183. package/dist/babylon/renderer/shadows/static-projected-shadow-baker.js.map +1 -0
  184. package/dist/babylon/renderer/shadows/static-projected-shadow-options.d.ts +3 -0
  185. package/dist/babylon/renderer/shadows/static-projected-shadow-options.d.ts.map +1 -0
  186. package/dist/babylon/renderer/shadows/static-projected-shadow-options.js +60 -0
  187. package/dist/babylon/renderer/shadows/static-projected-shadow-options.js.map +1 -0
  188. package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.d.ts +27 -0
  189. package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.d.ts.map +1 -0
  190. package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.js +220 -0
  191. package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.js.map +1 -0
  192. package/dist/babylon/renderer/shadows/types.d.ts +421 -0
  193. package/dist/babylon/renderer/shadows/types.d.ts.map +1 -0
  194. package/dist/babylon/renderer/shadows/types.js +127 -0
  195. package/dist/babylon/renderer/shadows/types.js.map +1 -0
  196. package/dist/babylon/renderer/world-rendering-controller.d.ts +29 -0
  197. package/dist/babylon/renderer/world-rendering-controller.d.ts.map +1 -0
  198. package/dist/babylon/renderer/world-rendering-controller.js +183 -0
  199. package/dist/babylon/renderer/world-rendering-controller.js.map +1 -0
  200. package/dist/babylon/renderer-controller.d.ts +93 -0
  201. package/dist/babylon/renderer-controller.d.ts.map +1 -0
  202. package/dist/babylon/renderer-controller.js +706 -0
  203. package/dist/babylon/renderer-controller.js.map +1 -0
  204. package/dist/babylon/scene-renderer.d.ts +20 -0
  205. package/dist/babylon/scene-renderer.d.ts.map +1 -0
  206. package/dist/babylon/scene-renderer.js +58 -0
  207. package/dist/babylon/scene-renderer.js.map +1 -0
  208. package/dist/babylon.d.ts +7 -0
  209. package/dist/babylon.d.ts.map +1 -1
  210. package/dist/babylon.js +7 -0
  211. package/dist/babylon.js.map +1 -1
  212. package/dist/build-info.json +4 -4
  213. package/dist/camera.d.ts +25 -0
  214. package/dist/camera.d.ts.map +1 -0
  215. package/dist/camera.js +68 -0
  216. package/dist/camera.js.map +1 -0
  217. package/dist/compiled-scene-data.d.ts +23 -0
  218. package/dist/compiled-scene-data.d.ts.map +1 -0
  219. package/dist/compiled-scene-data.js +100 -0
  220. package/dist/compiled-scene-data.js.map +1 -0
  221. package/dist/config.d.ts +9 -3
  222. package/dist/config.d.ts.map +1 -1
  223. package/dist/config.js +21 -0
  224. package/dist/config.js.map +1 -1
  225. package/dist/frame.d.ts +32 -0
  226. package/dist/frame.d.ts.map +1 -0
  227. package/dist/frame.js +54 -0
  228. package/dist/frame.js.map +1 -0
  229. package/dist/ground-decal.d.ts +22 -0
  230. package/dist/ground-decal.d.ts.map +1 -0
  231. package/dist/ground-decal.js +48 -0
  232. package/dist/ground-decal.js.map +1 -0
  233. package/dist/index.d.ts +16 -1
  234. package/dist/index.d.ts.map +1 -1
  235. package/dist/index.js +16 -1
  236. package/dist/index.js.map +1 -1
  237. package/dist/lighting.d.ts +13 -5
  238. package/dist/lighting.d.ts.map +1 -1
  239. package/dist/lighting.js +11 -5
  240. package/dist/lighting.js.map +1 -1
  241. package/dist/material-owner.d.ts +141 -0
  242. package/dist/material-owner.d.ts.map +1 -0
  243. package/dist/material-owner.js +552 -0
  244. package/dist/material-owner.js.map +1 -0
  245. package/dist/material-profile.d.ts +62 -0
  246. package/dist/material-profile.d.ts.map +1 -0
  247. package/dist/material-profile.js +132 -0
  248. package/dist/material-profile.js.map +1 -0
  249. package/dist/rendering/index.d.ts +14 -0
  250. package/dist/rendering/index.d.ts.map +1 -0
  251. package/dist/rendering/index.js +12 -0
  252. package/dist/rendering/index.js.map +1 -0
  253. package/dist/rendering/rendering-profile-types.d.ts +322 -0
  254. package/dist/rendering/rendering-profile-types.d.ts.map +1 -0
  255. package/dist/rendering/rendering-profile-types.js +2 -0
  256. package/dist/rendering/rendering-profile-types.js.map +1 -0
  257. package/dist/rendering/rendering-profile-validation.d.ts +2 -0
  258. package/dist/rendering/rendering-profile-validation.d.ts.map +1 -0
  259. package/dist/rendering/rendering-profile-validation.js +364 -0
  260. package/dist/rendering/rendering-profile-validation.js.map +1 -0
  261. package/dist/rendering/rendering-profile-values.d.ts +19 -0
  262. package/dist/rendering/rendering-profile-values.d.ts.map +1 -0
  263. package/dist/rendering/rendering-profile-values.js +79 -0
  264. package/dist/rendering/rendering-profile-values.js.map +1 -0
  265. package/dist/rendering/rendering-profile.d.ts +15 -0
  266. package/dist/rendering/rendering-profile.d.ts.map +1 -0
  267. package/dist/rendering/rendering-profile.js +442 -0
  268. package/dist/rendering/rendering-profile.js.map +1 -0
  269. package/dist/rendering/shadow-map-experiment-depth.d.ts +63 -0
  270. package/dist/rendering/shadow-map-experiment-depth.d.ts.map +1 -0
  271. package/dist/rendering/shadow-map-experiment-depth.js +186 -0
  272. package/dist/rendering/shadow-map-experiment-depth.js.map +1 -0
  273. package/dist/rendering/shadow-map-experiment-lifecycle.d.ts +94 -0
  274. package/dist/rendering/shadow-map-experiment-lifecycle.d.ts.map +1 -0
  275. package/dist/rendering/shadow-map-experiment-lifecycle.js +407 -0
  276. package/dist/rendering/shadow-map-experiment-lifecycle.js.map +1 -0
  277. package/dist/rendering/shadow-map-experiment.d.ts +189 -0
  278. package/dist/rendering/shadow-map-experiment.d.ts.map +1 -0
  279. package/dist/rendering/shadow-map-experiment.js +549 -0
  280. package/dist/rendering/shadow-map-experiment.js.map +1 -0
  281. package/dist/rendering/shadow-quality.d.ts +5 -0
  282. package/dist/rendering/shadow-quality.d.ts.map +1 -0
  283. package/dist/rendering/shadow-quality.js +83 -0
  284. package/dist/rendering/shadow-quality.js.map +1 -0
  285. package/dist/rendering/shadow-runtime.d.ts +59 -0
  286. package/dist/rendering/shadow-runtime.d.ts.map +1 -0
  287. package/dist/rendering/shadow-runtime.js +378 -0
  288. package/dist/rendering/shadow-runtime.js.map +1 -0
  289. package/dist/rendering/shadow-settings.d.ts +15 -0
  290. package/dist/rendering/shadow-settings.d.ts.map +1 -0
  291. package/dist/rendering/shadow-settings.js +129 -0
  292. package/dist/rendering/shadow-settings.js.map +1 -0
  293. package/dist/rendering/shadow-types.d.ts +99 -0
  294. package/dist/rendering/shadow-types.d.ts.map +1 -0
  295. package/dist/rendering/shadow-types.js +5 -0
  296. package/dist/rendering/shadow-types.js.map +1 -0
  297. package/dist/rendering/shadow-validation.d.ts +3 -0
  298. package/dist/rendering/shadow-validation.d.ts.map +1 -0
  299. package/dist/rendering/shadow-validation.js +166 -0
  300. package/dist/rendering/shadow-validation.js.map +1 -0
  301. package/dist/rendering/value.d.ts +2 -0
  302. package/dist/rendering/value.d.ts.map +1 -0
  303. package/dist/rendering/value.js +6 -0
  304. package/dist/rendering/value.js.map +1 -0
  305. package/dist/runtime-data-token.d.ts +4 -0
  306. package/dist/runtime-data-token.d.ts.map +1 -0
  307. package/dist/runtime-data-token.js +4 -0
  308. package/dist/runtime-data-token.js.map +1 -0
  309. package/dist/runtime-data-types.d.ts +83 -0
  310. package/dist/runtime-data-types.d.ts.map +1 -0
  311. package/dist/runtime-data-types.js +2 -0
  312. package/dist/runtime-data-types.js.map +1 -0
  313. package/dist/runtime-data.d.ts +18 -0
  314. package/dist/runtime-data.d.ts.map +1 -0
  315. package/dist/runtime-data.js +301 -0
  316. package/dist/runtime-data.js.map +1 -0
  317. package/dist/runtime-scene-resolvers.d.ts +33 -0
  318. package/dist/runtime-scene-resolvers.d.ts.map +1 -0
  319. package/dist/runtime-scene-resolvers.js +36 -0
  320. package/dist/runtime-scene-resolvers.js.map +1 -0
  321. package/dist/scene-artifact-types.d.ts +385 -0
  322. package/dist/scene-artifact-types.d.ts.map +1 -0
  323. package/dist/scene-artifact-types.js +2 -0
  324. package/dist/scene-artifact-types.js.map +1 -0
  325. package/dist/scene-artifact-validation.d.ts +20 -0
  326. package/dist/scene-artifact-validation.d.ts.map +1 -0
  327. package/dist/scene-artifact-validation.js +1402 -0
  328. package/dist/scene-artifact-validation.js.map +1 -0
  329. package/dist/static-shadow-artifact.d.ts +131 -0
  330. package/dist/static-shadow-artifact.d.ts.map +1 -0
  331. package/dist/static-shadow-artifact.js +327 -0
  332. package/dist/static-shadow-artifact.js.map +1 -0
  333. package/package.json +51 -1
@@ -0,0 +1,4535 @@
1
+ import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js';
2
+ import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator.js';
3
+ import './shadow-generator-shaders.js';
4
+ import { Matrix, Vector3 } from '@babylonjs/core/Maths/math.vector.js';
5
+ import { createShadowMapExperimentPlanSnapshot, createShadowMapExperimentPlanFingerprint, resolveShadowMapExperimentAppearance, createShadowMapExperimentActivityScheduler, createShadowMapExperimentDepthRefreshScheduler, createShadowMapExperimentInfluenceCullingState, createShadowMapExperimentRegistry, diffShadowMapExperimentPlans, shadowMapExperimentDepthLayerForUpdateClass, } from '../../../rendering/index.js';
6
+ import { createBabylonShadowMapExperimentDepthBackend, createBabylonShadowMapExperimentSingleDepthBackend, readBabylonShadowMapExperimentAllocationCleanup, } from './shadow-map-experiment-depth-backend.js';
7
+ import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent.js';
8
+ export function readBabylonShadowMapExperimentSystemCleanup(error) {
9
+ if (!error || (typeof error !== 'object' && typeof error !== 'function'))
10
+ return null;
11
+ const cleanup = error.shadowMapExperimentSystemCleanup;
12
+ return typeof cleanup === 'function' ? cleanup : null;
13
+ }
14
+ export function readBabylonRuntimeShadowObjectsRecoveryOwner(error) {
15
+ if (!error || (typeof error !== 'object' && typeof error !== 'function'))
16
+ return null;
17
+ const owner = error.runtimeShadowObjectsRecoveryOwner;
18
+ return owner && typeof owner === 'object' && typeof owner.dispose === 'function'
19
+ ? owner
20
+ : null;
21
+ }
22
+ export function createBabylonShadowMapExperimentSystem(scene, initialPlan = null, options = {}) {
23
+ let disposed = false;
24
+ let disposing = false;
25
+ let sourcePlan = null;
26
+ let plan = null;
27
+ let runtimeOverlayRevision = 0;
28
+ let runtimeIncrementalMutationCount = 0;
29
+ let effectivePlanMaterializationCount = 0;
30
+ let materializedEffectivePlan = null;
31
+ let lightState = null;
32
+ let generator = null;
33
+ let depthBackend = null;
34
+ let registry = null;
35
+ let previousSceneShadowsEnabled = null;
36
+ let refreshCount = 0;
37
+ let lastRefreshReasons = Object.freeze([]);
38
+ const activityScheduler = createShadowMapExperimentActivityScheduler();
39
+ const depthRefreshScheduler = createShadowMapExperimentDepthRefreshScheduler();
40
+ const influenceCulling = createShadowMapExperimentInfluenceCullingState(0.25);
41
+ const candidateLayerByMesh = new Map();
42
+ const candidateMeshCounts = { static: 0, dynamic: 0 };
43
+ const meshDataCacheByMesh = new Map();
44
+ const conservativeCasterMeshes = new Set();
45
+ const pendingMeshObserverCleanups = new Map();
46
+ const pendingMeshObserverRestorations = new Map();
47
+ const pendingMeshRegistrationRollbacks = new Set();
48
+ const pendingSceneMeshRegistrations = new Set();
49
+ const pendingMeshUnregistrations = new Set();
50
+ const pendingRenderableMembershipRetries = new Map();
51
+ const pendingExplicitVisibility = new Map();
52
+ const pendingTransformMembershipRetries = new Set();
53
+ const activeCasterEntityIds = new Set();
54
+ const activeDeformationSourceCountByEntity = new Map();
55
+ const pendingFinalDeformationEntityIds = new Set();
56
+ const pendingDeformationInvalidations = new Map();
57
+ const pendingMembershipInvalidationLayers = new Set();
58
+ const pendingCompositeRefreshReasons = new Set();
59
+ const fallbackWarnings = new Set();
60
+ let pendingBackendAllocationCleanup = null;
61
+ let pendingFallbackReason = null;
62
+ let pendingCoverageMembershipRebuild = false;
63
+ // Scene-fit bounds are read only at an explicit membership invalidation
64
+ // boundary, never by an otherwise idle render frame.
65
+ let sceneFitCoverageInvalidated = options.coverageMode === 'scene-fit';
66
+ let syncingSceneFitMembership = false;
67
+ let cullingStable = false;
68
+ let lastCullingInvalidationReason = null;
69
+ let effectiveCoverageCenter = null;
70
+ let effectiveCoverageExtent = null;
71
+ let effectiveCoverageNear = null;
72
+ let effectiveCoverageFar = null;
73
+ const objectByEntity = new Map();
74
+ const objectByRenderable = new Map();
75
+ const meshesByRenderable = new Map();
76
+ const bindings = new Map();
77
+ const automaticActivityCandidateBindings = new Map();
78
+ const receiverStates = new Map();
79
+ const receiverTargetByMesh = new Map();
80
+ // The target, rather than the requesting mesh, owns a failed receiver write:
81
+ // several source meshes can resolve to the same Babylon receiver.
82
+ const pendingReceiverRestorations = new Map();
83
+ const receiverRestorationAttemptsThisFlush = new Set();
84
+ const diagnostics = [];
85
+ const diagnosticKeys = new Set();
86
+ const runtimeRegistrations = new Map();
87
+ const sceneOwnedMeshes = new Set(scene.meshes);
88
+ const runtimeOwnedMeshes = new Set();
89
+ const explicitRuntimeActivityEntityIds = new Set();
90
+ const automaticAnimationGroups = new Map();
91
+ const automaticAnimationGroupsByResolvedTarget = new Map();
92
+ const automaticAnimationGroupsByResolvedTargetAncestor = new Map();
93
+ const automaticAnimationGroupsByResolvedTargetRenderable = new Map();
94
+ // Failed dormant source or full-detach cleanup is the only automatic-
95
+ // activity work allowed to wake the frame hook. Normal playback is event-driven.
96
+ const pendingAutomaticAnimationGroupSourceCleanups = new Set();
97
+ const pendingAutomaticAnimationGroupDetachCleanups = new Set();
98
+ const pendingAutomaticAnimationGroupReconciliations = new Set();
99
+ const pendingAutomaticAnimationGroupAttachments = new Set();
100
+ let pendingAutomaticActivityCandidateTransition = null;
101
+ let automaticActivityCandidateRevision = 0;
102
+ let automaticActivityTargetResolutionCount = 0;
103
+ let suppressAutomaticActivityCandidateRevision = false;
104
+ let stagingPlanCandidateTransitions = false;
105
+ let applyAutomaticActivityCandidateTransition = () => false;
106
+ let flushPendingAutomaticActivityCandidateTransition = () => { };
107
+ let flushingRefresh = false;
108
+ let pendingPlanRollbackRecovery = null;
109
+ let activatingRecoveryPlan = null;
110
+ let allowingRecoveryActivityRegistration = false;
111
+ const bumpAutomaticActivityCandidateRevision = () => {
112
+ if (suppressAutomaticActivityCandidateRevision)
113
+ return;
114
+ automaticActivityCandidateRevision += 1;
115
+ };
116
+ const isAutomaticActivityCandidateObject = (object) => object?.cast === true
117
+ && object.updateClass !== 'static'
118
+ && !explicitRuntimeActivityEntityIds.has(object.entityId);
119
+ const haveSameAutomaticActivityCandidates = (left, right, leftCandidateObjects = left.objects) => {
120
+ // Incremental runtime mutations intentionally leave `plan.objects` as the
121
+ // authored operational snapshot and advance only its revision. An
122
+ // authored update, on the other hand, arrives as a fully composed Plan.
123
+ // Compare the same candidate topology without forcing the operational
124
+ // snapshot to materialize every runtime overlay object.
125
+ const signature = (objects) => objects
126
+ .filter(isAutomaticActivityCandidateObject)
127
+ .flatMap(object => object.renderableIds.map(renderableId => `${object.entityId}\u0000${renderableId}\u0000${object.updateClass}`))
128
+ .sort();
129
+ const leftCandidates = signature(leftCandidateObjects);
130
+ const rightCandidates = signature(right.objects);
131
+ return leftCandidates.length === rightCandidates.length
132
+ && leftCandidates.every((candidate, index) => candidate === rightCandidates[index]);
133
+ };
134
+ const updateAutomaticActivityCandidateBinding = (candidateView, binding, object) => {
135
+ // Candidate topology describes which registered meshes can ever provide
136
+ // automatic activity. Runtime visibility only controls caster membership
137
+ // and entity eligibility; it must not invalidate every AnimationGroup
138
+ // binding when a runtime state temporarily hides a model.
139
+ const shouldTrack = isAutomaticActivityCandidateObject(object);
140
+ const existing = candidateView.get(binding.mesh);
141
+ const tracked = existing !== undefined;
142
+ // A mesh can keep the same Babylon identity while its plan renderable is
143
+ // reassigned to another entity. Treat that as a topology transition too:
144
+ // the automatic source belongs to the entity, not just to the mesh.
145
+ if (shouldTrack && tracked
146
+ && existing.entityId === binding.entityId
147
+ && existing.renderableId === binding.renderableId)
148
+ return false;
149
+ if (!shouldTrack && !tracked)
150
+ return false;
151
+ if (shouldTrack)
152
+ candidateView.set(binding.mesh, binding);
153
+ else
154
+ candidateView.delete(binding.mesh);
155
+ return true;
156
+ };
157
+ const syncAutomaticActivityCandidateBindingsForRenderable = (renderableId) => {
158
+ const object = objectByRenderable.get(renderableId);
159
+ const renderableBindings = [...(meshesByRenderable.get(renderableId) ?? [])]
160
+ .flatMap(mesh => {
161
+ const binding = bindings.get(mesh);
162
+ return binding ? [binding] : [];
163
+ });
164
+ const alreadySynchronized = renderableBindings.every(binding => {
165
+ const existing = automaticActivityCandidateBindings.get(binding.mesh);
166
+ if (!isAutomaticActivityCandidateObject(object))
167
+ return existing === undefined;
168
+ return existing?.entityId === binding.entityId
169
+ && existing.renderableId === binding.renderableId;
170
+ });
171
+ if (alreadySynchronized)
172
+ return false;
173
+ const nextCandidates = new Map(automaticActivityCandidateBindings);
174
+ let changed = false;
175
+ for (const binding of renderableBindings) {
176
+ changed = updateAutomaticActivityCandidateBinding(nextCandidates, binding, object) || changed;
177
+ }
178
+ if (changed && !applyAutomaticActivityCandidateTransition(nextCandidates)) {
179
+ throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
180
+ }
181
+ return changed;
182
+ };
183
+ const assertUsable = (allowDisposing = false) => {
184
+ if (disposed || (disposing && !allowDisposing))
185
+ throw new Error('shadowMapExperiment.systemDisposed');
186
+ };
187
+ const assertNoPlanRollbackRecovery = () => {
188
+ if (pendingPlanRollbackRecovery) {
189
+ throw new Error('shadowMapExperiment.planRollbackRecoveryPending');
190
+ }
191
+ };
192
+ const rememberDiagnostic = (code, message, identity = {}) => {
193
+ const key = `${code}\u0000${identity.entityId ?? ''}\u0000${identity.renderableId ?? ''}`;
194
+ if (diagnosticKeys.has(key))
195
+ return;
196
+ diagnosticKeys.add(key);
197
+ diagnostics.push({ code, message, ...identity });
198
+ };
199
+ const requireRuntimeProfile = (basePlan, behaviorProfileId, entityId) => {
200
+ if (!basePlan || basePlan.schemaVersion !== 3) {
201
+ const code = 'shadowMapExperiment.runtimeProfileCatalogUnavailable';
202
+ rememberDiagnostic(code, 'Plan v3 behavior profile catalog is required for runtime shadow registration.', { entityId });
203
+ throw new Error(`${code}:plan-v${basePlan?.schemaVersion ?? 'none'}`);
204
+ }
205
+ const profile = basePlan.behaviorProfiles[behaviorProfileId];
206
+ if (!profile) {
207
+ const code = 'shadowMapExperiment.runtimeBehaviorProfileUnknown';
208
+ rememberDiagnostic(code, `Unknown runtime behavior profile: ${behaviorProfileId}`, { entityId });
209
+ throw new Error(`${code}:${behaviorProfileId}`);
210
+ }
211
+ if (profile.updateClass === 'auto') {
212
+ const code = 'shadowMapExperiment.runtimeBehaviorProfileAutoUnsupported';
213
+ rememberDiagnostic(code, `Runtime behavior profile ${behaviorProfileId} must declare a deterministic updateClass.`, { entityId });
214
+ throw new Error(`${code}:${behaviorProfileId}`);
215
+ }
216
+ return profile;
217
+ };
218
+ const composeEffectivePlan = (basePlan, records, overlayRevision) => {
219
+ if (!basePlan) {
220
+ if (records.length > 0)
221
+ throw new Error('shadowMapExperiment.runtimePlanUnavailable');
222
+ return null;
223
+ }
224
+ if (records.length === 0)
225
+ return basePlan;
226
+ const runtimeObjects = createRuntimePlanObjects(basePlan, records);
227
+ return Object.freeze({
228
+ ...basePlan,
229
+ revision: basePlan.revision + overlayRevision,
230
+ objects: Object.freeze([...basePlan.objects, ...runtimeObjects]
231
+ .sort((left, right) => left.entityId.localeCompare(right.entityId))),
232
+ });
233
+ };
234
+ const createRuntimePlanObjects = (basePlan, records) => Object.freeze(records.map(record => {
235
+ const profile = requireRuntimeProfile(basePlan, record.behaviorProfileId, record.entityId);
236
+ if (record.activities.length > 0 && (!profile.cast || profile.updateClass === 'static')) {
237
+ throw new Error(`shadowMapExperiment.runtimeActivityProfileIneligible:${record.behaviorProfileId}`);
238
+ }
239
+ return Object.freeze({
240
+ entityId: record.entityId,
241
+ renderableIds: Object.freeze(record.renderables.map(renderable => renderable.renderableId).sort()),
242
+ cast: profile.cast,
243
+ receive: profile.receive,
244
+ updateClass: profile.updateClass,
245
+ behaviorProfileId: record.behaviorProfileId,
246
+ });
247
+ }));
248
+ const invalidateMaterializedEffectivePlan = () => {
249
+ materializedEffectivePlan = null;
250
+ };
251
+ const readMaterializedEffectivePlan = () => {
252
+ if (!sourcePlan || !plan)
253
+ return null;
254
+ if (!materializedEffectivePlan) {
255
+ materializedEffectivePlan = composeEffectivePlan(sourcePlan, [...runtimeRegistrations.values()].filter(record => !record.disposed), runtimeOverlayRevision);
256
+ effectivePlanMaterializationCount += 1;
257
+ }
258
+ return materializedEffectivePlan;
259
+ };
260
+ const updateOperationalPlanRevision = (overlayRevision) => {
261
+ if (!plan || !sourcePlan)
262
+ return;
263
+ plan = Object.freeze({
264
+ ...plan,
265
+ revision: sourcePlan.revision + overlayRevision,
266
+ });
267
+ invalidateMaterializedEffectivePlan();
268
+ };
269
+ const requestRefreshFrame = (reason) => {
270
+ if (disposed || disposing || flushingRefresh || !options.requestFrame)
271
+ return;
272
+ try {
273
+ options.requestFrame(reason);
274
+ }
275
+ catch (error) {
276
+ const code = 'shadowMapExperiment.frameRequestFailed';
277
+ if (diagnosticKeys.has(code))
278
+ return;
279
+ diagnosticKeys.add(code);
280
+ diagnostics.push({
281
+ code,
282
+ message: `Shadow refresh frame request failed: ${error instanceof Error ? error.message : String(error)}`,
283
+ });
284
+ }
285
+ };
286
+ const enqueueRefresh = (reason) => {
287
+ activityScheduler.markDirty(reason);
288
+ requestRefreshFrame(`shadow-map-experiment-${reason}`);
289
+ };
290
+ const markDepthDirty = (layer, reason) => {
291
+ depthRefreshScheduler.markDirty(layer, reason);
292
+ requestRefreshFrame(`shadow-map-experiment-${reason}`);
293
+ };
294
+ const markAllDepthDirty = (reason) => {
295
+ depthRefreshScheduler.markAllDirty(reason);
296
+ requestRefreshFrame(`shadow-map-experiment-${reason}`);
297
+ };
298
+ const markCompositeDepthDirty = (reason) => {
299
+ depthRefreshScheduler.markCompositeDirty(reason);
300
+ requestRefreshFrame(`shadow-map-experiment-${reason}`);
301
+ };
302
+ const invalidateSceneFitBounds = (renderableIds = null, refreshLocalBounds = false) => {
303
+ if (options.coverageMode !== 'scene-fit')
304
+ return;
305
+ const selected = renderableIds ? new Set(renderableIds) : null;
306
+ if (refreshLocalBounds) {
307
+ for (const binding of bindings.values()) {
308
+ if (!selected || selected.has(binding.renderableId)) {
309
+ binding.sceneFitLocalBounds = readMeshLocalBounds(binding.mesh);
310
+ }
311
+ }
312
+ }
313
+ sceneFitCoverageInvalidated = true;
314
+ };
315
+ const syncSceneFitCoverage = (force = false) => {
316
+ if (!plan?.enabled || !lightState || options.coverageMode !== 'scene-fit')
317
+ return;
318
+ if (!force && !sceneFitCoverageInvalidated)
319
+ return;
320
+ const sceneBounds = collectActiveCasterWorldBounds(bindings, registry);
321
+ if (!sceneBounds) {
322
+ sceneFitCoverageInvalidated = false;
323
+ return;
324
+ }
325
+ const nextCoverage = createSceneFitCoverage(plan, sceneBounds);
326
+ const currentCenter = effectiveCoverageCenter ?? plan.generator.bounds.center;
327
+ const currentExtent = effectiveCoverageExtent ?? plan.generator.bounds.extent;
328
+ const currentNear = effectiveCoverageNear ?? plan.generator.bounds.near;
329
+ const currentFar = effectiveCoverageFar ?? plan.generator.bounds.far;
330
+ if (!force
331
+ && areTuple3Equal(currentCenter, nextCoverage.center)
332
+ && areTuple3Equal(currentExtent, nextCoverage.extent)
333
+ && currentNear === nextCoverage.near
334
+ && currentFar === nextCoverage.far) {
335
+ sceneFitCoverageInvalidated = false;
336
+ return;
337
+ }
338
+ effectiveCoverageCenter = nextCoverage.center;
339
+ effectiveCoverageExtent = nextCoverage.extent;
340
+ effectiveCoverageNear = nextCoverage.near;
341
+ effectiveCoverageFar = nextCoverage.far;
342
+ configureLight(lightState.light, plan, nextCoverage);
343
+ pendingCoverageMembershipRebuild = true;
344
+ sceneFitCoverageInvalidated = false;
345
+ lastCullingInvalidationReason = 'scene-fit-coverage-changed';
346
+ markAllDepthDirty('camera-influence-region-changed');
347
+ };
348
+ const handleCompositorLifecycleEvent = (event) => {
349
+ if (disposed || disposing)
350
+ return;
351
+ if (event === 'ready')
352
+ markAllDepthDirty('compositor-ready');
353
+ else
354
+ requestRefreshFrame(`shadow-map-experiment-compositor-${event}`);
355
+ };
356
+ const routeDirtyReason = (reason) => {
357
+ if (isDynamicDirtyReason(reason)) {
358
+ depthRefreshScheduler.markDirty('dynamic', reason);
359
+ return;
360
+ }
361
+ if (isStaticDirtyReason(reason)) {
362
+ depthRefreshScheduler.markDirty('static', reason);
363
+ return;
364
+ }
365
+ depthRefreshScheduler.markAllDirty(reason);
366
+ };
367
+ const syncCandidateCounts = () => {
368
+ depthBackend?.setCandidateCounts(candidateMeshCounts.static, candidateMeshCounts.dynamic);
369
+ };
370
+ const setCandidateLayer = (mesh, nextLayer) => {
371
+ const previousLayer = candidateLayerByMesh.get(mesh) ?? null;
372
+ if (previousLayer === nextLayer)
373
+ return;
374
+ if (previousLayer)
375
+ candidateMeshCounts[previousLayer] -= 1;
376
+ if (nextLayer) {
377
+ candidateLayerByMesh.set(mesh, nextLayer);
378
+ candidateMeshCounts[nextLayer] += 1;
379
+ }
380
+ else {
381
+ candidateLayerByMesh.delete(mesh);
382
+ }
383
+ };
384
+ const warnForFallback = (reason) => {
385
+ if (fallbackWarnings.has(reason))
386
+ return;
387
+ fallbackWarnings.add(reason);
388
+ console.warn(`[shadow-map-experiment] split backend unavailable; using single ShadowMap (${reason})`);
389
+ };
390
+ const flushRefresh = () => {
391
+ if (disposed || disposing)
392
+ return;
393
+ let postFlushFrameReason = null;
394
+ flushingRefresh = true;
395
+ receiverRestorationAttemptsThisFlush.clear();
396
+ try {
397
+ // Recovery owns the old transaction until cleanup and (when applicable)
398
+ // reactivation have both completed. Do one bounded attempt and leave all
399
+ // ordinary queues untouched until that ownership is released.
400
+ if (pendingPlanRollbackRecovery) {
401
+ flushPendingPlanRollbackRecovery();
402
+ return;
403
+ }
404
+ flushPendingReceiverRestorations();
405
+ // No animation group is polled during normal rendering. A frame retry
406
+ // exists only while a previous observer/source teardown is incomplete.
407
+ flushPendingAutomaticAnimationGroupDetachCleanups();
408
+ if (pendingAutomaticActivityCandidateTransition
409
+ && pendingMeshUnregistrations.size === 0
410
+ && pendingRenderableMembershipRetries.size === 0) {
411
+ // One candidate-transition retry gets one cleanup attempt per frame.
412
+ // Running the generic cleanup queue again in the same frame would
413
+ // collapse consecutive retry boundaries and could spin on a failing
414
+ // disposer. A mesh-unregistration owner retries the transition from
415
+ // inside its own backend/observer/registry journal instead.
416
+ flushPendingAutomaticActivityCandidateTransition();
417
+ }
418
+ else if (!pendingAutomaticActivityCandidateTransition) {
419
+ flushPendingAutomaticAnimationGroupSourceCleanups();
420
+ }
421
+ flushPendingAutomaticAnimationGroupAttachments();
422
+ flushPendingAutomaticAnimationGroupReconciliations();
423
+ flushPendingMeshObserverCleanups();
424
+ flushPendingMeshRegistrationRollbacks();
425
+ flushPendingBackendAllocationCleanup();
426
+ recoverPendingFallbackBackend();
427
+ if (!depthBackend || !registry)
428
+ return;
429
+ downgradeFailedSplitBackend();
430
+ if (!depthBackend)
431
+ return;
432
+ flushPendingMeshUnregistrations();
433
+ flushPendingSceneMeshRegistrations();
434
+ flushPendingRenderableMembershipRetries();
435
+ flushPendingTransformMembershipRetries();
436
+ flushMembershipInvalidations();
437
+ registry.consumeDirtyReasons();
438
+ const activityDecision = activityScheduler.consumeFrameDecision();
439
+ resyncExplicitDeformationMembership();
440
+ for (const reason of activityDecision.reasons)
441
+ routeDirtyReason(reason);
442
+ // Membership and deformation invalidations refresh local bounds first;
443
+ // scene-fit consumes the resulting committed topology once per event.
444
+ syncSceneFitCoverage();
445
+ updateCameraInfluenceCoverage();
446
+ const decision = depthRefreshScheduler.consumeFrameDecision();
447
+ let refreshDecision = decision;
448
+ if (options.requestFrame && depthBackend.mode === 'split-static-dynamic') {
449
+ const settlementReasons = Object.freeze([...pendingCompositeRefreshReasons]);
450
+ pendingCompositeRefreshReasons.clear();
451
+ for (const reason of decision.composite.reasons)
452
+ pendingCompositeRefreshReasons.add(reason);
453
+ refreshDecision = Object.freeze({
454
+ static: decision.static,
455
+ dynamic: decision.dynamic,
456
+ composite: Object.freeze({
457
+ shouldRefresh: settlementReasons.length > 0,
458
+ reasons: settlementReasons,
459
+ }),
460
+ });
461
+ if (pendingCompositeRefreshReasons.size > 0) {
462
+ postFlushFrameReason = 'shadow-map-experiment-composite-settlement';
463
+ }
464
+ }
465
+ else if (pendingCompositeRefreshReasons.size > 0) {
466
+ const compositeReasons = Object.freeze([
467
+ ...new Set([...pendingCompositeRefreshReasons, ...decision.composite.reasons]),
468
+ ]);
469
+ pendingCompositeRefreshReasons.clear();
470
+ refreshDecision = Object.freeze({
471
+ static: decision.static,
472
+ dynamic: decision.dynamic,
473
+ composite: Object.freeze({ shouldRefresh: true, reasons: compositeReasons }),
474
+ });
475
+ }
476
+ if (!refreshDecision.static.shouldRefresh
477
+ && !refreshDecision.dynamic.shouldRefresh
478
+ && !refreshDecision.composite.shouldRefresh)
479
+ return;
480
+ depthBackend.refresh(refreshDecision);
481
+ if (!refreshDecision.composite.shouldRefresh)
482
+ return;
483
+ registry.markShadowRendered();
484
+ refreshCount += 1;
485
+ lastRefreshReasons = refreshDecision.composite.reasons;
486
+ }
487
+ finally {
488
+ receiverRestorationAttemptsThisFlush.clear();
489
+ if (pendingPlanRollbackRecovery) {
490
+ postFlushFrameReason = 'shadow-map-experiment-plan-rollback-recovery';
491
+ }
492
+ const pendingFallbackRecovery = pendingFallbackReason !== null
493
+ && depthBackend === null
494
+ && plan?.enabled === true
495
+ && plan.generator.backend.fallback !== 'disable';
496
+ if (!postFlushFrameReason
497
+ && (pendingAutomaticActivityCandidateTransition !== null
498
+ || pendingAutomaticAnimationGroupSourceCleanups.size > 0
499
+ || pendingAutomaticAnimationGroupDetachCleanups.size > 0
500
+ || pendingAutomaticAnimationGroupAttachments.size > 0
501
+ || pendingAutomaticAnimationGroupReconciliations.size > 0
502
+ || pendingSceneMeshRegistrations.size > 0
503
+ || pendingMeshRegistrationRollbacks.size > 0
504
+ || pendingMeshObserverCleanups.size > 0
505
+ || pendingMeshObserverRestorations.size > 0
506
+ || pendingReceiverRestorations.size > 0
507
+ || pendingBackendAllocationCleanup !== null
508
+ || pendingFallbackRecovery
509
+ || pendingMeshUnregistrations.size > 0
510
+ || pendingRenderableMembershipRetries.size > 0
511
+ || pendingTransformMembershipRetries.size > 0
512
+ || pendingFinalDeformationEntityIds.size > 0
513
+ || pendingDeformationInvalidations.size > 0
514
+ || pendingCoverageMembershipRebuild
515
+ || pendingMembershipInvalidationLayers.size > 0)) {
516
+ postFlushFrameReason = pendingAutomaticActivityCandidateTransition !== null
517
+ || pendingAutomaticAnimationGroupSourceCleanups.size > 0
518
+ || pendingAutomaticAnimationGroupDetachCleanups.size > 0
519
+ || pendingAutomaticAnimationGroupAttachments.size > 0
520
+ || pendingAutomaticAnimationGroupReconciliations.size > 0
521
+ ? 'shadow-map-experiment-automatic-activity-cleanup-retry'
522
+ : pendingReceiverRestorations.size > 0
523
+ ? 'shadow-map-experiment-receiver-restoration-retry'
524
+ : pendingBackendAllocationCleanup !== null
525
+ ? 'shadow-map-experiment-backend-allocation-cleanup-retry'
526
+ : pendingFallbackRecovery
527
+ ? 'shadow-map-experiment-fallback-retry'
528
+ : 'shadow-map-experiment-membership-retry';
529
+ }
530
+ flushingRefresh = false;
531
+ if (postFlushFrameReason)
532
+ requestRefreshFrame(postFlushFrameReason);
533
+ }
534
+ };
535
+ const syncActivityEligibility = (entityId) => {
536
+ const object = objectByEntity.get(entityId);
537
+ const active = object?.renderableIds.some(renderableId => registry?.getRecord(renderableId)?.state === 'active') === true;
538
+ activityScheduler.setEntityActive(entityId, active);
539
+ if (active)
540
+ activeCasterEntityIds.add(entityId);
541
+ else
542
+ activeCasterEntityIds.delete(entityId);
543
+ };
544
+ const shouldRetainCaster = (mesh, updateClass, reuseCachedBounds = false) => {
545
+ if (plan?.culling.enabled === false) {
546
+ conservativeCasterMeshes.delete(mesh);
547
+ return true;
548
+ }
549
+ const deformable = isDeformableCaster(mesh, updateClass);
550
+ const binding = bindings.get(mesh);
551
+ let currentBoundingInfo;
552
+ try {
553
+ currentBoundingInfo = mesh.getBoundingInfo();
554
+ }
555
+ catch {
556
+ currentBoundingInfo = undefined;
557
+ }
558
+ const canReuseBounds = reuseCachedBounds
559
+ && binding?.influenceWorldBounds !== undefined
560
+ && binding.influenceBoundingInfo === currentBoundingInfo;
561
+ const bounds = canReuseBounds
562
+ ? binding.influenceWorldBounds ?? null
563
+ : readCasterWorldBounds(mesh, deformable, deformable
564
+ ? meshDataCacheByMesh.get(mesh) ?? createMeshDataCache(mesh, meshDataCacheByMesh)
565
+ : null);
566
+ if (binding && !canReuseBounds) {
567
+ binding.influenceWorldBounds = bounds;
568
+ binding.influenceBoundingInfo = currentBoundingInfo;
569
+ }
570
+ if (!bounds) {
571
+ conservativeCasterMeshes.add(mesh);
572
+ const binding = bindings.get(mesh);
573
+ const code = deformable
574
+ ? 'shadowMapExperiment.deformationBoundsUnavailable'
575
+ : 'shadowMapExperiment.casterBoundsUnavailable';
576
+ const key = `${code}:${mesh.uniqueId}`;
577
+ if (!diagnosticKeys.has(key)) {
578
+ diagnosticKeys.add(key);
579
+ diagnostics.push({
580
+ code,
581
+ message: deformable
582
+ ? 'Deformed caster bounds were unavailable, so influence culling retained the caster conservatively.'
583
+ : 'Caster bounds were unavailable, so influence culling retained the caster conservatively.',
584
+ entityId: binding?.entityId,
585
+ renderableId: binding?.renderableId,
586
+ });
587
+ }
588
+ }
589
+ else {
590
+ conservativeCasterMeshes.delete(mesh);
591
+ }
592
+ return influenceCulling.shouldRetainCaster({
593
+ casterBounds: bounds,
594
+ lightDirection: plan?.light.direction ?? null,
595
+ projectionDistance: plan
596
+ ? Math.max(0, (effectiveCoverageFar ?? plan.generator.bounds.far)
597
+ - (effectiveCoverageNear ?? plan.generator.bounds.near))
598
+ : 0,
599
+ });
600
+ };
601
+ const resyncExplicitDeformationMembership = () => {
602
+ const finalEntityIds = [...pendingFinalDeformationEntityIds];
603
+ const deformationInvalidations = [...pendingDeformationInvalidations.entries()];
604
+ if (finalEntityIds.length === 0 && deformationInvalidations.length === 0)
605
+ return;
606
+ const renderableIds = new Set();
607
+ if (finalEntityIds.length > 0) {
608
+ for (const entityId of finalEntityIds) {
609
+ if (!activeCasterEntityIds.has(entityId))
610
+ continue;
611
+ const object = objectByEntity.get(entityId);
612
+ if (!object)
613
+ continue;
614
+ for (const renderableId of object.renderableIds)
615
+ renderableIds.add(renderableId);
616
+ }
617
+ }
618
+ for (const [entityId] of deformationInvalidations) {
619
+ if (!activeCasterEntityIds.has(entityId))
620
+ continue;
621
+ const object = objectByEntity.get(entityId);
622
+ if (!object)
623
+ continue;
624
+ for (const renderableId of object.renderableIds)
625
+ renderableIds.add(renderableId);
626
+ }
627
+ for (const renderableId of renderableIds)
628
+ syncCaster(renderableId);
629
+ if (renderableIds.size > 0)
630
+ invalidateSceneFitBounds(renderableIds, true);
631
+ for (const entityId of finalEntityIds)
632
+ pendingFinalDeformationEntityIds.delete(entityId);
633
+ if (finalEntityIds.length > 0) {
634
+ markDepthDirty('dynamic', 'caster-activity-stopped');
635
+ }
636
+ for (const [entityId, invalidationReasons] of deformationInvalidations) {
637
+ pendingDeformationInvalidations.delete(entityId);
638
+ for (const reason of invalidationReasons)
639
+ markDepthDirty('dynamic', reason);
640
+ }
641
+ };
642
+ const resyncAllCasterMembership = () => {
643
+ const beforeStatic = depthBackend?.getEvidence().static.retainedMeshCount ?? 0;
644
+ const beforeDynamic = depthBackend?.getEvidence().dynamic.retainedMeshCount ?? 0;
645
+ const casterRenderableIds = new Set();
646
+ for (const mesh of candidateLayerByMesh.keys()) {
647
+ const binding = bindings.get(mesh);
648
+ if (binding)
649
+ casterRenderableIds.add(binding.renderableId);
650
+ }
651
+ for (const renderableId of casterRenderableIds)
652
+ syncCaster(renderableId, true);
653
+ const after = depthBackend?.getEvidence();
654
+ const changed = [];
655
+ if (beforeStatic !== (after?.static.retainedMeshCount ?? 0))
656
+ changed.push('static');
657
+ if (beforeDynamic !== (after?.dynamic.retainedMeshCount ?? 0))
658
+ changed.push('dynamic');
659
+ return Object.freeze(changed);
660
+ };
661
+ const resyncLayerCasterMembership = (layer) => {
662
+ const renderableIds = new Set();
663
+ for (const [mesh, candidateLayer] of candidateLayerByMesh) {
664
+ if (candidateLayer !== layer)
665
+ continue;
666
+ const binding = bindings.get(mesh);
667
+ if (binding)
668
+ renderableIds.add(binding.renderableId);
669
+ }
670
+ for (const renderableId of renderableIds)
671
+ syncCaster(renderableId);
672
+ };
673
+ const flushMembershipInvalidations = () => {
674
+ for (const layer of pendingMembershipInvalidationLayers)
675
+ resyncLayerCasterMembership(layer);
676
+ pendingMembershipInvalidationLayers.clear();
677
+ };
678
+ const updateCameraInfluenceCoverage = () => {
679
+ if (!plan?.enabled)
680
+ return;
681
+ if (!plan.culling.enabled) {
682
+ cullingStable = true;
683
+ pendingCoverageMembershipRebuild = false;
684
+ return;
685
+ }
686
+ const camera = scene.activeCamera;
687
+ const decision = influenceCulling.updateCoverage({
688
+ cameraId: camera ? String(camera.uniqueId) : 'none',
689
+ projectionSignature: camera
690
+ ? `${readCameraProjectionSignature(camera, scene)}:${JSON.stringify({
691
+ ...plan.generator.bounds,
692
+ center: effectiveCoverageCenter ?? plan.generator.bounds.center,
693
+ extent: effectiveCoverageExtent ?? plan.generator.bounds.extent,
694
+ near: effectiveCoverageNear ?? plan.generator.bounds.near,
695
+ far: effectiveCoverageFar ?? plan.generator.bounds.far,
696
+ })}`
697
+ : 'none',
698
+ visibleBounds: camera
699
+ ? calculateCameraShadowCoverage(camera, plan, effectiveCoverageCenter ?? plan.generator.bounds.center, effectiveCoverageExtent ?? plan.generator.bounds.extent)
700
+ : null,
701
+ });
702
+ const retryingMembershipRebuild = pendingCoverageMembershipRebuild;
703
+ cullingStable = !decision.rebuilt && !retryingMembershipRebuild;
704
+ if (!decision.rebuilt && !retryingMembershipRebuild)
705
+ return;
706
+ if (decision.rebuilt && !retryingMembershipRebuild) {
707
+ lastCullingInvalidationReason = 'camera-influence-region-changed';
708
+ }
709
+ let changedLayers;
710
+ try {
711
+ syncingSceneFitMembership = true;
712
+ changedLayers = resyncAllCasterMembership();
713
+ pendingCoverageMembershipRebuild = false;
714
+ }
715
+ catch (error) {
716
+ pendingCoverageMembershipRebuild = true;
717
+ cullingStable = false;
718
+ throw error;
719
+ }
720
+ finally {
721
+ syncingSceneFitMembership = false;
722
+ }
723
+ const dirtyLayers = retryingMembershipRebuild
724
+ ? Object.freeze(['static', 'dynamic'])
725
+ : changedLayers;
726
+ for (const layer of dirtyLayers) {
727
+ markDepthDirty(layer, 'camera-influence-region-changed');
728
+ }
729
+ };
730
+ const downgradeFailedSplitBackend = () => {
731
+ if (!depthBackend || depthBackend.mode !== 'split-static-dynamic' || !plan?.enabled || !lightState)
732
+ return;
733
+ const runtimeFailureReason = depthBackend.getRuntimeFailureReason();
734
+ if (!runtimeFailureReason)
735
+ return;
736
+ const failedBackend = depthBackend;
737
+ try {
738
+ failedBackend.dispose();
739
+ }
740
+ catch (error) {
741
+ depthBackend = null;
742
+ generator = null;
743
+ pendingFallbackReason = runtimeFailureReason;
744
+ pendingBackendAllocationCleanup = () => {
745
+ try {
746
+ failedBackend.dispose();
747
+ return Object.freeze([]);
748
+ }
749
+ catch (retryError) {
750
+ return Object.freeze([retryError]);
751
+ }
752
+ };
753
+ throw error;
754
+ }
755
+ depthBackend = null;
756
+ generator = null;
757
+ pendingFallbackReason = runtimeFailureReason;
758
+ recoverPendingFallbackBackend();
759
+ };
760
+ const rememberBackendAllocationCleanup = (error) => {
761
+ const cleanup = readBabylonShadowMapExperimentAllocationCleanup(error);
762
+ if (cleanup)
763
+ pendingBackendAllocationCleanup = cleanup;
764
+ };
765
+ const flushPendingBackendAllocationCleanup = () => {
766
+ if (!pendingBackendAllocationCleanup)
767
+ return;
768
+ const cleanupErrors = pendingBackendAllocationCleanup();
769
+ if (cleanupErrors.length > 0) {
770
+ throw Object.assign(new Error('shadowMapExperiment.backendAllocationCleanupFailed'), { cleanupErrors });
771
+ }
772
+ pendingBackendAllocationCleanup = null;
773
+ };
774
+ const prepareBackendCasterMembership = (backend) => {
775
+ let staticCandidateCount = 0;
776
+ let dynamicCandidateCount = 0;
777
+ for (const [mesh, layer] of candidateLayerByMesh) {
778
+ if (layer === 'static')
779
+ staticCandidateCount += 1;
780
+ else
781
+ dynamicCandidateCount += 1;
782
+ const binding = bindings.get(mesh);
783
+ const updateClass = binding ? objectByRenderable.get(binding.renderableId)?.updateClass : null;
784
+ if (!updateClass || shouldRetainCaster(mesh, updateClass))
785
+ backend.addCaster(mesh, layer);
786
+ }
787
+ backend.setCandidateCounts(staticCandidateCount, dynamicCandidateCount);
788
+ };
789
+ const recoverPendingFallbackBackend = () => {
790
+ if (!pendingFallbackReason || depthBackend || !plan?.enabled || !lightState)
791
+ return;
792
+ flushPendingBackendAllocationCleanup();
793
+ const fallbackReason = pendingFallbackReason;
794
+ if (plan.generator.backend.fallback === 'disable')
795
+ return;
796
+ let replacement = null;
797
+ try {
798
+ replacement = createBabylonShadowMapExperimentSingleDepthBackend(lightState.light, plan, fallbackReason);
799
+ prepareBackendCasterMembership(replacement);
800
+ depthBackend = replacement;
801
+ generator = replacement.generator;
802
+ pendingFallbackReason = null;
803
+ }
804
+ catch (error) {
805
+ rememberBackendAllocationCleanup(error);
806
+ if (replacement) {
807
+ const ownedReplacement = replacement;
808
+ try {
809
+ ownedReplacement.dispose();
810
+ }
811
+ catch (cleanupError) {
812
+ pendingBackendAllocationCleanup = () => {
813
+ try {
814
+ ownedReplacement.dispose();
815
+ return Object.freeze([]);
816
+ }
817
+ catch (retryError) {
818
+ return Object.freeze([retryError]);
819
+ }
820
+ };
821
+ if (error && typeof error === 'object') {
822
+ attachFailureMetadata(error, {
823
+ fallbackPreparationCleanupErrors: Object.freeze([cleanupError]),
824
+ });
825
+ }
826
+ }
827
+ }
828
+ throw error;
829
+ }
830
+ warnForFallback(fallbackReason);
831
+ markAllDepthDirty('generator-created');
832
+ };
833
+ const syncCaster = (renderableId, reuseCachedBounds = false) => {
834
+ const record = registry?.getRecord(renderableId);
835
+ const meshes = meshesByRenderable.get(renderableId);
836
+ if (!depthBackend || !record || !meshes)
837
+ return () => { };
838
+ const layer = shadowMapExperimentDepthLayerForUpdateClass(record.updateClass);
839
+ const shouldCast = record.state === 'active';
840
+ const changedLayers = new Set();
841
+ const previousMembership = [...meshes].map(mesh => ({
842
+ mesh,
843
+ candidateLayer: candidateLayerByMesh.get(mesh) ?? null,
844
+ staticCaster: depthBackend.hasCaster(mesh, 'static'),
845
+ dynamicCaster: depthBackend.hasCaster(mesh, 'dynamic'),
846
+ conservative: conservativeCasterMeshes.has(mesh),
847
+ }));
848
+ if (options.coverageMode === 'scene-fit' && !syncingSceneFitMembership) {
849
+ sceneFitCoverageInvalidated = true;
850
+ }
851
+ const rollbackMembership = (primaryError) => {
852
+ const rollbackErrors = [];
853
+ for (const previous of [...previousMembership].reverse()) {
854
+ try {
855
+ if (previous.staticCaster)
856
+ depthBackend.addCaster(previous.mesh, 'static');
857
+ else if (previous.dynamicCaster)
858
+ depthBackend.addCaster(previous.mesh, 'dynamic');
859
+ else
860
+ depthBackend.removeCaster(previous.mesh);
861
+ }
862
+ catch (rollbackError) {
863
+ rollbackErrors.push(rollbackError);
864
+ }
865
+ setCandidateLayer(previous.mesh, previous.candidateLayer);
866
+ if (previous.conservative)
867
+ conservativeCasterMeshes.add(previous.mesh);
868
+ else
869
+ conservativeCasterMeshes.delete(previous.mesh);
870
+ }
871
+ syncCandidateCounts();
872
+ if (rollbackErrors.length > 0 && primaryError && typeof primaryError === 'object') {
873
+ attachFailureMetadata(primaryError, {
874
+ casterMembershipRollbackErrors: Object.freeze(rollbackErrors),
875
+ });
876
+ }
877
+ };
878
+ try {
879
+ for (const mesh of meshes) {
880
+ const previousLayer = candidateLayerByMesh.get(mesh);
881
+ if (shouldCast && isMeshRuntimeVisible(mesh)) {
882
+ const retained = shouldRetainCaster(mesh, record.updateClass, reuseCachedBounds);
883
+ if (retained) {
884
+ const wasRetained = depthBackend.hasCaster(mesh, layer);
885
+ depthBackend.addCaster(mesh, layer);
886
+ setCandidateLayer(mesh, layer);
887
+ if (!wasRetained || previousLayer !== layer)
888
+ changedLayers.add(layer);
889
+ if (previousLayer && previousLayer !== layer)
890
+ changedLayers.add(previousLayer);
891
+ }
892
+ else {
893
+ const wasRetained = depthBackend.hasCaster(mesh);
894
+ depthBackend.removeCaster(mesh);
895
+ setCandidateLayer(mesh, layer);
896
+ if (wasRetained)
897
+ changedLayers.add(previousLayer ?? layer);
898
+ }
899
+ }
900
+ else {
901
+ const wasRetained = depthBackend.hasCaster(mesh);
902
+ depthBackend.removeCaster(mesh);
903
+ setCandidateLayer(mesh, null);
904
+ conservativeCasterMeshes.delete(mesh);
905
+ if (wasRetained)
906
+ changedLayers.add(previousLayer ?? layer);
907
+ }
908
+ }
909
+ }
910
+ catch (error) {
911
+ rollbackMembership(error);
912
+ throw error;
913
+ }
914
+ syncCandidateCounts();
915
+ for (const changedLayer of changedLayers) {
916
+ markDepthDirty(changedLayer, 'caster-influence-membership-changed');
917
+ }
918
+ syncActivityEligibility(record.entityId);
919
+ return rollbackMembership;
920
+ };
921
+ const restorePendingReceiver = (target) => {
922
+ const desired = pendingReceiverRestorations.get(target);
923
+ if (desired === undefined)
924
+ return;
925
+ if (flushingRefresh && receiverRestorationAttemptsThisFlush.has(target)) {
926
+ throw new Error('shadowMapExperiment.receiverRestorationPending');
927
+ }
928
+ if (!isDisposed(target) && target.receiveShadows !== desired)
929
+ target.receiveShadows = desired;
930
+ pendingReceiverRestorations.delete(target);
931
+ };
932
+ const flushPendingReceiverRestorations = () => {
933
+ for (const target of [...pendingReceiverRestorations.keys()]) {
934
+ receiverRestorationAttemptsThisFlush.add(target);
935
+ try {
936
+ const desired = pendingReceiverRestorations.get(target);
937
+ if (desired !== undefined && !isDisposed(target) && target.receiveShadows !== desired) {
938
+ target.receiveShadows = desired;
939
+ }
940
+ pendingReceiverRestorations.delete(target);
941
+ }
942
+ catch {
943
+ // The target remains the owner and the post-flush predicate schedules
944
+ // exactly one further frame; no unrelated receiver is scanned.
945
+ }
946
+ }
947
+ };
948
+ const writeReceiver = (target, desired) => {
949
+ try {
950
+ if (!isDisposed(target) && target.receiveShadows !== desired)
951
+ target.receiveShadows = desired;
952
+ pendingReceiverRestorations.delete(target);
953
+ }
954
+ catch (error) {
955
+ pendingReceiverRestorations.set(target, desired);
956
+ throw error;
957
+ }
958
+ };
959
+ const restoreReceiver = (mesh) => {
960
+ const target = receiverTargetByMesh.get(mesh) ?? resolveReceiverTarget(mesh);
961
+ receiverTargetByMesh.delete(mesh);
962
+ const state = receiverStates.get(target);
963
+ if (!state)
964
+ return;
965
+ if (state.requests.get(mesh) === true)
966
+ state.enabledRequestCount -= 1;
967
+ state.requests.delete(mesh);
968
+ if (state.requests.size > 0) {
969
+ const next = state.enabledRequestCount > 0;
970
+ state.ownedValue = next;
971
+ writeReceiver(target, next);
972
+ return;
973
+ }
974
+ if (!isDisposed(target) && target.receiveShadows === state.ownedValue) {
975
+ writeReceiver(target, state.previousReceiveShadows);
976
+ }
977
+ receiverStates.delete(target);
978
+ };
979
+ const setReceiver = (mesh, enabled, renderableId) => {
980
+ const eligible = !enabled || isReceiverEligible(mesh);
981
+ const next = enabled && eligible;
982
+ const previousTarget = receiverTargetByMesh.get(mesh);
983
+ const target = resolveReceiverTarget(mesh);
984
+ // A failed journal rollback must be restored before this target is read as
985
+ // a new baseline. Otherwise a later retry would capture our own stale
986
+ // value as caller state.
987
+ restorePendingReceiver(target);
988
+ if (previousTarget && previousTarget !== target)
989
+ restoreReceiver(mesh);
990
+ receiverTargetByMesh.set(mesh, target);
991
+ let state = receiverStates.get(target);
992
+ if (!state) {
993
+ state = {
994
+ mesh: target,
995
+ previousReceiveShadows: target.receiveShadows,
996
+ requests: new Map(),
997
+ enabledRequestCount: 0,
998
+ ownedValue: next,
999
+ };
1000
+ receiverStates.set(target, state);
1001
+ }
1002
+ const previousRequest = state.requests.get(mesh);
1003
+ if (previousRequest !== next) {
1004
+ if (previousRequest === true)
1005
+ state.enabledRequestCount -= 1;
1006
+ if (next)
1007
+ state.enabledRequestCount += 1;
1008
+ }
1009
+ state.requests.set(mesh, next);
1010
+ const ownedValue = state.enabledRequestCount > 0;
1011
+ state.ownedValue = ownedValue;
1012
+ writeReceiver(target, ownedValue);
1013
+ if (enabled && !eligible) {
1014
+ const code = 'shadowMapExperiment.transparentReceiver';
1015
+ const key = `${code}:${renderableId}`;
1016
+ if (!diagnosticKeys.has(key)) {
1017
+ diagnosticKeys.add(key);
1018
+ diagnostics.push({
1019
+ code,
1020
+ message: 'Receiver was excluded because its current material is not shadow-receiver eligible.',
1021
+ renderableId,
1022
+ });
1023
+ }
1024
+ }
1025
+ };
1026
+ const captureReceiverJournal = (meshes) => {
1027
+ const touchedMeshes = [...meshes];
1028
+ const touchedMeshesByTarget = new Map();
1029
+ const trackTarget = (target, mesh) => {
1030
+ let targetMeshes = touchedMeshesByTarget.get(target);
1031
+ if (!targetMeshes) {
1032
+ targetMeshes = new Set();
1033
+ touchedMeshesByTarget.set(target, targetMeshes);
1034
+ }
1035
+ targetMeshes.add(mesh);
1036
+ };
1037
+ for (const mesh of touchedMeshes) {
1038
+ trackTarget(resolveReceiverTarget(mesh), mesh);
1039
+ const previousTarget = receiverTargetByMesh.get(mesh);
1040
+ if (previousTarget)
1041
+ trackTarget(previousTarget, mesh);
1042
+ }
1043
+ for (const target of touchedMeshesByTarget.keys())
1044
+ restorePendingReceiver(target);
1045
+ const states = [...touchedMeshesByTarget].map(([target, targetMeshes]) => {
1046
+ const state = receiverStates.get(target);
1047
+ return {
1048
+ target,
1049
+ state: state ?? null,
1050
+ enabledRequestCount: state?.enabledRequestCount ?? 0,
1051
+ ownedValue: state?.ownedValue ?? false,
1052
+ requests: [...targetMeshes].map(mesh => [mesh, state?.requests.get(mesh)]),
1053
+ };
1054
+ });
1055
+ return {
1056
+ states,
1057
+ targetsByMesh: touchedMeshes.map(mesh => [mesh, receiverTargetByMesh.get(mesh) ?? null]),
1058
+ targetValues: [...touchedMeshesByTarget.keys()].map(target => [target, target.receiveShadows]),
1059
+ };
1060
+ };
1061
+ const rollbackReceiverJournal = (journal, primaryError) => {
1062
+ const rollbackErrors = [];
1063
+ for (const entry of journal.states) {
1064
+ if (!entry.state) {
1065
+ receiverStates.delete(entry.target);
1066
+ continue;
1067
+ }
1068
+ for (const [mesh, request] of entry.requests) {
1069
+ if (request === undefined)
1070
+ entry.state.requests.delete(mesh);
1071
+ else
1072
+ entry.state.requests.set(mesh, request);
1073
+ }
1074
+ entry.state.enabledRequestCount = entry.enabledRequestCount;
1075
+ entry.state.ownedValue = entry.ownedValue;
1076
+ receiverStates.set(entry.target, entry.state);
1077
+ }
1078
+ for (const [mesh, target] of journal.targetsByMesh) {
1079
+ receiverTargetByMesh.delete(mesh);
1080
+ if (target)
1081
+ receiverTargetByMesh.set(mesh, target);
1082
+ }
1083
+ for (const [target, receiveShadows] of journal.targetValues) {
1084
+ try {
1085
+ writeReceiver(target, receiveShadows);
1086
+ }
1087
+ catch (error) {
1088
+ rollbackErrors.push(error);
1089
+ }
1090
+ }
1091
+ if (rollbackErrors.length > 0 && primaryError && typeof primaryError === 'object') {
1092
+ attachFailureMetadata(primaryError, { receiverRollbackErrors: Object.freeze(rollbackErrors) });
1093
+ }
1094
+ };
1095
+ const syncRenderable = (renderableId) => {
1096
+ const meshes = meshesByRenderable.get(renderableId);
1097
+ const object = objectByRenderable.get(renderableId);
1098
+ if (!meshes || !object || !registry)
1099
+ return;
1100
+ const visible = [...meshes].some(isMeshRuntimeVisible);
1101
+ const previousRecord = registry.getRecord(renderableId);
1102
+ const receiverJournal = captureReceiverJournal(meshes);
1103
+ let rollbackCasterMembership = null;
1104
+ const nextRecord = previousRecord?.renderableReady
1105
+ ? registry.setVisible(renderableId, visible)
1106
+ : registry.attach(renderableId, visible);
1107
+ if (previousRecord?.state === 'active'
1108
+ && nextRecord?.state !== 'active'
1109
+ && registry.snapshot().activeCasterCount === 0) {
1110
+ registry.markDirty('empty-list-clear');
1111
+ markDepthDirty(shadowMapExperimentDepthLayerForUpdateClass(object.updateClass), 'empty-list-clear');
1112
+ }
1113
+ try {
1114
+ // Receiver preparation is fallible in host integrations and is journaled
1115
+ // together with registry/backend state. Visibility/membership changes
1116
+ // never publish automatic activity topology.
1117
+ for (const mesh of meshes)
1118
+ setReceiver(mesh, object.receive, renderableId);
1119
+ rollbackCasterMembership = syncCaster(renderableId);
1120
+ pendingRenderableMembershipRetries.delete(renderableId);
1121
+ }
1122
+ catch (error) {
1123
+ rollbackCasterMembership?.(error);
1124
+ rollbackReceiverJournal(receiverJournal, error);
1125
+ if (previousRecord) {
1126
+ if (previousRecord.renderableReady)
1127
+ registry.attach(renderableId, previousRecord.runtimeVisible);
1128
+ else
1129
+ registry.detach(renderableId);
1130
+ registry.setVisible(renderableId, previousRecord.runtimeVisible);
1131
+ }
1132
+ syncActivityEligibility(object.entityId);
1133
+ pendingRenderableMembershipRetries.set(renderableId, 'renderable');
1134
+ requestRefreshFrame('shadow-map-experiment-membership-retry');
1135
+ throw error;
1136
+ }
1137
+ };
1138
+ const flushPendingRenderableMembershipRetries = () => {
1139
+ // Membership retries own only local receiver/caster state. Candidate
1140
+ // topology is published by Mesh registration, unregistration, or an
1141
+ // explicit Plan/runtime-object delta.
1142
+ for (const [renderableId, retryKind] of [...pendingRenderableMembershipRetries]) {
1143
+ if (!objectByRenderable.has(renderableId) || !meshesByRenderable.has(renderableId)) {
1144
+ pendingRenderableMembershipRetries.delete(renderableId);
1145
+ continue;
1146
+ }
1147
+ if (retryKind === 'renderable') {
1148
+ try {
1149
+ syncRenderable(renderableId);
1150
+ }
1151
+ catch (error) {
1152
+ if (error instanceof Error
1153
+ && error.message === 'shadowMapExperiment.automaticActivityCandidateCommitFailed') {
1154
+ continue;
1155
+ }
1156
+ throw error;
1157
+ }
1158
+ }
1159
+ else {
1160
+ const pendingVisibility = pendingExplicitVisibility.get(renderableId);
1161
+ const previousRecord = registry?.getRecord(renderableId) ?? null;
1162
+ let rollbackCasterMembership = null;
1163
+ if (pendingVisibility !== undefined)
1164
+ registry?.setVisible(renderableId, pendingVisibility);
1165
+ try {
1166
+ rollbackCasterMembership = syncCaster(renderableId);
1167
+ }
1168
+ catch (error) {
1169
+ rollbackCasterMembership?.(error);
1170
+ if (previousRecord && registry) {
1171
+ if (previousRecord.renderableReady)
1172
+ registry.attach(renderableId, previousRecord.runtimeVisible);
1173
+ else
1174
+ registry.detach(renderableId);
1175
+ registry.setVisible(renderableId, previousRecord.runtimeVisible);
1176
+ }
1177
+ requestRefreshFrame('shadow-map-experiment-membership-retry');
1178
+ if (error instanceof Error
1179
+ && error.message === 'shadowMapExperiment.automaticActivityCandidateCommitFailed') {
1180
+ continue;
1181
+ }
1182
+ throw error;
1183
+ }
1184
+ pendingExplicitVisibility.delete(renderableId);
1185
+ pendingRenderableMembershipRetries.delete(renderableId);
1186
+ }
1187
+ pendingRenderableMembershipRetries.delete(renderableId);
1188
+ enqueueRefresh('caster-visibility-changed');
1189
+ }
1190
+ };
1191
+ const syncMatrixObserver = (binding) => {
1192
+ const object = objectByRenderable.get(binding.renderableId);
1193
+ if (!object?.cast) {
1194
+ if (binding.matrixObserver)
1195
+ binding.mesh.onAfterWorldMatrixUpdateObservable.remove(binding.matrixObserver);
1196
+ binding.matrixObserver = null;
1197
+ binding.worldMatrixSnapshot = null;
1198
+ pendingTransformMembershipRetries.delete(binding.mesh);
1199
+ return;
1200
+ }
1201
+ if (binding.matrixObserver)
1202
+ return;
1203
+ binding.worldMatrixSnapshot = readWorldMatrixSnapshot(binding.mesh);
1204
+ binding.matrixObserver = binding.mesh.onAfterWorldMatrixUpdateObservable.add(() => {
1205
+ if (disposed || disposing)
1206
+ return;
1207
+ const nextSnapshot = readWorldMatrixSnapshot(binding.mesh);
1208
+ if (areMatrixSnapshotsEqual(binding.worldMatrixSnapshot, nextSnapshot))
1209
+ return;
1210
+ const currentObject = objectByRenderable.get(binding.renderableId);
1211
+ if (!currentObject?.cast) {
1212
+ binding.worldMatrixSnapshot = nextSnapshot;
1213
+ return;
1214
+ }
1215
+ try {
1216
+ syncCaster(binding.renderableId);
1217
+ }
1218
+ catch (error) {
1219
+ pendingTransformMembershipRetries.add(binding.mesh);
1220
+ requestRefreshFrame('shadow-map-experiment-membership-retry');
1221
+ throw error;
1222
+ }
1223
+ binding.worldMatrixSnapshot = nextSnapshot;
1224
+ enqueueRefresh(currentObject.updateClass === 'static' ? 'static-transform-changed' : 'dynamic-transform-changed');
1225
+ });
1226
+ };
1227
+ const flushPendingTransformMembershipRetries = () => {
1228
+ for (const mesh of [...pendingTransformMembershipRetries]) {
1229
+ const binding = bindings.get(mesh);
1230
+ const currentObject = binding ? objectByRenderable.get(binding.renderableId) : null;
1231
+ if (!binding || !currentObject?.cast) {
1232
+ pendingTransformMembershipRetries.delete(mesh);
1233
+ if (binding)
1234
+ binding.worldMatrixSnapshot = readWorldMatrixSnapshot(mesh);
1235
+ continue;
1236
+ }
1237
+ syncCaster(binding.renderableId);
1238
+ binding.worldMatrixSnapshot = readWorldMatrixSnapshot(mesh);
1239
+ pendingTransformMembershipRetries.delete(mesh);
1240
+ enqueueRefresh(currentObject.updateClass === 'static' ? 'static-transform-changed' : 'dynamic-transform-changed');
1241
+ }
1242
+ };
1243
+ const flushPendingMeshObserverRestoration = (mesh) => {
1244
+ const pending = pendingMeshObserverRestorations.get(mesh);
1245
+ if (!pending)
1246
+ return Object.freeze([]);
1247
+ const restorationErrors = [];
1248
+ const mutable = pending.binding;
1249
+ if (pending.enabledObserver) {
1250
+ try {
1251
+ const template = pending.enabledObserver;
1252
+ mutable.enabledObserver = mesh.onEnabledStateChangedObservable.add(template.callback, template.mask, false, template.scope);
1253
+ pending.enabledObserver = null;
1254
+ }
1255
+ catch (error) {
1256
+ restorationErrors.push(error);
1257
+ }
1258
+ }
1259
+ for (const restoration of [...pending.ancestorEnabledObservers]) {
1260
+ try {
1261
+ const { binding, template } = restoration;
1262
+ binding.observer = binding.node.onEnabledStateChangedObservable.add(template.callback, template.mask, false, template.scope);
1263
+ pending.ancestorEnabledObservers.splice(pending.ancestorEnabledObservers.indexOf(restoration), 1);
1264
+ }
1265
+ catch (error) {
1266
+ restorationErrors.push(error);
1267
+ }
1268
+ }
1269
+ if (pending.disposeObserver) {
1270
+ try {
1271
+ const template = pending.disposeObserver;
1272
+ mutable.disposeObserver = mesh.onDisposeObservable.add(template.callback, template.mask, false, template.scope);
1273
+ pending.disposeObserver = null;
1274
+ }
1275
+ catch (error) {
1276
+ restorationErrors.push(error);
1277
+ }
1278
+ }
1279
+ if (pending.matrixObserver) {
1280
+ try {
1281
+ const template = pending.matrixObserver;
1282
+ mutable.matrixObserver = mesh.onAfterWorldMatrixUpdateObservable.add(template.callback, template.mask, false, template.scope);
1283
+ pending.matrixObserver = null;
1284
+ }
1285
+ catch (error) {
1286
+ restorationErrors.push(error);
1287
+ }
1288
+ }
1289
+ if (!pending.enabledObserver
1290
+ && pending.ancestorEnabledObservers.length === 0
1291
+ && !pending.disposeObserver
1292
+ && !pending.matrixObserver) {
1293
+ pendingMeshObserverRestorations.delete(mesh);
1294
+ }
1295
+ return Object.freeze(restorationErrors);
1296
+ };
1297
+ const unregisterMesh = (mesh) => {
1298
+ const binding = bindings.get(mesh);
1299
+ if (!binding)
1300
+ return false;
1301
+ const pendingObserverRestorationErrors = flushPendingMeshObserverRestoration(mesh);
1302
+ if (pendingObserverRestorationErrors.length > 0) {
1303
+ pendingMeshUnregistrations.add(mesh);
1304
+ requestRefreshFrame('shadow-map-experiment-mesh-observer-restoration-retry');
1305
+ throw Object.assign(new Error('shadowMapExperiment.meshObserverRestorationFailed'), {
1306
+ cleanupErrors: pendingObserverRestorationErrors,
1307
+ });
1308
+ }
1309
+ const previousLayer = candidateLayerByMesh.get(mesh);
1310
+ const wasStaticCaster = depthBackend?.hasCaster(mesh, 'static') === true;
1311
+ const wasDynamicCaster = depthBackend?.hasCaster(mesh, 'dynamic') === true;
1312
+ const wasCaster = wasStaticCaster || wasDynamicCaster;
1313
+ const wasConservative = conservativeCasterMeshes.has(mesh);
1314
+ const previousRecord = registry?.getRecord(binding.renderableId) ?? null;
1315
+ const wasActiveRecord = previousRecord?.state === 'active';
1316
+ const receiverJournal = captureReceiverJournal([mesh]);
1317
+ const meshes = meshesByRenderable.get(binding.renderableId);
1318
+ const remainingMeshes = [...(meshes ?? [])].filter(candidate => candidate !== mesh);
1319
+ let enabledObserverRemoved = false;
1320
+ const ancestorEnabledObserversRemoved = [];
1321
+ let disposeObserverRemoved = false;
1322
+ let matrixObserverRemoved = false;
1323
+ const enabledObserverTemplate = binding.enabledObserver;
1324
+ const disposeObserverTemplate = binding.disposeObserver;
1325
+ const matrixObserverTemplate = binding.matrixObserver;
1326
+ let automaticActivityCandidatesChanged = false;
1327
+ const restoreRegistry = () => {
1328
+ if (!registry || !previousRecord)
1329
+ return;
1330
+ if (previousRecord.renderableReady)
1331
+ registry.attach(binding.renderableId, previousRecord.runtimeVisible);
1332
+ else
1333
+ registry.detach(binding.renderableId);
1334
+ registry.setVisible(binding.renderableId, previousRecord.runtimeVisible);
1335
+ };
1336
+ try {
1337
+ depthBackend?.removeCaster(mesh);
1338
+ restoreReceiver(mesh);
1339
+ if (remainingMeshes.length === 0)
1340
+ registry?.detach(binding.renderableId);
1341
+ else
1342
+ registry?.setVisible(binding.renderableId, remainingMeshes.some(isMeshRuntimeVisible));
1343
+ enabledObserverRemoved = mesh.onEnabledStateChangedObservable.remove(binding.enabledObserver);
1344
+ for (const ancestorBinding of binding.ancestorEnabledObservers) {
1345
+ const template = ancestorBinding.observer;
1346
+ if (ancestorBinding.node.onEnabledStateChangedObservable.remove(template)) {
1347
+ ancestorEnabledObserversRemoved.push({ binding: ancestorBinding, template });
1348
+ }
1349
+ }
1350
+ disposeObserverRemoved = mesh.onDisposeObservable.remove(binding.disposeObserver);
1351
+ if (binding.matrixObserver) {
1352
+ matrixObserverRemoved = mesh.onAfterWorldMatrixUpdateObservable.remove(binding.matrixObserver);
1353
+ }
1354
+ if (!stagingPlanCandidateTransitions) {
1355
+ // A registration can fail while its new binding exists only in the
1356
+ // unpublished proposal. Rollback must remove it from that proposed
1357
+ // view, not just from the last committed candidate map, otherwise the
1358
+ // next frame could publish a candidate whose mesh binding was deleted.
1359
+ const ownsPendingCandidateTransition = pendingAutomaticActivityCandidateTransition !== null;
1360
+ const nextCandidates = new Map(pendingAutomaticActivityCandidateTransition
1361
+ ?? automaticActivityCandidateBindings);
1362
+ automaticActivityCandidatesChanged = updateAutomaticActivityCandidateBinding(nextCandidates, binding, null);
1363
+ if ((automaticActivityCandidatesChanged || ownsPendingCandidateTransition)
1364
+ && !applyAutomaticActivityCandidateTransition(nextCandidates)) {
1365
+ throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
1366
+ }
1367
+ }
1368
+ }
1369
+ catch (error) {
1370
+ const rollbackErrors = [];
1371
+ try {
1372
+ if (wasStaticCaster)
1373
+ depthBackend?.addCaster(mesh, 'static');
1374
+ else if (wasDynamicCaster)
1375
+ depthBackend?.addCaster(mesh, 'dynamic');
1376
+ else
1377
+ depthBackend?.removeCaster(mesh);
1378
+ }
1379
+ catch (rollbackError) {
1380
+ rollbackErrors.push(rollbackError);
1381
+ }
1382
+ try {
1383
+ rollbackReceiverJournal(receiverJournal, error);
1384
+ }
1385
+ catch (rollbackError) {
1386
+ rollbackErrors.push(rollbackError);
1387
+ }
1388
+ try {
1389
+ restoreRegistry();
1390
+ }
1391
+ catch (rollbackError) {
1392
+ rollbackErrors.push(rollbackError);
1393
+ }
1394
+ if (enabledObserverRemoved
1395
+ || ancestorEnabledObserversRemoved.length > 0
1396
+ || disposeObserverRemoved
1397
+ || matrixObserverRemoved) {
1398
+ pendingMeshObserverRestorations.set(mesh, {
1399
+ mesh,
1400
+ binding,
1401
+ enabledObserver: enabledObserverRemoved ? enabledObserverTemplate : null,
1402
+ ancestorEnabledObservers: ancestorEnabledObserversRemoved,
1403
+ disposeObserver: disposeObserverRemoved ? disposeObserverTemplate : null,
1404
+ matrixObserver: matrixObserverRemoved ? matrixObserverTemplate : null,
1405
+ });
1406
+ rollbackErrors.push(...flushPendingMeshObserverRestoration(mesh));
1407
+ }
1408
+ if (wasConservative)
1409
+ conservativeCasterMeshes.add(mesh);
1410
+ else
1411
+ conservativeCasterMeshes.delete(mesh);
1412
+ if (rollbackErrors.length > 0 && error && typeof error === 'object') {
1413
+ attachFailureMetadata(error, { unregisterRollbackErrors: Object.freeze(rollbackErrors) });
1414
+ }
1415
+ // The binding/candidate map deliberately stays intact. The queued retry
1416
+ // is therefore the sole owner until every teardown step commits.
1417
+ pendingMeshUnregistrations.add(mesh);
1418
+ requestRefreshFrame(pendingMeshObserverRestorations.has(mesh)
1419
+ ? 'shadow-map-experiment-mesh-observer-restoration-retry'
1420
+ : 'shadow-map-experiment-membership-retry');
1421
+ throw error;
1422
+ }
1423
+ if (remainingMeshes.length === 0) {
1424
+ meshesByRenderable.delete(binding.renderableId);
1425
+ pendingRenderableMembershipRetries.delete(binding.renderableId);
1426
+ pendingExplicitVisibility.delete(binding.renderableId);
1427
+ }
1428
+ else {
1429
+ meshes?.delete(mesh);
1430
+ }
1431
+ setCandidateLayer(mesh, null);
1432
+ conservativeCasterMeshes.delete(mesh);
1433
+ meshDataCacheByMesh.delete(mesh);
1434
+ syncCandidateCounts();
1435
+ syncActivityEligibility(binding.entityId);
1436
+ if (wasCaster || wasActiveRecord) {
1437
+ if (registry?.snapshot().activeCasterCount === 0)
1438
+ registry.markDirty('empty-list-clear');
1439
+ if (previousLayer)
1440
+ markDepthDirty(previousLayer, 'caster-removed');
1441
+ else
1442
+ enqueueRefresh('caster-removed');
1443
+ if (candidateLayerByMesh.size === 0 && previousLayer) {
1444
+ markDepthDirty(previousLayer, 'empty-list-clear');
1445
+ }
1446
+ }
1447
+ bindings.delete(mesh);
1448
+ pendingMeshObserverRestorations.delete(mesh);
1449
+ pendingMeshRegistrationRollbacks.delete(mesh);
1450
+ pendingMeshUnregistrations.delete(mesh);
1451
+ pendingTransformMembershipRetries.delete(mesh);
1452
+ // Commit the candidate removal only after every fallible renderer,
1453
+ // observer and receiver teardown above has completed. Retrying a failed
1454
+ // unregister therefore continues to expose the old valid topology.
1455
+ return automaticActivityCandidatesChanged;
1456
+ };
1457
+ const requestMeshUnregistration = (mesh) => {
1458
+ pendingSceneMeshRegistrations.delete(mesh);
1459
+ try {
1460
+ unregisterMesh(mesh);
1461
+ pendingMeshUnregistrations.delete(mesh);
1462
+ }
1463
+ catch (error) {
1464
+ pendingMeshUnregistrations.add(mesh);
1465
+ requestRefreshFrame('shadow-map-experiment-membership-retry');
1466
+ throw error;
1467
+ }
1468
+ };
1469
+ const flushPendingMeshUnregistrations = () => {
1470
+ for (const mesh of [...pendingMeshUnregistrations])
1471
+ requestMeshUnregistration(mesh);
1472
+ };
1473
+ const registerMesh = (mesh) => {
1474
+ const pendingObserverCleanupErrors = flushPendingMeshObserverCleanup(mesh);
1475
+ if (pendingObserverCleanupErrors.length > 0) {
1476
+ throw Object.assign(new Error('shadowMapExperiment.meshObserverCleanupFailed'), {
1477
+ cleanupErrors: pendingObserverCleanupErrors,
1478
+ });
1479
+ }
1480
+ if (pendingMeshRegistrationRollbacks.has(mesh)) {
1481
+ try {
1482
+ unregisterMesh(mesh);
1483
+ }
1484
+ catch (error) {
1485
+ throw Object.assign(new Error('shadowMapExperiment.meshRegistrationRollbackFailed'), {
1486
+ cause: error,
1487
+ });
1488
+ }
1489
+ pendingMeshRegistrationRollbacks.delete(mesh);
1490
+ }
1491
+ if (!(plan ?? activatingRecoveryPlan)?.enabled || !registry || bindings.has(mesh) || isDisposed(mesh))
1492
+ return false;
1493
+ const renderableId = resolveRenderableId(mesh, objectByRenderable);
1494
+ if (!renderableId)
1495
+ return false;
1496
+ const object = objectByRenderable.get(renderableId);
1497
+ if (!object || (!object.cast && !object.receive))
1498
+ return false;
1499
+ let enabledObserver = null;
1500
+ const ancestorEnabledObservers = [];
1501
+ let disposeObserver = null;
1502
+ let bindingCommitted = false;
1503
+ try {
1504
+ const syncEnabledState = () => {
1505
+ if (!disposed && !disposing) {
1506
+ try {
1507
+ syncRenderable(renderableId);
1508
+ }
1509
+ catch (error) {
1510
+ pendingRenderableMembershipRetries.set(renderableId, 'renderable');
1511
+ requestRefreshFrame('shadow-map-experiment-membership-retry');
1512
+ if (!(error instanceof Error)
1513
+ || error.message !== 'shadowMapExperiment.automaticActivityCandidateCommitFailed') {
1514
+ throw error;
1515
+ }
1516
+ }
1517
+ }
1518
+ };
1519
+ enabledObserver = mesh.onEnabledStateChangedObservable.add(syncEnabledState);
1520
+ let ancestor = mesh.parent;
1521
+ while (ancestor) {
1522
+ ancestorEnabledObservers.push({
1523
+ node: ancestor,
1524
+ observer: ancestor.onEnabledStateChangedObservable.add(syncEnabledState),
1525
+ });
1526
+ ancestor = ancestor.parent;
1527
+ }
1528
+ disposeObserver = mesh.onDisposeObservable.add(() => requestMeshUnregistration(mesh));
1529
+ const meshes = meshesByRenderable.get(renderableId) ?? new Set();
1530
+ meshes.add(mesh);
1531
+ meshesByRenderable.set(renderableId, meshes);
1532
+ const binding = {
1533
+ mesh,
1534
+ entityId: object.entityId,
1535
+ renderableId,
1536
+ sceneFitLocalBounds: readMeshLocalBounds(mesh),
1537
+ influenceWorldBounds: undefined,
1538
+ influenceBoundingInfo: undefined,
1539
+ enabledObserver,
1540
+ ancestorEnabledObservers,
1541
+ disposeObserver,
1542
+ matrixObserver: null,
1543
+ worldMatrixSnapshot: null,
1544
+ };
1545
+ bindings.set(mesh, binding);
1546
+ bindingCommitted = true;
1547
+ if (options.coverageMode === 'scene-fit')
1548
+ sceneFitCoverageInvalidated = true;
1549
+ syncMatrixObserver(binding);
1550
+ syncRenderable(renderableId);
1551
+ if (!stagingPlanCandidateTransitions) {
1552
+ syncAutomaticActivityCandidateBindingsForRenderable(renderableId);
1553
+ }
1554
+ }
1555
+ catch (error) {
1556
+ const rollbackErrors = [];
1557
+ if (bindingCommitted) {
1558
+ try {
1559
+ unregisterMesh(mesh);
1560
+ }
1561
+ catch (rollbackError) {
1562
+ pendingMeshRegistrationRollbacks.add(mesh);
1563
+ requestRefreshFrame('shadow-map-experiment-mesh-registration-rollback-retry');
1564
+ rollbackErrors.push(rollbackError);
1565
+ }
1566
+ }
1567
+ else {
1568
+ pendingMeshObserverCleanups.set(mesh, {
1569
+ mesh,
1570
+ enabledObserver,
1571
+ ancestorEnabledObservers,
1572
+ disposeObserver,
1573
+ });
1574
+ rollbackErrors.push(...flushPendingMeshObserverCleanup(mesh));
1575
+ if (pendingMeshObserverCleanups.has(mesh)) {
1576
+ requestRefreshFrame('shadow-map-experiment-mesh-observer-cleanup-retry');
1577
+ }
1578
+ }
1579
+ if (rollbackErrors.length > 0 && error && typeof error === 'object') {
1580
+ attachFailureMetadata(error, { registrationRollbackErrors: Object.freeze(rollbackErrors) });
1581
+ }
1582
+ throw error;
1583
+ }
1584
+ return true;
1585
+ };
1586
+ const flushPendingMeshObserverCleanup = (mesh) => {
1587
+ const pending = pendingMeshObserverCleanups.get(mesh);
1588
+ if (!pending)
1589
+ return Object.freeze([]);
1590
+ const cleanupErrors = [];
1591
+ if (pending.disposeObserver) {
1592
+ try {
1593
+ pending.mesh.onDisposeObservable.remove(pending.disposeObserver);
1594
+ pending.disposeObserver = null;
1595
+ }
1596
+ catch (error) {
1597
+ cleanupErrors.push(error);
1598
+ }
1599
+ }
1600
+ if (pending.enabledObserver) {
1601
+ try {
1602
+ pending.mesh.onEnabledStateChangedObservable.remove(pending.enabledObserver);
1603
+ pending.enabledObserver = null;
1604
+ }
1605
+ catch (error) {
1606
+ cleanupErrors.push(error);
1607
+ }
1608
+ }
1609
+ for (const binding of [...pending.ancestorEnabledObservers]) {
1610
+ try {
1611
+ binding.node.onEnabledStateChangedObservable.remove(binding.observer);
1612
+ pending.ancestorEnabledObservers.splice(pending.ancestorEnabledObservers.indexOf(binding), 1);
1613
+ }
1614
+ catch (error) {
1615
+ cleanupErrors.push(error);
1616
+ }
1617
+ }
1618
+ if (!pending.disposeObserver
1619
+ && !pending.enabledObserver
1620
+ && pending.ancestorEnabledObservers.length === 0) {
1621
+ pendingMeshObserverCleanups.delete(mesh);
1622
+ }
1623
+ return Object.freeze(cleanupErrors);
1624
+ };
1625
+ const flushPendingMeshObserverCleanups = () => {
1626
+ for (const mesh of [...pendingMeshObserverCleanups.keys()]) {
1627
+ flushPendingMeshObserverCleanup(mesh);
1628
+ }
1629
+ };
1630
+ const flushPendingMeshRegistrationRollbacks = () => {
1631
+ for (const mesh of [...pendingMeshRegistrationRollbacks]) {
1632
+ try {
1633
+ unregisterMesh(mesh);
1634
+ pendingMeshRegistrationRollbacks.delete(mesh);
1635
+ }
1636
+ catch {
1637
+ // Ownership remains in the queue and the post-flush wake predicate
1638
+ // schedules the next bounded attempt.
1639
+ }
1640
+ }
1641
+ };
1642
+ const flushPendingSceneMeshRegistrations = () => {
1643
+ for (const mesh of [...pendingSceneMeshRegistrations]) {
1644
+ if (!scene.meshes.includes(mesh)) {
1645
+ pendingSceneMeshRegistrations.delete(mesh);
1646
+ continue;
1647
+ }
1648
+ registerMesh(mesh);
1649
+ pendingSceneMeshRegistrations.delete(mesh);
1650
+ }
1651
+ };
1652
+ const deactivate = () => {
1653
+ pendingCompositeRefreshReasons.clear();
1654
+ // A proposed candidate view is unpublished state. Revoking it before any
1655
+ // fallible backend cleanup prevents a later retry frame from committing
1656
+ // that stale proposal after the Plan has been disabled or replaced.
1657
+ pendingAutomaticActivityCandidateTransition = null;
1658
+ const cleanupErrors = [];
1659
+ const attempt = (operation) => {
1660
+ try {
1661
+ operation();
1662
+ return true;
1663
+ }
1664
+ catch (error) {
1665
+ cleanupErrors.push(error);
1666
+ return false;
1667
+ }
1668
+ };
1669
+ for (const target of [...pendingReceiverRestorations.keys()]) {
1670
+ attempt(() => restorePendingReceiver(target));
1671
+ }
1672
+ for (const [mesh, binding] of [...bindings]) {
1673
+ let cleaned = true;
1674
+ cleaned = attempt(() => mesh.onEnabledStateChangedObservable.remove(binding.enabledObserver)) && cleaned;
1675
+ for (const ancestorBinding of binding.ancestorEnabledObservers) {
1676
+ cleaned = attempt(() => {
1677
+ ancestorBinding.node.onEnabledStateChangedObservable.remove(ancestorBinding.observer);
1678
+ }) && cleaned;
1679
+ }
1680
+ cleaned = attempt(() => mesh.onDisposeObservable.remove(binding.disposeObserver)) && cleaned;
1681
+ if (binding.matrixObserver) {
1682
+ cleaned = attempt(() => mesh.onAfterWorldMatrixUpdateObservable.remove(binding.matrixObserver)) && cleaned;
1683
+ }
1684
+ cleaned = attempt(() => restoreReceiver(mesh)) && cleaned;
1685
+ if (depthBackend) {
1686
+ cleaned = attempt(() => depthBackend.removeCaster(mesh)) && cleaned;
1687
+ }
1688
+ cleaned = attempt(() => activityScheduler.setEntityActive(binding.entityId, false)) && cleaned;
1689
+ if (cleaned)
1690
+ bindings.delete(mesh);
1691
+ if (cleaned)
1692
+ pendingMeshRegistrationRollbacks.delete(mesh);
1693
+ }
1694
+ for (const mesh of [...pendingMeshObserverCleanups.keys()]) {
1695
+ cleanupErrors.push(...flushPendingMeshObserverCleanup(mesh));
1696
+ }
1697
+ for (const [mesh, state] of [...receiverStates]) {
1698
+ pendingReceiverRestorations.set(state.mesh, state.previousReceiveShadows);
1699
+ if (attempt(() => {
1700
+ if (!isDisposed(state.mesh) && state.mesh.receiveShadows === state.ownedValue) {
1701
+ writeReceiver(state.mesh, state.previousReceiveShadows);
1702
+ }
1703
+ else {
1704
+ pendingReceiverRestorations.delete(state.mesh);
1705
+ }
1706
+ }))
1707
+ receiverStates.delete(mesh);
1708
+ }
1709
+ if (bindings.size === 0) {
1710
+ if (automaticActivityCandidateBindings.size > 0) {
1711
+ automaticActivityCandidateBindings.clear();
1712
+ bumpAutomaticActivityCandidateRevision();
1713
+ }
1714
+ meshesByRenderable.clear();
1715
+ candidateLayerByMesh.clear();
1716
+ candidateMeshCounts.static = 0;
1717
+ candidateMeshCounts.dynamic = 0;
1718
+ conservativeCasterMeshes.clear();
1719
+ meshDataCacheByMesh.clear();
1720
+ pendingMeshRegistrationRollbacks.clear();
1721
+ pendingMeshObserverRestorations.clear();
1722
+ pendingSceneMeshRegistrations.clear();
1723
+ pendingMeshUnregistrations.clear();
1724
+ pendingRenderableMembershipRetries.clear();
1725
+ pendingTransformMembershipRetries.clear();
1726
+ activeCasterEntityIds.clear();
1727
+ pendingFinalDeformationEntityIds.clear();
1728
+ pendingDeformationInvalidations.clear();
1729
+ pendingMembershipInvalidationLayers.clear();
1730
+ pendingCoverageMembershipRebuild = false;
1731
+ objectByRenderable.clear();
1732
+ objectByEntity.clear();
1733
+ }
1734
+ receiverTargetByMesh.clear();
1735
+ if (registry) {
1736
+ const ownedRegistry = registry;
1737
+ if (attempt(() => ownedRegistry.dispose()))
1738
+ registry = null;
1739
+ }
1740
+ if (depthBackend) {
1741
+ const ownedBackend = depthBackend;
1742
+ if (attempt(() => ownedBackend.dispose())) {
1743
+ depthBackend = null;
1744
+ generator = null;
1745
+ }
1746
+ }
1747
+ if (pendingBackendAllocationCleanup) {
1748
+ const cleanup = pendingBackendAllocationCleanup;
1749
+ if (attempt(() => {
1750
+ const errors = cleanup();
1751
+ if (errors.length > 0) {
1752
+ throw Object.assign(new Error('shadowMapExperiment.backendAllocationCleanupFailed'), { cleanupErrors: errors });
1753
+ }
1754
+ }))
1755
+ pendingBackendAllocationCleanup = null;
1756
+ }
1757
+ if (!depthBackend && !pendingBackendAllocationCleanup)
1758
+ pendingFallbackReason = null;
1759
+ if (lightState) {
1760
+ const ownedLightState = lightState;
1761
+ if (attempt(() => restoreLight(ownedLightState)))
1762
+ lightState = null;
1763
+ }
1764
+ if (previousSceneShadowsEnabled !== null) {
1765
+ const ownedSceneShadowsEnabled = previousSceneShadowsEnabled;
1766
+ if (attempt(() => { scene.shadowsEnabled = ownedSceneShadowsEnabled; }))
1767
+ previousSceneShadowsEnabled = null;
1768
+ }
1769
+ effectiveCoverageCenter = null;
1770
+ effectiveCoverageExtent = null;
1771
+ effectiveCoverageNear = null;
1772
+ effectiveCoverageFar = null;
1773
+ if (cleanupErrors.length > 0) {
1774
+ throw Object.assign(new Error('shadowMapExperiment.deactivationFailed'), { cleanupErrors });
1775
+ }
1776
+ };
1777
+ const activate = (nextPlan) => {
1778
+ const light = resolveBabylonDirectionalLight(scene, nextPlan.light.entityId);
1779
+ if (!light)
1780
+ throw new Error(`shadowMapExperiment.lightMissing:${nextPlan.light.entityId}`);
1781
+ const existingGenerators = readShadowGenerators(light);
1782
+ if (existingGenerators.length > 0)
1783
+ throw new Error(`shadowMapExperiment.backendConflict:${nextPlan.light.entityId}`);
1784
+ previousSceneShadowsEnabled = scene.shadowsEnabled;
1785
+ influenceCulling.configure(nextPlan.culling.guardBandRatio);
1786
+ lightState = captureLight(light);
1787
+ effectiveCoverageCenter = nextPlan.generator.bounds.center;
1788
+ effectiveCoverageExtent = nextPlan.generator.bounds.extent;
1789
+ effectiveCoverageNear = nextPlan.generator.bounds.near;
1790
+ effectiveCoverageFar = nextPlan.generator.bounds.far;
1791
+ configureLight(light, nextPlan, {
1792
+ center: nextPlan.generator.bounds.center,
1793
+ extent: nextPlan.generator.bounds.extent,
1794
+ near: nextPlan.generator.bounds.near,
1795
+ far: nextPlan.generator.bounds.far,
1796
+ });
1797
+ scene.shadowsEnabled = true;
1798
+ light.shadowEnabled = true;
1799
+ try {
1800
+ depthBackend = createBabylonShadowMapExperimentDepthBackend(scene, light, nextPlan, {
1801
+ onCompositorLifecycleEvent: handleCompositorLifecycleEvent,
1802
+ });
1803
+ }
1804
+ catch (error) {
1805
+ rememberBackendAllocationCleanup(error);
1806
+ throw error;
1807
+ }
1808
+ generator = depthBackend.generator;
1809
+ if (depthBackend.fallbackReason)
1810
+ warnForFallback(depthBackend.fallbackReason);
1811
+ registry = createShadowMapExperimentRegistry(nextPlan);
1812
+ for (const object of nextPlan.objects) {
1813
+ objectByEntity.set(object.entityId, object);
1814
+ for (const renderableId of object.renderableIds)
1815
+ objectByRenderable.set(renderableId, object);
1816
+ }
1817
+ updateCameraInfluenceCoverage();
1818
+ stagingPlanCandidateTransitions = true;
1819
+ try {
1820
+ for (const mesh of scene.meshes)
1821
+ registerMesh(mesh);
1822
+ }
1823
+ finally {
1824
+ stagingPlanCandidateTransitions = false;
1825
+ }
1826
+ const nextCandidates = new Map();
1827
+ for (const binding of bindings.values()) {
1828
+ updateAutomaticActivityCandidateBinding(nextCandidates, binding, objectByRenderable.get(binding.renderableId));
1829
+ }
1830
+ if (!applyAutomaticActivityCandidateTransition(nextCandidates)
1831
+ && pendingAutomaticAnimationGroupAttachments.size === 0
1832
+ && pendingAutomaticAnimationGroupDetachCleanups.size === 0) {
1833
+ throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
1834
+ }
1835
+ markAllDepthDirty('generator-created');
1836
+ };
1837
+ const registerActivitySource = (input) => {
1838
+ assertUsable();
1839
+ const controller = activityScheduler.registerSource(input);
1840
+ const deformationSource = input.kind === 'animation' || input.kind === 'morph';
1841
+ let sourceActive = false;
1842
+ let controllerDisposed = false;
1843
+ const updateDeformationSourceCount = (previousActive, nextActive) => {
1844
+ if (!deformationSource || previousActive === nextActive)
1845
+ return;
1846
+ const previousCount = activeDeformationSourceCountByEntity.get(input.entityId) ?? 0;
1847
+ if (nextActive) {
1848
+ activeDeformationSourceCountByEntity.set(input.entityId, previousCount + 1);
1849
+ pendingFinalDeformationEntityIds.delete(input.entityId);
1850
+ return;
1851
+ }
1852
+ if (previousCount <= 1) {
1853
+ activeDeformationSourceCountByEntity.delete(input.entityId);
1854
+ if (activeCasterEntityIds.has(input.entityId))
1855
+ pendingFinalDeformationEntityIds.add(input.entityId);
1856
+ }
1857
+ else {
1858
+ activeDeformationSourceCountByEntity.set(input.entityId, previousCount - 1);
1859
+ }
1860
+ };
1861
+ return Object.freeze({
1862
+ entityId: controller.entityId,
1863
+ kind: controller.kind,
1864
+ sourceId: controller.sourceId,
1865
+ setActive(nextActive) {
1866
+ if (disposed || disposing || controllerDisposed || sourceActive === nextActive)
1867
+ return false;
1868
+ if (!controller.setActive(nextActive))
1869
+ return false;
1870
+ updateDeformationSourceCount(sourceActive, nextActive);
1871
+ sourceActive = nextActive;
1872
+ requestRefreshFrame(`shadow-map-experiment-${nextActive ? 'caster-activity-started' : 'caster-activity-stopped'}`);
1873
+ return true;
1874
+ },
1875
+ invalidate() {
1876
+ if (disposed || disposing || controllerDisposed)
1877
+ return false;
1878
+ const invalidated = controller.invalidate();
1879
+ if (!invalidated)
1880
+ return false;
1881
+ const reason = dirtyReasonForActivityKind(input.kind);
1882
+ if (deformationSource) {
1883
+ const invalidationReasons = pendingDeformationInvalidations.get(input.entityId) ?? new Set();
1884
+ invalidationReasons.add(reason);
1885
+ pendingDeformationInvalidations.set(input.entityId, invalidationReasons);
1886
+ }
1887
+ requestRefreshFrame(`shadow-map-experiment-${reason}`);
1888
+ return true;
1889
+ },
1890
+ dispose() {
1891
+ if (controllerDisposed)
1892
+ return;
1893
+ const wasActive = sourceActive;
1894
+ updateDeformationSourceCount(sourceActive, false);
1895
+ sourceActive = false;
1896
+ controllerDisposed = true;
1897
+ controller.dispose();
1898
+ if (wasActive)
1899
+ requestRefreshFrame('shadow-map-experiment-caster-activity-stopped');
1900
+ },
1901
+ });
1902
+ };
1903
+ const resolveAutomaticActivityTargets = (group, candidateView = automaticActivityCandidateBindings) => {
1904
+ const desired = new Map();
1905
+ if (candidateView.size === 0)
1906
+ return desired;
1907
+ const targets = new Set(group.targetedAnimations.map(targetedAnimation => targetedAnimation.target));
1908
+ // Build the candidate relationship index once per group reconciliation.
1909
+ // A skinned AnimationGroup commonly contains one targeted animation per
1910
+ // linked bone TransformNode. Resolving every target by rescanning every
1911
+ // candidate skeleton makes playback start O(targets × meshes × bones).
1912
+ const targetIndex = createAutomaticActivityTargetIndex(candidateView, targets);
1913
+ for (const target of targets) {
1914
+ const resolvedTarget = resolveAutomaticActivityTarget(candidateView, target, objectByRenderable, targetIndex);
1915
+ for (const mesh of resolvedTarget.meshes) {
1916
+ const meshBinding = candidateView.get(mesh);
1917
+ const object = meshBinding ? objectByEntity.get(meshBinding.entityId) : null;
1918
+ if (!meshBinding || !isAutomaticActivityCandidateObject(object))
1919
+ continue;
1920
+ const key = `${meshBinding.entityId}\u0000${resolvedTarget.kind}`;
1921
+ desired.set(key, { entityId: meshBinding.entityId, kind: resolvedTarget.kind });
1922
+ }
1923
+ }
1924
+ return desired;
1925
+ };
1926
+ const updateAutomaticAnimationGroupTargetIndex = (binding, targets) => {
1927
+ const removeBinding = (index, key) => {
1928
+ const indexedBindings = index.get(key);
1929
+ indexedBindings?.delete(binding);
1930
+ if (indexedBindings?.size === 0)
1931
+ index.delete(key);
1932
+ };
1933
+ for (const key of binding.resolvedTargetIndexKeys) {
1934
+ removeBinding(automaticAnimationGroupsByResolvedTarget, key);
1935
+ }
1936
+ for (const key of binding.resolvedTargetAncestorIndexKeys) {
1937
+ removeBinding(automaticAnimationGroupsByResolvedTargetAncestor, key);
1938
+ }
1939
+ for (const key of binding.resolvedTargetRenderableIndexKeys) {
1940
+ removeBinding(automaticAnimationGroupsByResolvedTargetRenderable, key);
1941
+ }
1942
+ const targetKeys = new Set();
1943
+ const ancestorKeys = new Set();
1944
+ const renderableKeys = new Set();
1945
+ for (const target of targets) {
1946
+ if (!target || (typeof target !== 'object' && typeof target !== 'function'))
1947
+ continue;
1948
+ targetKeys.add(target);
1949
+ let current = target;
1950
+ let ancestor = false;
1951
+ const visited = new Set();
1952
+ while (current && (typeof current === 'object' || typeof current === 'function')) {
1953
+ if (visited.has(current))
1954
+ break;
1955
+ visited.add(current);
1956
+ if (ancestor)
1957
+ ancestorKeys.add(current);
1958
+ for (const identity of collectIdentityCandidates(current)) {
1959
+ renderableKeys.add(identity);
1960
+ }
1961
+ current = current.parent ?? null;
1962
+ ancestor = true;
1963
+ }
1964
+ }
1965
+ for (const key of targetKeys) {
1966
+ addMapSetValue(automaticAnimationGroupsByResolvedTarget, key, binding);
1967
+ }
1968
+ for (const key of ancestorKeys) {
1969
+ addMapSetValue(automaticAnimationGroupsByResolvedTargetAncestor, key, binding);
1970
+ }
1971
+ for (const key of renderableKeys) {
1972
+ addMapSetValue(automaticAnimationGroupsByResolvedTargetRenderable, key, binding);
1973
+ }
1974
+ binding.resolvedTargetIndexKeys = Object.freeze([...targetKeys]);
1975
+ binding.resolvedTargetAncestorIndexKeys = Object.freeze([...ancestorKeys]);
1976
+ binding.resolvedTargetRenderableIndexKeys = Object.freeze([...renderableKeys]);
1977
+ };
1978
+ const collectAutomaticActivityCandidateRelationshipKeys = (mesh) => {
1979
+ const keys = new Set();
1980
+ let current = mesh;
1981
+ while (current && (typeof current === 'object' || typeof current === 'function')) {
1982
+ if (keys.has(current))
1983
+ break;
1984
+ keys.add(current);
1985
+ current = current.parent ?? null;
1986
+ }
1987
+ const skeleton = mesh.skeleton;
1988
+ if (skeleton) {
1989
+ keys.add(skeleton);
1990
+ for (const bone of skeleton.bones ?? []) {
1991
+ keys.add(bone);
1992
+ const transformNode = callOptional(bone, 'getTransformNode')
1993
+ ?? bone?._linkedTransformNode
1994
+ ?? null;
1995
+ if (transformNode)
1996
+ keys.add(transformNode);
1997
+ }
1998
+ }
1999
+ const manager = mesh.morphTargetManager;
2000
+ const count = manager?.numTargets ?? 0;
2001
+ if (manager?.getTarget && Number.isInteger(count) && count > 0) {
2002
+ for (let index = 0; index < count; index += 1) {
2003
+ try {
2004
+ const target = manager.getTarget(index);
2005
+ if (target)
2006
+ keys.add(target);
2007
+ }
2008
+ catch {
2009
+ // A single malformed morph slot must not make the candidate delta
2010
+ // fan out to every AnimationGroup.
2011
+ }
2012
+ }
2013
+ }
2014
+ return Object.freeze([...keys]);
2015
+ };
2016
+ const collectAutomaticAnimationGroupsAffectedByCandidateTransition = (nextCandidates) => {
2017
+ const affected = new Set();
2018
+ const changedCandidates = [];
2019
+ const changedEntityIds = new Set();
2020
+ for (const mesh of new Set([
2021
+ ...automaticActivityCandidateBindings.keys(),
2022
+ ...nextCandidates.keys(),
2023
+ ])) {
2024
+ const previous = automaticActivityCandidateBindings.get(mesh);
2025
+ const next = nextCandidates.get(mesh);
2026
+ if (previous?.entityId === next?.entityId
2027
+ && previous?.renderableId === next?.renderableId)
2028
+ continue;
2029
+ changedCandidates.push({ mesh, previous, next });
2030
+ if (previous)
2031
+ changedEntityIds.add(previous.entityId);
2032
+ if (next)
2033
+ changedEntityIds.add(next.entityId);
2034
+ }
2035
+ for (const binding of automaticAnimationGroups.values()) {
2036
+ if (!binding.ready)
2037
+ continue;
2038
+ if (binding.rebindPending
2039
+ || binding.resolvedCandidateRevision !== automaticActivityCandidateRevision
2040
+ || [...binding.sources.values()].some(source => changedEntityIds.has(source.entityId))) {
2041
+ affected.add(binding);
2042
+ }
2043
+ }
2044
+ const includeIndexedBindings = (index, key) => {
2045
+ for (const binding of index.get(key) ?? []) {
2046
+ if (binding.ready)
2047
+ affected.add(binding);
2048
+ }
2049
+ };
2050
+ for (const { mesh, previous, next } of changedCandidates) {
2051
+ for (const key of collectAutomaticActivityCandidateRelationshipKeys(mesh)) {
2052
+ includeIndexedBindings(automaticAnimationGroupsByResolvedTarget, key);
2053
+ }
2054
+ // A target below the candidate mesh is related, but a target below a
2055
+ // sibling that merely shares a generic root is not.
2056
+ includeIndexedBindings(automaticAnimationGroupsByResolvedTargetAncestor, mesh);
2057
+ for (const renderableId of new Set([
2058
+ previous?.renderableId,
2059
+ next?.renderableId,
2060
+ ].filter((value) => typeof value === 'string'))) {
2061
+ includeIndexedBindings(automaticAnimationGroupsByResolvedTargetRenderable, renderableId);
2062
+ }
2063
+ }
2064
+ return affected;
2065
+ };
2066
+ const shouldFullyResolveAutomaticActivityTargetsOnPlay = (binding) => {
2067
+ if (binding.resolvedCandidateRevision !== automaticActivityCandidateRevision)
2068
+ return false;
2069
+ const currentTargets = binding.group.targetedAnimations.map(targetedAnimation => targetedAnimation.target);
2070
+ if (currentTargets.length !== binding.resolvedTargets.length
2071
+ || currentTargets.some((target, index) => target !== binding.resolvedTargets[index])) {
2072
+ return true;
2073
+ }
2074
+ // A negative binding has no owner-scoped subset to validate. Preserve the
2075
+ // restart boundary that lets a previously unresolved target become owned.
2076
+ if (binding.sources.size === 0)
2077
+ return true;
2078
+ const sourceEntityIds = new Set([...binding.sources.values()].map(source => source.entityId));
2079
+ const sourceCandidates = new Map();
2080
+ for (const [mesh, candidate] of automaticActivityCandidateBindings) {
2081
+ if (sourceEntityIds.has(candidate.entityId))
2082
+ sourceCandidates.set(mesh, candidate);
2083
+ }
2084
+ const desired = resolveAutomaticActivityTargets(binding.group, sourceCandidates);
2085
+ return desired.size !== binding.sources.size
2086
+ || [...desired.keys()].some(key => !binding.sources.has(key));
2087
+ };
2088
+ const syncAutomaticAnimationGroupPlayback = (binding) => {
2089
+ const active = binding.group.isPlaying === true;
2090
+ for (const source of binding.sources.values())
2091
+ source.controller.setActive(active);
2092
+ binding.active = active;
2093
+ };
2094
+ const cleanupStagedAutomaticAnimationGroupSources = (binding) => {
2095
+ const cleanupErrors = [];
2096
+ for (const [key, source] of [...binding.stagedSources.entries()].reverse()) {
2097
+ try {
2098
+ source.controller.setActive(false);
2099
+ }
2100
+ catch (error) {
2101
+ cleanupErrors.push(error);
2102
+ }
2103
+ try {
2104
+ source.controller.dispose();
2105
+ binding.stagedSources.delete(key);
2106
+ }
2107
+ catch (error) {
2108
+ cleanupErrors.push(error);
2109
+ }
2110
+ }
2111
+ if (cleanupErrors.length > 0) {
2112
+ pendingAutomaticAnimationGroupSourceCleanups.add(binding);
2113
+ }
2114
+ return Object.freeze(cleanupErrors);
2115
+ };
2116
+ const prepareAutomaticAnimationGroupRebind = (binding, candidateView, forceTargetResolution = false) => {
2117
+ const shouldResolveTargets = forceTargetResolution
2118
+ || binding.resolvedCandidateRevision !== automaticActivityCandidateRevision;
2119
+ if (!shouldResolveTargets)
2120
+ return false;
2121
+ automaticActivityTargetResolutionCount += 1;
2122
+ if (binding.stagedSources.size > 0) {
2123
+ const cleanupErrors = cleanupStagedAutomaticAnimationGroupSources(binding);
2124
+ if (cleanupErrors.length > 0) {
2125
+ throw cleanupErrors[0];
2126
+ }
2127
+ }
2128
+ const desired = resolveAutomaticActivityTargets(binding.group, candidateView);
2129
+ if (desired.size !== binding.sources.size
2130
+ || [...desired.keys()].some(key => !binding.sources.has(key))) {
2131
+ binding.rebindPending = true;
2132
+ binding.resolvedCandidateRevision = -1;
2133
+ }
2134
+ // Source replacement has one commit boundary: every stale committed
2135
+ // source must be fully disposed before any replacement is acquired.
2136
+ for (const [key, source] of [...binding.sources]) {
2137
+ if (desired.has(key))
2138
+ continue;
2139
+ binding.rebindPending = true;
2140
+ try {
2141
+ source.controller.setActive(false);
2142
+ source.controller.dispose();
2143
+ binding.sources.delete(key);
2144
+ }
2145
+ catch (error) {
2146
+ binding.resolvedCandidateRevision = -1;
2147
+ pendingAutomaticAnimationGroupSourceCleanups.add(binding);
2148
+ throw error;
2149
+ }
2150
+ }
2151
+ try {
2152
+ for (const [key, descriptor] of desired) {
2153
+ if (binding.sources.has(key) || binding.stagedSources.has(key))
2154
+ continue;
2155
+ const sourceId = `fps:auto:animation-group:${binding.group.uniqueId}:${descriptor.kind}`;
2156
+ const controller = system.registerCasterActivitySource({ ...descriptor, sourceId });
2157
+ binding.stagedSources.set(key, { ...descriptor, controller });
2158
+ }
2159
+ }
2160
+ catch (error) {
2161
+ cleanupStagedAutomaticAnimationGroupSources(binding);
2162
+ throw error;
2163
+ }
2164
+ return true;
2165
+ };
2166
+ const commitPreparedAutomaticAnimationGroupRebind = (binding, candidateRevision) => {
2167
+ for (const [key, source] of binding.stagedSources) {
2168
+ binding.sources.set(key, source);
2169
+ }
2170
+ binding.stagedSources.clear();
2171
+ binding.rebindPending = false;
2172
+ binding.resolvedCandidateRevision = candidateRevision;
2173
+ const resolvedTargets = Object.freeze(binding.group.targetedAnimations.map(targetedAnimation => targetedAnimation.target));
2174
+ binding.resolvedTargets = resolvedTargets;
2175
+ updateAutomaticAnimationGroupTargetIndex(binding, resolvedTargets);
2176
+ };
2177
+ const reconcileAutomaticAnimationGroup = (binding, forceTargetResolution = false) => {
2178
+ const prepared = prepareAutomaticAnimationGroupRebind(binding, automaticActivityCandidateBindings, forceTargetResolution);
2179
+ if (prepared) {
2180
+ commitPreparedAutomaticAnimationGroupRebind(binding, automaticActivityCandidateRevision);
2181
+ }
2182
+ syncAutomaticAnimationGroupPlayback(binding);
2183
+ };
2184
+ const safelyReconcileAutomaticAnimationGroup = (binding, forceTargetResolution = false) => {
2185
+ if (disposed || disposing || !binding.ready)
2186
+ return true;
2187
+ try {
2188
+ reconcileAutomaticAnimationGroup(binding, forceTargetResolution);
2189
+ pendingAutomaticAnimationGroupReconciliations.delete(binding);
2190
+ return true;
2191
+ }
2192
+ catch (error) {
2193
+ pendingAutomaticAnimationGroupReconciliations.add(binding);
2194
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-reconcile-retry');
2195
+ if (pendingAutomaticAnimationGroupSourceCleanups.has(binding)) {
2196
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-cleanup-retry');
2197
+ }
2198
+ rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} could not bind shadow activity: ${error instanceof Error ? error.message : String(error)}`);
2199
+ return false;
2200
+ }
2201
+ };
2202
+ const attachAutomaticAnimationGroup = (group, deferInitialReconcile = false) => {
2203
+ if (automaticAnimationGroups.has(group) || disposed || disposing)
2204
+ return;
2205
+ const binding = {
2206
+ group,
2207
+ playObserver: null,
2208
+ pauseObserver: null,
2209
+ endObserver: null,
2210
+ sources: new Map(),
2211
+ stagedSources: new Map(),
2212
+ active: false,
2213
+ ready: false,
2214
+ rebindPending: false,
2215
+ resolvedCandidateRevision: -1,
2216
+ resolvedTargets: Object.freeze([]),
2217
+ resolvedTargetIndexKeys: Object.freeze([]),
2218
+ resolvedTargetAncestorIndexKeys: Object.freeze([]),
2219
+ resolvedTargetRenderableIndexKeys: Object.freeze([]),
2220
+ };
2221
+ automaticAnimationGroups.set(group, binding);
2222
+ try {
2223
+ binding.playObserver = group.onAnimationGroupPlayObservable.add(() => {
2224
+ if (!binding.ready)
2225
+ return;
2226
+ safelyReconcileAutomaticAnimationGroup(binding, shouldFullyResolveAutomaticActivityTargetsOnPlay(binding));
2227
+ requestRefreshFrame('shadow-map-experiment-animation-group-played');
2228
+ });
2229
+ binding.pauseObserver = group.onAnimationGroupPauseObservable.add(() => {
2230
+ if (!binding.ready)
2231
+ return;
2232
+ try {
2233
+ syncAutomaticAnimationGroupPlayback(binding);
2234
+ }
2235
+ catch (error) {
2236
+ rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} could not update shadow activity: ${error instanceof Error ? error.message : String(error)}`);
2237
+ }
2238
+ });
2239
+ binding.endObserver = group.onAnimationGroupEndObservable.add(() => {
2240
+ if (!binding.ready)
2241
+ return;
2242
+ try {
2243
+ syncAutomaticAnimationGroupPlayback(binding);
2244
+ }
2245
+ catch (error) {
2246
+ rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} could not update shadow activity: ${error instanceof Error ? error.message : String(error)}`);
2247
+ }
2248
+ });
2249
+ binding.ready = true;
2250
+ if (!deferInitialReconcile)
2251
+ safelyReconcileAutomaticAnimationGroup(binding);
2252
+ pendingAutomaticAnimationGroupAttachments.delete(group);
2253
+ }
2254
+ catch (error) {
2255
+ binding.ready = false;
2256
+ const cleanupErrors = detachAutomaticAnimationGroup(group);
2257
+ if (cleanupErrors.length > 0 && error && typeof error === 'object') {
2258
+ attachFailureMetadata(error, { cleanupErrors });
2259
+ }
2260
+ throw error;
2261
+ }
2262
+ };
2263
+ const detachAutomaticAnimationGroup = (group) => {
2264
+ const binding = automaticAnimationGroups.get(group);
2265
+ if (!binding)
2266
+ return Object.freeze([]);
2267
+ binding.ready = false;
2268
+ updateAutomaticAnimationGroupTargetIndex(binding, Object.freeze([]));
2269
+ pendingAutomaticAnimationGroupReconciliations.delete(binding);
2270
+ const cleanupErrors = [...cleanupAutomaticAnimationGroupSources(binding)];
2271
+ pendingAutomaticAnimationGroupSourceCleanups.delete(binding);
2272
+ if (binding.playObserver) {
2273
+ try {
2274
+ group.onAnimationGroupPlayObservable.remove(binding.playObserver);
2275
+ binding.playObserver = null;
2276
+ }
2277
+ catch (error) {
2278
+ cleanupErrors.push(error);
2279
+ }
2280
+ }
2281
+ if (binding.pauseObserver) {
2282
+ try {
2283
+ group.onAnimationGroupPauseObservable.remove(binding.pauseObserver);
2284
+ binding.pauseObserver = null;
2285
+ }
2286
+ catch (error) {
2287
+ cleanupErrors.push(error);
2288
+ }
2289
+ }
2290
+ if (binding.endObserver) {
2291
+ try {
2292
+ group.onAnimationGroupEndObservable.remove(binding.endObserver);
2293
+ binding.endObserver = null;
2294
+ }
2295
+ catch (error) {
2296
+ cleanupErrors.push(error);
2297
+ }
2298
+ }
2299
+ if (cleanupErrors.length === 0
2300
+ && binding.sources.size === 0
2301
+ && binding.stagedSources.size === 0
2302
+ && !binding.playObserver
2303
+ && !binding.pauseObserver
2304
+ && !binding.endObserver) {
2305
+ automaticAnimationGroups.delete(group);
2306
+ pendingAutomaticAnimationGroupDetachCleanups.delete(binding);
2307
+ }
2308
+ else {
2309
+ pendingAutomaticAnimationGroupDetachCleanups.add(binding);
2310
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-detach-retry');
2311
+ }
2312
+ return Object.freeze(cleanupErrors);
2313
+ };
2314
+ const flushPendingAutomaticAnimationGroupDetachCleanups = () => {
2315
+ if (pendingAutomaticAnimationGroupDetachCleanups.size === 0)
2316
+ return;
2317
+ for (const binding of [...pendingAutomaticAnimationGroupDetachCleanups]) {
2318
+ const group = binding.group;
2319
+ const cleanupErrors = detachAutomaticAnimationGroup(group);
2320
+ if (cleanupErrors.length > 0)
2321
+ continue;
2322
+ if (!pendingAutomaticActivityCandidateTransition
2323
+ && automaticActivityCandidateBindings.size > 0
2324
+ && scene.animationGroups.includes(group)) {
2325
+ try {
2326
+ attachAutomaticAnimationGroup(group);
2327
+ pendingAutomaticAnimationGroupAttachments.delete(group);
2328
+ }
2329
+ catch (error) {
2330
+ pendingAutomaticAnimationGroupAttachments.add(group);
2331
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-attach-retry');
2332
+ rememberDiagnostic('shadowMapExperiment.automaticActivityObserverFailed', `AnimationGroup ${group.name || group.uniqueId} could not be reattached after cleanup: ${error instanceof Error ? error.message : String(error)}`);
2333
+ }
2334
+ }
2335
+ }
2336
+ };
2337
+ const flushPendingAutomaticAnimationGroupAttachments = () => {
2338
+ // The proposed candidate transition owns observer acquisition while it is
2339
+ // pending. Attaching here would reconcile against the old committed view.
2340
+ if (pendingAutomaticActivityCandidateTransition)
2341
+ return;
2342
+ for (const group of [...pendingAutomaticAnimationGroupAttachments]) {
2343
+ if (!scene.animationGroups.includes(group) || automaticActivityCandidateBindings.size === 0) {
2344
+ pendingAutomaticAnimationGroupAttachments.delete(group);
2345
+ continue;
2346
+ }
2347
+ try {
2348
+ attachAutomaticAnimationGroup(group);
2349
+ pendingAutomaticAnimationGroupAttachments.delete(group);
2350
+ }
2351
+ catch {
2352
+ // Retain ownership in the queue; the post-flush wake predicate retries.
2353
+ }
2354
+ }
2355
+ };
2356
+ const flushPendingAutomaticAnimationGroupReconciliations = () => {
2357
+ // A candidate transition owns source acquisition until it either commits
2358
+ // or rolls back. Rebinding an individual group against the previously
2359
+ // committed view here would recreate the stale source set mid-transaction.
2360
+ if (pendingAutomaticActivityCandidateTransition)
2361
+ return;
2362
+ for (const binding of [...pendingAutomaticAnimationGroupReconciliations]) {
2363
+ if (!binding.ready || !automaticAnimationGroups.has(binding.group)) {
2364
+ pendingAutomaticAnimationGroupReconciliations.delete(binding);
2365
+ continue;
2366
+ }
2367
+ safelyReconcileAutomaticAnimationGroup(binding);
2368
+ }
2369
+ };
2370
+ const cleanupAutomaticAnimationGroupSources = (binding) => {
2371
+ const cleanupErrors = [];
2372
+ binding.active = false;
2373
+ for (const [key, source] of [...binding.sources.entries()].reverse()) {
2374
+ try {
2375
+ source.controller.setActive(false);
2376
+ }
2377
+ catch (error) {
2378
+ cleanupErrors.push(error);
2379
+ }
2380
+ try {
2381
+ source.controller.dispose();
2382
+ binding.sources.delete(key);
2383
+ }
2384
+ catch (error) {
2385
+ cleanupErrors.push(error);
2386
+ }
2387
+ }
2388
+ for (const [key, source] of [...binding.stagedSources.entries()].reverse()) {
2389
+ try {
2390
+ source.controller.setActive(false);
2391
+ }
2392
+ catch (error) {
2393
+ cleanupErrors.push(error);
2394
+ }
2395
+ try {
2396
+ source.controller.dispose();
2397
+ binding.stagedSources.delete(key);
2398
+ }
2399
+ catch (error) {
2400
+ cleanupErrors.push(error);
2401
+ }
2402
+ }
2403
+ return Object.freeze(cleanupErrors);
2404
+ };
2405
+ const enterAutomaticAnimationGroupDormant = (binding) => {
2406
+ const cleanupErrors = cleanupAutomaticAnimationGroupSources(binding);
2407
+ binding.resolvedCandidateRevision = automaticActivityCandidateRevision;
2408
+ if (cleanupErrors.length === 0 && binding.sources.size === 0 && binding.stagedSources.size === 0) {
2409
+ binding.rebindPending = false;
2410
+ pendingAutomaticAnimationGroupSourceCleanups.delete(binding);
2411
+ return;
2412
+ }
2413
+ binding.rebindPending = true;
2414
+ pendingAutomaticAnimationGroupSourceCleanups.add(binding);
2415
+ rememberDiagnostic('shadowMapExperiment.automaticActivityCleanupFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} activity cleanup failed.`);
2416
+ };
2417
+ const flushPendingAutomaticAnimationGroupSourceCleanups = () => {
2418
+ if (pendingAutomaticAnimationGroupSourceCleanups.size === 0)
2419
+ return;
2420
+ for (const binding of [...pendingAutomaticAnimationGroupSourceCleanups]) {
2421
+ const cleanupErrors = cleanupAutomaticAnimationGroupSources(binding);
2422
+ if (cleanupErrors.length > 0) {
2423
+ rememberDiagnostic('shadowMapExperiment.automaticActivityCleanupFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} activity cleanup failed.`);
2424
+ }
2425
+ else if (binding.sources.size === 0 && binding.stagedSources.size === 0) {
2426
+ if (!pendingAutomaticActivityCandidateTransition)
2427
+ binding.rebindPending = false;
2428
+ pendingAutomaticAnimationGroupSourceCleanups.delete(binding);
2429
+ if (!pendingAutomaticActivityCandidateTransition
2430
+ && automaticActivityCandidateBindings.size > 0
2431
+ && binding.ready) {
2432
+ safelyReconcileAutomaticAnimationGroup(binding);
2433
+ }
2434
+ }
2435
+ }
2436
+ if (pendingAutomaticAnimationGroupSourceCleanups.size > 0) {
2437
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-cleanup-retry');
2438
+ }
2439
+ };
2440
+ const haveSameAutomaticActivityCandidateView = (left, right) => left.size === right.size && [...left].every(([mesh, binding]) => {
2441
+ const other = right.get(mesh);
2442
+ return other?.entityId === binding.entityId
2443
+ && other.renderableId === binding.renderableId;
2444
+ });
2445
+ applyAutomaticActivityCandidateTransition = (nextCandidates) => {
2446
+ if (haveSameAutomaticActivityCandidateView(automaticActivityCandidateBindings, nextCandidates)) {
2447
+ pendingAutomaticActivityCandidateTransition = null;
2448
+ return true;
2449
+ }
2450
+ if (suppressAutomaticActivityCandidateRevision) {
2451
+ // A full backend rebuild whose authored candidate signature is unchanged
2452
+ // preserves the already-owned automatic sources. Only replace the
2453
+ // equivalent mesh-binding view; there is no topology publication.
2454
+ automaticActivityCandidateBindings.clear();
2455
+ for (const [mesh, binding] of nextCandidates) {
2456
+ automaticActivityCandidateBindings.set(mesh, binding);
2457
+ }
2458
+ pendingAutomaticActivityCandidateTransition = null;
2459
+ return true;
2460
+ }
2461
+ pendingAutomaticActivityCandidateTransition = new Map(nextCandidates);
2462
+ if (pendingAutomaticAnimationGroupDetachCleanups.size > 0) {
2463
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-detach-retry');
2464
+ return false;
2465
+ }
2466
+ if (pendingAutomaticAnimationGroupSourceCleanups.size > 0) {
2467
+ flushPendingAutomaticAnimationGroupSourceCleanups();
2468
+ if (pendingAutomaticAnimationGroupSourceCleanups.size > 0) {
2469
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-cleanup-retry');
2470
+ return false;
2471
+ }
2472
+ }
2473
+ if (nextCandidates.size > 0) {
2474
+ for (const group of scene.animationGroups) {
2475
+ try {
2476
+ // The proposed candidate view owns initial source resolution. Only
2477
+ // acquire the lifecycle observers here; resolving against the old
2478
+ // committed view would publish a stale source set mid-transition.
2479
+ attachAutomaticAnimationGroup(group, true);
2480
+ }
2481
+ catch (error) {
2482
+ pendingAutomaticAnimationGroupAttachments.add(group);
2483
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-attach-retry');
2484
+ rememberDiagnostic('shadowMapExperiment.automaticActivityObserverFailed', `AnimationGroup ${group.name || group.uniqueId} could not be observed: ${error instanceof Error ? error.message : String(error)}`);
2485
+ return false;
2486
+ }
2487
+ }
2488
+ }
2489
+ const affectedBindings = collectAutomaticAnimationGroupsAffectedByCandidateTransition(nextCandidates);
2490
+ const preparedBindings = [];
2491
+ try {
2492
+ for (const binding of affectedBindings) {
2493
+ prepareAutomaticAnimationGroupRebind(binding, nextCandidates, true);
2494
+ preparedBindings.push(binding);
2495
+ }
2496
+ }
2497
+ catch (error) {
2498
+ for (const binding of preparedBindings) {
2499
+ cleanupStagedAutomaticAnimationGroupSources(binding);
2500
+ }
2501
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-reconcile-retry');
2502
+ rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `Automatic candidate transition could not prepare activity sources: ${error instanceof Error ? error.message : String(error)}`);
2503
+ return false;
2504
+ }
2505
+ const nextRevision = automaticActivityCandidateRevision + 1;
2506
+ automaticActivityCandidateBindings.clear();
2507
+ for (const [mesh, binding] of nextCandidates) {
2508
+ automaticActivityCandidateBindings.set(mesh, binding);
2509
+ }
2510
+ automaticActivityCandidateRevision = nextRevision;
2511
+ for (const binding of preparedBindings) {
2512
+ commitPreparedAutomaticAnimationGroupRebind(binding, nextRevision);
2513
+ try {
2514
+ syncAutomaticAnimationGroupPlayback(binding);
2515
+ pendingAutomaticAnimationGroupReconciliations.delete(binding);
2516
+ }
2517
+ catch (error) {
2518
+ pendingAutomaticAnimationGroupReconciliations.add(binding);
2519
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-reconcile-retry');
2520
+ rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} could not activate committed shadow activity: ${error instanceof Error ? error.message : String(error)}`);
2521
+ }
2522
+ }
2523
+ for (const binding of automaticAnimationGroups.values()) {
2524
+ if (binding.ready && !affectedBindings.has(binding)) {
2525
+ binding.resolvedCandidateRevision = nextRevision;
2526
+ }
2527
+ }
2528
+ pendingAutomaticActivityCandidateTransition = null;
2529
+ return true;
2530
+ };
2531
+ flushPendingAutomaticActivityCandidateTransition = () => {
2532
+ if (!pendingAutomaticActivityCandidateTransition)
2533
+ return;
2534
+ applyAutomaticActivityCandidateTransition(new Map(pendingAutomaticActivityCandidateTransition));
2535
+ };
2536
+ const reconcileAutomaticAnimationGroups = () => {
2537
+ if (automaticActivityCandidateBindings.size === 0) {
2538
+ pendingAutomaticAnimationGroupAttachments.clear();
2539
+ pendingAutomaticAnimationGroupReconciliations.clear();
2540
+ for (const binding of automaticAnimationGroups.values())
2541
+ enterAutomaticAnimationGroupDormant(binding);
2542
+ if (pendingAutomaticAnimationGroupSourceCleanups.size > 0) {
2543
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-cleanup-retry');
2544
+ }
2545
+ return pendingAutomaticAnimationGroupSourceCleanups.size === 0;
2546
+ }
2547
+ // A candidate can recover while an earlier dormant cleanup is still
2548
+ // retrying. Finish that cleanup before acquiring a new observer set.
2549
+ flushPendingAutomaticAnimationGroupSourceCleanups();
2550
+ for (const group of scene.animationGroups) {
2551
+ try {
2552
+ attachAutomaticAnimationGroup(group);
2553
+ }
2554
+ catch (error) {
2555
+ pendingAutomaticAnimationGroupAttachments.add(group);
2556
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-attach-retry');
2557
+ rememberDiagnostic('shadowMapExperiment.automaticActivityObserverFailed', `AnimationGroup ${group.name || group.uniqueId} could not be observed: ${error instanceof Error ? error.message : String(error)}`);
2558
+ }
2559
+ }
2560
+ let committed = true;
2561
+ for (const binding of automaticAnimationGroups.values()) {
2562
+ if (!pendingAutomaticAnimationGroupSourceCleanups.has(binding)) {
2563
+ committed = safelyReconcileAutomaticAnimationGroup(binding) && committed;
2564
+ }
2565
+ }
2566
+ return committed && pendingAutomaticAnimationGroupSourceCleanups.size === 0;
2567
+ };
2568
+ const applyPlanInPlace = (previousPlan, nextPlan, runtimeOverlayMeshes = null, previousCandidateObjects = previousPlan.objects) => {
2569
+ if (!registry || !generator || !depthBackend)
2570
+ throw new Error('shadowMapExperiment.backendUnavailable');
2571
+ const delta = diffShadowMapExperimentPlans(previousPlan, nextPlan);
2572
+ const appearanceUpdate = delta.appearanceChanged
2573
+ ? depthBackend.setAppearance(resolveShadowMapExperimentAppearance(nextPlan.generator))
2574
+ : null;
2575
+ plan = nextPlan;
2576
+ if (delta.appearanceChanged) {
2577
+ requestRefreshFrame('shadow-map-experiment-appearance-changed');
2578
+ if (appearanceUpdate?.compositeRefreshRequired)
2579
+ markCompositeDepthDirty('quality-changed');
2580
+ }
2581
+ if (delta.appearanceOnly)
2582
+ return;
2583
+ if (delta.lightDirectionChanged && lightState) {
2584
+ configureLight(lightState.light, nextPlan, {
2585
+ center: effectiveCoverageCenter ?? nextPlan.generator.bounds.center,
2586
+ extent: effectiveCoverageExtent ?? nextPlan.generator.bounds.extent,
2587
+ near: effectiveCoverageNear ?? nextPlan.generator.bounds.near,
2588
+ far: effectiveCoverageFar ?? nextPlan.generator.bounds.far,
2589
+ });
2590
+ influenceCulling.configure(nextPlan.culling.guardBandRatio);
2591
+ pendingCoverageMembershipRebuild = nextPlan.culling.enabled;
2592
+ cullingStable = !nextPlan.culling.enabled;
2593
+ lastCullingInvalidationReason = 'light-direction-changed';
2594
+ }
2595
+ objectByRenderable.clear();
2596
+ objectByEntity.clear();
2597
+ for (const object of nextPlan.objects) {
2598
+ objectByEntity.set(object.entityId, object);
2599
+ for (const renderableId of object.renderableIds)
2600
+ objectByRenderable.set(renderableId, object);
2601
+ }
2602
+ activityScheduler.applyPlan(nextPlan);
2603
+ registry.applyPlan(nextPlan);
2604
+ invalidateSceneFitBounds();
2605
+ stagingPlanCandidateTransitions = true;
2606
+ try {
2607
+ const bindingsToReconcile = runtimeOverlayMeshes
2608
+ ? [...runtimeOverlayMeshes].flatMap(mesh => {
2609
+ const binding = bindings.get(mesh);
2610
+ return binding ? [[mesh, binding]] : [];
2611
+ })
2612
+ : [...bindings];
2613
+ for (const [mesh, binding] of bindingsToReconcile) {
2614
+ const resolvedId = resolveRenderableId(mesh, objectByRenderable);
2615
+ const resolvedObject = resolvedId ? objectByRenderable.get(resolvedId) : null;
2616
+ const resolvedEntityId = resolvedObject?.entityId ?? null;
2617
+ if (resolvedId !== binding.renderableId
2618
+ || resolvedEntityId !== binding.entityId
2619
+ || (!resolvedObject?.cast && !resolvedObject?.receive))
2620
+ unregisterMesh(mesh);
2621
+ else {
2622
+ syncMatrixObserver(binding);
2623
+ syncRenderable(binding.renderableId);
2624
+ }
2625
+ }
2626
+ for (const mesh of runtimeOverlayMeshes ?? scene.meshes)
2627
+ registerMesh(mesh);
2628
+ }
2629
+ finally {
2630
+ stagingPlanCandidateTransitions = false;
2631
+ }
2632
+ if (delta.lightDirectionChanged) {
2633
+ syncSceneFitCoverage(true);
2634
+ updateCameraInfluenceCoverage();
2635
+ }
2636
+ if (!haveSameAutomaticActivityCandidates(previousPlan, nextPlan, previousCandidateObjects)) {
2637
+ const nextCandidates = new Map();
2638
+ for (const binding of bindings.values()) {
2639
+ updateAutomaticActivityCandidateBinding(nextCandidates, binding, objectByRenderable.get(binding.renderableId));
2640
+ }
2641
+ if (!applyAutomaticActivityCandidateTransition(nextCandidates)) {
2642
+ throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
2643
+ }
2644
+ }
2645
+ for (const reason of delta.dirtyReasons) {
2646
+ if (reason === 'quality-changed'
2647
+ && delta.appearanceChanged
2648
+ && !delta.generatorResourcesChanged)
2649
+ continue;
2650
+ registry.markDirty(reason);
2651
+ enqueueRefresh(reason);
2652
+ }
2653
+ };
2654
+ const flushPendingPlanRollbackRecovery = () => {
2655
+ const recovery = pendingPlanRollbackRecovery;
2656
+ if (!recovery)
2657
+ return;
2658
+ if (recovery.phase === 'deactivate') {
2659
+ try {
2660
+ deactivate();
2661
+ }
2662
+ catch { /* queue ownership remains for the next frame */ }
2663
+ if (hasOwnedBackendState()) {
2664
+ requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
2665
+ return;
2666
+ }
2667
+ if (!recovery.targetPlan?.enabled) {
2668
+ pendingPlanRollbackRecovery = null;
2669
+ return;
2670
+ }
2671
+ recovery.phase = 'activate';
2672
+ }
2673
+ const targetPlan = recovery.targetPlan;
2674
+ if (!targetPlan)
2675
+ return;
2676
+ try {
2677
+ if (!recovery.activated) {
2678
+ // `plan` and the scheduler intentionally remain null until activation
2679
+ // and automatic candidate/source publication have both completed.
2680
+ activityScheduler.applyPlan(targetPlan);
2681
+ activatingRecoveryPlan = targetPlan;
2682
+ allowingRecoveryActivityRegistration = true;
2683
+ try {
2684
+ activate(targetPlan);
2685
+ }
2686
+ finally {
2687
+ activatingRecoveryPlan = null;
2688
+ }
2689
+ recovery.activated = true;
2690
+ }
2691
+ allowingRecoveryActivityRegistration = true;
2692
+ try {
2693
+ flushPendingAutomaticAnimationGroupDetachCleanups();
2694
+ flushPendingAutomaticAnimationGroupSourceCleanups();
2695
+ flushPendingAutomaticActivityCandidateTransition();
2696
+ flushPendingAutomaticAnimationGroupAttachments();
2697
+ flushPendingAutomaticAnimationGroupReconciliations();
2698
+ }
2699
+ finally {
2700
+ allowingRecoveryActivityRegistration = false;
2701
+ }
2702
+ if (pendingAutomaticActivityCandidateTransition
2703
+ || pendingAutomaticAnimationGroupSourceCleanups.size > 0
2704
+ || pendingAutomaticAnimationGroupDetachCleanups.size > 0
2705
+ || pendingAutomaticAnimationGroupAttachments.size > 0
2706
+ || pendingAutomaticAnimationGroupReconciliations.size > 0) {
2707
+ requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
2708
+ return;
2709
+ }
2710
+ for (const entityId of recovery.activeEntityIds) {
2711
+ activityScheduler.setEntityActive(entityId, true);
2712
+ syncActivityEligibility(entityId);
2713
+ }
2714
+ plan = targetPlan;
2715
+ automaticActivityCandidateRevision = recovery.candidateRevision;
2716
+ for (const binding of automaticAnimationGroups.values()) {
2717
+ binding.resolvedCandidateRevision = recovery.candidateRevision;
2718
+ }
2719
+ pendingPlanRollbackRecovery = null;
2720
+ }
2721
+ catch {
2722
+ allowingRecoveryActivityRegistration = false;
2723
+ plan = null;
2724
+ activityScheduler.applyPlan(null);
2725
+ recovery.phase = 'deactivate';
2726
+ recovery.activated = false;
2727
+ try {
2728
+ deactivate();
2729
+ }
2730
+ catch { /* retained for bounded retry */ }
2731
+ requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
2732
+ }
2733
+ };
2734
+ const rollbackPlanUpdate = (previousPlan, error, previousCandidateRevision = automaticActivityCandidateRevision) => {
2735
+ let cleanupError = null;
2736
+ try {
2737
+ deactivate();
2738
+ }
2739
+ catch (deactivationError) {
2740
+ cleanupError = deactivationError;
2741
+ }
2742
+ plan = null;
2743
+ activityScheduler.applyPlan(null);
2744
+ automaticActivityCandidateRevision = previousCandidateRevision;
2745
+ if (cleanupError || hasOwnedBackendState()) {
2746
+ pendingPlanRollbackRecovery = {
2747
+ targetPlan: previousPlan,
2748
+ candidateRevision: previousCandidateRevision,
2749
+ activeEntityIds: new Set(activeCasterEntityIds),
2750
+ phase: 'deactivate',
2751
+ activated: false,
2752
+ };
2753
+ requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
2754
+ if (cleanupError && error && typeof error === 'object') {
2755
+ attachFailureMetadata(error, { rollbackDeactivationErrors: Object.freeze([cleanupError]) });
2756
+ }
2757
+ throw error;
2758
+ }
2759
+ if (previousPlan?.enabled) {
2760
+ pendingPlanRollbackRecovery = {
2761
+ targetPlan: previousPlan,
2762
+ candidateRevision: previousCandidateRevision,
2763
+ activeEntityIds: new Set(activeCasterEntityIds),
2764
+ phase: 'activate',
2765
+ activated: false,
2766
+ };
2767
+ requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
2768
+ }
2769
+ else {
2770
+ automaticActivityCandidateRevision = previousCandidateRevision;
2771
+ }
2772
+ throw error;
2773
+ };
2774
+ const hasOwnedBackendState = () => bindings.size > 0
2775
+ || pendingAutomaticActivityCandidateTransition !== null
2776
+ || pendingMeshObserverCleanups.size > 0
2777
+ || pendingMeshObserverRestorations.size > 0
2778
+ || pendingMeshRegistrationRollbacks.size > 0
2779
+ || pendingSceneMeshRegistrations.size > 0
2780
+ || pendingMeshUnregistrations.size > 0
2781
+ || pendingRenderableMembershipRetries.size > 0
2782
+ || pendingTransformMembershipRetries.size > 0
2783
+ || receiverStates.size > 0
2784
+ || pendingReceiverRestorations.size > 0
2785
+ || registry !== null
2786
+ || depthBackend !== null
2787
+ || pendingBackendAllocationCleanup !== null
2788
+ || generator !== null
2789
+ || lightState !== null
2790
+ || previousSceneShadowsEnabled !== null;
2791
+ const applyEffectivePlan = (value, allowDisposing = false, runtimeOverlayMeshes = null) => {
2792
+ assertUsable(allowDisposing);
2793
+ // Every caller supplies either a source snapshot or composeEffectivePlan()
2794
+ // output. Re-cloning the full Plan for each runtime entity made a batch of
2795
+ // synchronous overlay additions scale with the complete authored scene.
2796
+ const nextPlan = value;
2797
+ const previousPlan = plan;
2798
+ const previousRuntimeOverlayObjects = [...runtimeRegistrations.values()]
2799
+ .filter(record => !record.disposed)
2800
+ .flatMap(record => {
2801
+ const object = objectByEntity.get(record.entityId);
2802
+ return object ? [object] : [];
2803
+ });
2804
+ // `plan.objects` may be a lazy operational snapshot (missing active
2805
+ // overlays) or a previously materialized effective Plan (retaining a
2806
+ // disposed overlay). Rebuild only the candidate topology from the
2807
+ // authored source and live overlays, never from that mixed snapshot.
2808
+ const previousCandidateObjects = sourcePlan
2809
+ ? [...sourcePlan.objects, ...previousRuntimeOverlayObjects]
2810
+ : previousPlan?.objects ?? [];
2811
+ const previousCandidateRevision = automaticActivityCandidateRevision;
2812
+ if (previousPlan?.enabled && nextPlan?.enabled) {
2813
+ const delta = diffShadowMapExperimentPlans(previousPlan, nextPlan);
2814
+ const currentLight = resolveBabylonDirectionalLight(scene, nextPlan.light.entityId);
2815
+ const backendStillAttached = currentLight !== null
2816
+ && lightState?.light === currentLight
2817
+ && depthBackend?.isAttachedTo(currentLight) === true;
2818
+ if (!delta.generatorResourcesChanged && backendStillAttached) {
2819
+ try {
2820
+ applyPlanInPlace(previousPlan, nextPlan, runtimeOverlayMeshes, previousCandidateObjects);
2821
+ return;
2822
+ }
2823
+ catch (error) {
2824
+ rollbackPlanUpdate(previousPlan, error, previousCandidateRevision);
2825
+ }
2826
+ }
2827
+ }
2828
+ const preserveAutomaticCandidates = previousPlan?.enabled === true
2829
+ && nextPlan?.enabled === true
2830
+ && haveSameAutomaticActivityCandidates(previousPlan, nextPlan, previousCandidateObjects);
2831
+ suppressAutomaticActivityCandidateRevision = preserveAutomaticCandidates;
2832
+ try {
2833
+ deactivate();
2834
+ plan = nextPlan;
2835
+ activityScheduler.applyPlan(nextPlan?.enabled ? nextPlan : null);
2836
+ if (!nextPlan?.enabled) {
2837
+ reconcileAutomaticAnimationGroups();
2838
+ return;
2839
+ }
2840
+ try {
2841
+ activate(nextPlan);
2842
+ }
2843
+ catch (error) {
2844
+ rollbackPlanUpdate(previousPlan, error, previousCandidateRevision);
2845
+ }
2846
+ }
2847
+ finally {
2848
+ suppressAutomaticActivityCandidateRevision = false;
2849
+ }
2850
+ };
2851
+ const setPlan = (value) => {
2852
+ assertUsable();
2853
+ assertNoPlanRollbackRecovery();
2854
+ const nextSourcePlan = value
2855
+ ? deepFreezeShadowMapExperimentPlan(createShadowMapExperimentPlanSnapshot(value))
2856
+ : null;
2857
+ const records = [...runtimeRegistrations.values()].filter(record => !record.disposed);
2858
+ const nextEffectivePlan = composeEffectivePlan(nextSourcePlan, records, runtimeOverlayRevision);
2859
+ const previousSourcePlan = sourcePlan;
2860
+ const previousMaterializedEffectivePlan = materializedEffectivePlan;
2861
+ try {
2862
+ applyEffectivePlan(nextEffectivePlan);
2863
+ sourcePlan = nextSourcePlan;
2864
+ materializedEffectivePlan = nextEffectivePlan;
2865
+ }
2866
+ catch (error) {
2867
+ sourcePlan = previousSourcePlan;
2868
+ materializedEffectivePlan = previousMaterializedEffectivePlan;
2869
+ throw error;
2870
+ }
2871
+ };
2872
+ const previewLightDirection = (entityId, direction) => {
2873
+ assertUsable();
2874
+ assertNoPlanRollbackRecovery();
2875
+ if (!sourcePlan?.enabled
2876
+ || !plan?.enabled
2877
+ || !registry
2878
+ || !lightState
2879
+ || sourcePlan.light.entityId !== entityId)
2880
+ return false;
2881
+ if (areTuple3Equal(sourcePlan.light.direction, direction))
2882
+ return true;
2883
+ const nextLight = Object.freeze({
2884
+ ...sourcePlan.light,
2885
+ direction: Object.freeze([...direction]),
2886
+ });
2887
+ const nextSourcePlan = Object.freeze({
2888
+ ...sourcePlan,
2889
+ light: nextLight,
2890
+ });
2891
+ const nextOperationalPlan = Object.freeze({ ...plan, light: nextLight });
2892
+ const nextMaterializedEffectivePlan = materializedEffectivePlan
2893
+ ? Object.freeze({ ...materializedEffectivePlan, light: nextLight })
2894
+ : null;
2895
+ const previousPlan = plan;
2896
+ const previousCandidateRevision = automaticActivityCandidateRevision;
2897
+ const previousLightState = captureLight(lightState.light);
2898
+ try {
2899
+ configureLight(lightState.light, nextOperationalPlan, {
2900
+ center: effectiveCoverageCenter ?? nextOperationalPlan.generator.bounds.center,
2901
+ extent: effectiveCoverageExtent ?? nextOperationalPlan.generator.bounds.extent,
2902
+ near: effectiveCoverageNear ?? nextOperationalPlan.generator.bounds.near,
2903
+ far: effectiveCoverageFar ?? nextOperationalPlan.generator.bounds.far,
2904
+ });
2905
+ }
2906
+ catch (error) {
2907
+ try {
2908
+ restoreLight(previousLightState);
2909
+ }
2910
+ catch (rollbackError) {
2911
+ if (error && typeof error === 'object') {
2912
+ attachFailureMetadata(error, {
2913
+ lightPreviewRollbackErrors: Object.freeze([rollbackError]),
2914
+ });
2915
+ }
2916
+ rollbackPlanUpdate(previousPlan, error, previousCandidateRevision);
2917
+ }
2918
+ throw error;
2919
+ }
2920
+ sourcePlan = nextSourcePlan;
2921
+ plan = nextOperationalPlan;
2922
+ materializedEffectivePlan = nextMaterializedEffectivePlan;
2923
+ influenceCulling.configure(nextOperationalPlan.culling.guardBandRatio);
2924
+ pendingCoverageMembershipRebuild = nextOperationalPlan.culling.enabled;
2925
+ cullingStable = !nextOperationalPlan.culling.enabled;
2926
+ lastCullingInvalidationReason = 'light-direction-changed';
2927
+ registry.markDirty('light-direction-changed');
2928
+ enqueueRefresh('light-direction-changed');
2929
+ return true;
2930
+ };
2931
+ const previewAppearance = (value) => {
2932
+ assertUsable();
2933
+ assertNoPlanRollbackRecovery();
2934
+ if (!sourcePlan?.enabled || !plan?.enabled || !depthBackend)
2935
+ return false;
2936
+ const nextAppearance = resolveShadowMapExperimentAppearance(value);
2937
+ const currentAppearance = resolveShadowMapExperimentAppearance(sourcePlan.generator);
2938
+ if (nextAppearance.darkness === currentAppearance.darkness
2939
+ && nextAppearance.softness === currentAppearance.softness)
2940
+ return true;
2941
+ setPlan(Object.freeze({
2942
+ ...sourcePlan,
2943
+ generator: Object.freeze({
2944
+ ...sourcePlan.generator,
2945
+ darkness: nextAppearance.darkness,
2946
+ softness: nextAppearance.softness,
2947
+ }),
2948
+ }));
2949
+ return true;
2950
+ };
2951
+ const { newMeshObserver, removedMeshObserver, frameObserver, newAnimationGroupObserver, removedAnimationGroupObserver, } = (() => {
2952
+ let acquiredNewMeshObserver = null;
2953
+ let acquiredRemovedMeshObserver = null;
2954
+ let acquiredFrameObserver = null;
2955
+ let acquiredNewAnimationGroupObserver = null;
2956
+ let acquiredRemovedAnimationGroupObserver = null;
2957
+ try {
2958
+ acquiredNewMeshObserver = scene.onNewMeshAddedObservable.add(mesh => {
2959
+ sceneOwnedMeshes.add(mesh);
2960
+ // Babylon announces a mesh from its constructor before every identity and
2961
+ // visibility field is guaranteed to be initialized. Bind after that
2962
+ // constructor completes; this is still an event-local operation, not a scan.
2963
+ if (disposed || disposing || !scene.meshes.includes(mesh))
2964
+ return;
2965
+ pendingSceneMeshRegistrations.add(mesh);
2966
+ requestRefreshFrame('shadow-map-experiment-mesh-registration-retry');
2967
+ queueMicrotask(() => {
2968
+ if (disposed || disposing || !pendingSceneMeshRegistrations.has(mesh))
2969
+ return;
2970
+ if (!scene.meshes.includes(mesh)) {
2971
+ pendingSceneMeshRegistrations.delete(mesh);
2972
+ return;
2973
+ }
2974
+ try {
2975
+ registerMesh(mesh);
2976
+ pendingSceneMeshRegistrations.delete(mesh);
2977
+ }
2978
+ catch {
2979
+ pendingSceneMeshRegistrations.add(mesh);
2980
+ requestRefreshFrame('shadow-map-experiment-mesh-registration-retry');
2981
+ }
2982
+ });
2983
+ });
2984
+ acquiredRemovedMeshObserver = scene.onMeshRemovedObservable.add(mesh => {
2985
+ sceneOwnedMeshes.delete(mesh);
2986
+ requestMeshUnregistration(mesh);
2987
+ });
2988
+ acquiredNewAnimationGroupObserver = scene.onNewAnimationGroupAddedObservable.add(group => {
2989
+ if (automaticActivityCandidateBindings.size === 0)
2990
+ return;
2991
+ try {
2992
+ attachAutomaticAnimationGroup(group);
2993
+ }
2994
+ catch (error) {
2995
+ pendingAutomaticAnimationGroupAttachments.add(group);
2996
+ requestRefreshFrame('shadow-map-experiment-automatic-activity-attach-retry');
2997
+ rememberDiagnostic('shadowMapExperiment.automaticActivityObserverFailed', `AnimationGroup ${group.name || group.uniqueId} could not be observed: ${error instanceof Error ? error.message : String(error)}`);
2998
+ }
2999
+ });
3000
+ acquiredRemovedAnimationGroupObserver = scene.onAnimationGroupRemovedObservable.add(group => {
3001
+ const cleanupErrors = detachAutomaticAnimationGroup(group);
3002
+ if (cleanupErrors.length > 0) {
3003
+ rememberDiagnostic('shadowMapExperiment.automaticActivityCleanupFailed', `AnimationGroup ${group.name || group.uniqueId} activity cleanup failed.`);
3004
+ }
3005
+ });
3006
+ acquiredFrameObserver = scene.onBeforeRenderObservable.add(() => flushRefresh());
3007
+ if (automaticActivityCandidateBindings.size > 0) {
3008
+ for (const group of scene.animationGroups)
3009
+ attachAutomaticAnimationGroup(group);
3010
+ }
3011
+ return {
3012
+ newMeshObserver: acquiredNewMeshObserver,
3013
+ removedMeshObserver: acquiredRemovedMeshObserver,
3014
+ frameObserver: acquiredFrameObserver,
3015
+ newAnimationGroupObserver: acquiredNewAnimationGroupObserver,
3016
+ removedAnimationGroupObserver: acquiredRemovedAnimationGroupObserver,
3017
+ };
3018
+ }
3019
+ catch (error) {
3020
+ const cleanup = () => {
3021
+ const cleanupErrors = [];
3022
+ for (const group of [...automaticAnimationGroups.keys()].reverse()) {
3023
+ cleanupErrors.push(...detachAutomaticAnimationGroup(group));
3024
+ }
3025
+ if (acquiredRemovedAnimationGroupObserver) {
3026
+ try {
3027
+ scene.onAnimationGroupRemovedObservable.remove(acquiredRemovedAnimationGroupObserver);
3028
+ acquiredRemovedAnimationGroupObserver = null;
3029
+ }
3030
+ catch (cleanupError) {
3031
+ cleanupErrors.push(cleanupError);
3032
+ }
3033
+ }
3034
+ if (acquiredNewAnimationGroupObserver) {
3035
+ try {
3036
+ scene.onNewAnimationGroupAddedObservable.remove(acquiredNewAnimationGroupObserver);
3037
+ acquiredNewAnimationGroupObserver = null;
3038
+ }
3039
+ catch (cleanupError) {
3040
+ cleanupErrors.push(cleanupError);
3041
+ }
3042
+ }
3043
+ if (acquiredFrameObserver) {
3044
+ try {
3045
+ scene.onBeforeRenderObservable.remove(acquiredFrameObserver);
3046
+ acquiredFrameObserver = null;
3047
+ }
3048
+ catch (cleanupError) {
3049
+ cleanupErrors.push(cleanupError);
3050
+ }
3051
+ }
3052
+ if (acquiredRemovedMeshObserver) {
3053
+ try {
3054
+ scene.onMeshRemovedObservable.remove(acquiredRemovedMeshObserver);
3055
+ acquiredRemovedMeshObserver = null;
3056
+ }
3057
+ catch (cleanupError) {
3058
+ cleanupErrors.push(cleanupError);
3059
+ }
3060
+ }
3061
+ if (acquiredNewMeshObserver) {
3062
+ try {
3063
+ scene.onNewMeshAddedObservable.remove(acquiredNewMeshObserver);
3064
+ acquiredNewMeshObserver = null;
3065
+ }
3066
+ catch (cleanupError) {
3067
+ cleanupErrors.push(cleanupError);
3068
+ }
3069
+ }
3070
+ return Object.freeze(cleanupErrors);
3071
+ };
3072
+ const cleanupErrors = cleanup();
3073
+ if (cleanupErrors.length > 0)
3074
+ throw createInitializationFailure(error, cleanup, cleanupErrors);
3075
+ throw error;
3076
+ }
3077
+ })();
3078
+ let disposeNewMeshObserverRemoved = false;
3079
+ let disposeRemovedMeshObserverRemoved = false;
3080
+ let disposeFrameObserverRemoved = false;
3081
+ let disposeNewAnimationGroupObserverRemoved = false;
3082
+ let disposeRemovedAnimationGroupObserverRemoved = false;
3083
+ let disposeActivitySchedulerDisposed = false;
3084
+ let disposeDepthRefreshSchedulerDisposed = false;
3085
+ let disposeInfluenceCullingDisposed = false;
3086
+ const disposeRuntimeRecordsForSystem = (records) => {
3087
+ const cleanupErrors = [];
3088
+ for (const record of [...records].reverse()) {
3089
+ for (const [sourceId, controller] of [...record.controllers.entries()].reverse()) {
3090
+ try {
3091
+ controller.setActive(false);
3092
+ }
3093
+ catch (error) {
3094
+ cleanupErrors.push(error);
3095
+ }
3096
+ try {
3097
+ controller.dispose();
3098
+ record.controllers.delete(sourceId);
3099
+ }
3100
+ catch (error) {
3101
+ cleanupErrors.push(error);
3102
+ }
3103
+ }
3104
+ if (record.controllers.size === 0) {
3105
+ runtimeRegistrations.delete(record.entityId);
3106
+ for (const renderable of record.renderables)
3107
+ runtimeOwnedMeshes.delete(renderable.mesh);
3108
+ explicitRuntimeActivityEntityIds.delete(record.entityId);
3109
+ record.disposed = true;
3110
+ }
3111
+ }
3112
+ return Object.freeze(cleanupErrors);
3113
+ };
3114
+ const applyRuntimeObjectDelta = (input) => {
3115
+ if (!sourcePlan || !plan?.enabled || !registry || !generator) {
3116
+ throw new Error('shadowMapExperiment.runtimePlanUnavailable');
3117
+ }
3118
+ runtimeIncrementalMutationCount += 1;
3119
+ const removedEntityIds = input.removedObjects.map(object => object.entityId);
3120
+ const removedRenderableIds = input.removedObjects.flatMap(object => object.renderableIds);
3121
+ updateOperationalPlanRevision(input.overlayRevision);
3122
+ for (const object of input.removedObjects) {
3123
+ objectByEntity.delete(object.entityId);
3124
+ for (const renderableId of object.renderableIds) {
3125
+ if (objectByRenderable.get(renderableId)?.entityId === object.entityId) {
3126
+ objectByRenderable.delete(renderableId);
3127
+ }
3128
+ }
3129
+ }
3130
+ for (const object of input.upsertedObjects) {
3131
+ objectByEntity.set(object.entityId, object);
3132
+ for (const renderableId of object.renderableIds)
3133
+ objectByRenderable.set(renderableId, object);
3134
+ }
3135
+ activityScheduler.applyObjectDelta({
3136
+ upsertedObjects: input.upsertedObjects,
3137
+ removedEntityIds,
3138
+ });
3139
+ registry.applyObjectDelta({
3140
+ planRevision: sourcePlan.revision + input.overlayRevision,
3141
+ upsertedObjects: input.upsertedObjects,
3142
+ removedRenderableIds,
3143
+ });
3144
+ invalidateSceneFitBounds();
3145
+ stagingPlanCandidateTransitions = true;
3146
+ try {
3147
+ for (const mesh of input.meshes) {
3148
+ const binding = bindings.get(mesh);
3149
+ if (!binding)
3150
+ continue;
3151
+ const resolvedId = resolveRenderableId(mesh, objectByRenderable);
3152
+ const resolvedObject = resolvedId ? objectByRenderable.get(resolvedId) : null;
3153
+ if (resolvedId !== binding.renderableId
3154
+ || resolvedObject?.entityId !== binding.entityId
3155
+ || (!resolvedObject.cast && !resolvedObject.receive)) {
3156
+ unregisterMesh(mesh);
3157
+ }
3158
+ else {
3159
+ syncMatrixObserver(binding);
3160
+ syncRenderable(binding.renderableId);
3161
+ }
3162
+ }
3163
+ for (const mesh of input.meshes)
3164
+ registerMesh(mesh);
3165
+ }
3166
+ finally {
3167
+ stagingPlanCandidateTransitions = false;
3168
+ }
3169
+ const candidateTopologyChanged = [...input.upsertedObjects, ...input.removedObjects]
3170
+ .some(isAutomaticActivityCandidateObject);
3171
+ if (candidateTopologyChanged) {
3172
+ const nextCandidates = new Map();
3173
+ for (const binding of bindings.values()) {
3174
+ updateAutomaticActivityCandidateBinding(nextCandidates, binding, objectByRenderable.get(binding.renderableId));
3175
+ }
3176
+ if (!applyAutomaticActivityCandidateTransition(nextCandidates)) {
3177
+ throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
3178
+ }
3179
+ }
3180
+ };
3181
+ const createRuntimeOwner = (records) => {
3182
+ const owner = Object.freeze({
3183
+ entityIds: Object.freeze(records.map(record => record.entityId).sort()),
3184
+ renderableIds: Object.freeze(records.flatMap(record => (record.renderables.map(renderable => renderable.renderableId))).sort()),
3185
+ getBehaviorProfileId(entityId) {
3186
+ return records.find(record => record.entityId === entityId && !record.disposed)?.behaviorProfileId ?? null;
3187
+ },
3188
+ getActivityController(entityId, sourceId) {
3189
+ const record = records.find(candidate => candidate.entityId === entityId && !candidate.disposed);
3190
+ return record?.controllers.get(sourceId) ?? null;
3191
+ },
3192
+ dispose() {
3193
+ if (records.every(record => record.disposed))
3194
+ return;
3195
+ assertUsable(true);
3196
+ if (pendingPlanRollbackRecovery && !disposing) {
3197
+ throw new Error('shadowMapExperiment.planRollbackRecoveryPending');
3198
+ }
3199
+ const cleanupErrors = [];
3200
+ for (const record of [...records].reverse()) {
3201
+ for (const sourceId of [...record.controllers.keys()].reverse()) {
3202
+ const controller = record.controllers.get(sourceId);
3203
+ if (!controller)
3204
+ continue;
3205
+ try {
3206
+ controller.setActive(false);
3207
+ }
3208
+ catch (error) {
3209
+ cleanupErrors.push(error);
3210
+ }
3211
+ try {
3212
+ controller.dispose();
3213
+ record.controllers.delete(sourceId);
3214
+ }
3215
+ catch (error) {
3216
+ cleanupErrors.push(error);
3217
+ }
3218
+ }
3219
+ }
3220
+ const controllersDisposed = records.every(record => record.controllers.size === 0);
3221
+ const anyRegistered = records.some(record => runtimeRegistrations.get(record.entityId) === record);
3222
+ if (controllersDisposed && anyRegistered) {
3223
+ const nextOverlayRevision = runtimeOverlayRevision + 1;
3224
+ const removedObjects = records.flatMap(record => {
3225
+ const object = objectByEntity.get(record.entityId);
3226
+ return object ? [object] : [];
3227
+ });
3228
+ const runtimeMeshes = collectRuntimeRecordMeshes(records);
3229
+ const previousCandidateRevision = automaticActivityCandidateRevision;
3230
+ try {
3231
+ applyRuntimeObjectDelta({
3232
+ upsertedObjects: Object.freeze([]),
3233
+ removedObjects,
3234
+ overlayRevision: nextOverlayRevision,
3235
+ meshes: runtimeMeshes,
3236
+ });
3237
+ for (const record of records) {
3238
+ runtimeRegistrations.delete(record.entityId);
3239
+ for (const renderable of record.renderables)
3240
+ runtimeOwnedMeshes.delete(renderable.mesh);
3241
+ explicitRuntimeActivityEntityIds.delete(record.entityId);
3242
+ }
3243
+ runtimeOverlayRevision = nextOverlayRevision;
3244
+ }
3245
+ catch (error) {
3246
+ try {
3247
+ rollbackPlanUpdate(composeEffectivePlan(sourcePlan, [...runtimeRegistrations.values()].filter(candidate => !candidate.disposed), runtimeOverlayRevision), error, previousCandidateRevision);
3248
+ }
3249
+ catch (rollbackError) {
3250
+ cleanupErrors.push(rollbackError);
3251
+ }
3252
+ }
3253
+ }
3254
+ if (controllersDisposed && records.every(record => runtimeRegistrations.get(record.entityId) !== record)) {
3255
+ for (const record of records)
3256
+ record.disposed = true;
3257
+ }
3258
+ if (cleanupErrors.length > 0) {
3259
+ const error = Object.assign(new Error('shadowMapExperiment.runtimeOwnerDisposeFailed'), {
3260
+ cleanupErrors: Object.freeze(cleanupErrors),
3261
+ runtimeShadowObjectsRecoveryOwner: owner,
3262
+ });
3263
+ throw error;
3264
+ }
3265
+ },
3266
+ });
3267
+ for (const record of records)
3268
+ record.owner = owner;
3269
+ return owner;
3270
+ };
3271
+ const collectRuntimeRecordMeshes = (records) => {
3272
+ const meshes = new Set();
3273
+ for (const record of records) {
3274
+ for (const renderable of record.renderables) {
3275
+ meshes.add(renderable.mesh);
3276
+ for (const mesh of meshesByRenderable.get(renderable.renderableId) ?? [])
3277
+ meshes.add(mesh);
3278
+ }
3279
+ }
3280
+ return meshes;
3281
+ };
3282
+ const isMeshOwnedByScene = (mesh) => {
3283
+ if (mesh.getScene() !== scene || isDisposed(mesh))
3284
+ return false;
3285
+ if (sceneOwnedMeshes.has(mesh))
3286
+ return true;
3287
+ // Babylon can emit a removal notification while a caller-owned Mesh still
3288
+ // remains in Scene.meshes. Revalidate only this cache-miss boundary and
3289
+ // repair the event-maintained index; normal runtime registration is O(1).
3290
+ if (!scene.meshes.includes(mesh))
3291
+ return false;
3292
+ sceneOwnedMeshes.add(mesh);
3293
+ return true;
3294
+ };
3295
+ const validateRuntimeRegistration = (input, batchEntityIds, batchRenderableIds, batchMeshes, batchActivityIdentities) => {
3296
+ const entityId = input.entityId?.trim();
3297
+ const behaviorProfileId = input.behaviorProfileId?.trim();
3298
+ if (!entityId || entityId !== input.entityId)
3299
+ throw new Error('shadowMapExperiment.runtimeEntityIdRequired');
3300
+ if (!behaviorProfileId || behaviorProfileId !== input.behaviorProfileId) {
3301
+ throw new Error('shadowMapExperiment.runtimeBehaviorProfileIdRequired');
3302
+ }
3303
+ const profile = requireRuntimeProfile(sourcePlan, behaviorProfileId, entityId);
3304
+ if (!sourcePlan?.enabled)
3305
+ throw new Error('shadowMapExperiment.runtimePlanDisabled');
3306
+ if (batchEntityIds.has(entityId) || runtimeRegistrations.has(entityId) || objectByEntity.has(entityId)) {
3307
+ throw new Error(`shadowMapExperiment.runtimeEntityConflict:${entityId}`);
3308
+ }
3309
+ batchEntityIds.add(entityId);
3310
+ if (!Array.isArray(input.renderables) || input.renderables.length === 0) {
3311
+ throw new Error(`shadowMapExperiment.runtimeRenderableRequired:${entityId}`);
3312
+ }
3313
+ const renderableIds = new Set();
3314
+ const renderables = input.renderables.map((renderable, index) => {
3315
+ const renderableId = renderable.renderableId?.trim();
3316
+ if (!renderableId || renderableId !== renderable.renderableId) {
3317
+ throw new Error(`shadowMapExperiment.runtimeRenderableIdRequired:${entityId}:${index}`);
3318
+ }
3319
+ if (renderableIds.has(renderableId) || batchRenderableIds.has(renderableId) || objectByRenderable.has(renderableId)) {
3320
+ throw new Error(`shadowMapExperiment.runtimeRenderableConflict:${renderableId}`);
3321
+ }
3322
+ if (!renderable.mesh || !isMeshOwnedByScene(renderable.mesh)) {
3323
+ throw new Error(`shadowMapExperiment.runtimeMeshSceneMismatch:${renderableId}`);
3324
+ }
3325
+ if (batchMeshes.has(renderable.mesh)
3326
+ || runtimeOwnedMeshes.has(renderable.mesh)
3327
+ || bindings.has(renderable.mesh)) {
3328
+ throw new Error(`shadowMapExperiment.runtimeMeshConflict:${renderableId}`);
3329
+ }
3330
+ if (!meshIdentityChainContains(renderable.mesh, renderableId)) {
3331
+ throw new Error(`shadowMapExperiment.runtimeRenderableIdentityMismatch:${renderableId}`);
3332
+ }
3333
+ renderableIds.add(renderableId);
3334
+ batchRenderableIds.add(renderableId);
3335
+ batchMeshes.add(renderable.mesh);
3336
+ return Object.freeze({ renderableId, mesh: renderable.mesh });
3337
+ });
3338
+ const activitySourceIds = new Set();
3339
+ const activities = (input.activities ?? []).map((activity, index) => {
3340
+ const sourceId = activity.sourceId?.trim();
3341
+ if (!sourceId || sourceId !== activity.sourceId) {
3342
+ throw new Error(`shadowMapExperiment.runtimeActivitySourceIdRequired:${entityId}:${index}`);
3343
+ }
3344
+ if (!['transform', 'animation', 'morph', 'physics'].includes(activity.kind)) {
3345
+ throw new Error(`shadowMapExperiment.runtimeActivityKindInvalid:${activity.kind}`);
3346
+ }
3347
+ const identity = `${entityId}\u0000${sourceId}`;
3348
+ if (activitySourceIds.has(sourceId)
3349
+ || batchActivityIdentities.has(identity)
3350
+ || activityScheduler.hasSource(entityId, sourceId)) {
3351
+ throw new Error(`shadowMapExperiment.runtimeActivityConflict:${entityId}:${sourceId}`);
3352
+ }
3353
+ if (!profile.cast || profile.updateClass === 'static') {
3354
+ throw new Error(`shadowMapExperiment.runtimeActivityProfileIneligible:${behaviorProfileId}`);
3355
+ }
3356
+ activitySourceIds.add(sourceId);
3357
+ batchActivityIdentities.add(identity);
3358
+ return Object.freeze({ kind: activity.kind, sourceId, initiallyActive: activity.initiallyActive === true });
3359
+ });
3360
+ return {
3361
+ entityId,
3362
+ behaviorProfileId,
3363
+ renderables: Object.freeze(renderables),
3364
+ activities: Object.freeze(activities),
3365
+ controllers: new Map(),
3366
+ owner: null,
3367
+ disposed: false,
3368
+ };
3369
+ };
3370
+ const createRuntimeSnapshot = () => Object.freeze({
3371
+ planSchemaVersion: sourcePlan?.schemaVersion ?? null,
3372
+ sourcePlanRevision: sourcePlan?.revision ?? null,
3373
+ effectivePlanRevision: plan?.revision ?? null,
3374
+ overlayRevision: runtimeOverlayRevision,
3375
+ incrementalMutationCount: runtimeIncrementalMutationCount,
3376
+ effectivePlanMaterializationCount,
3377
+ registrationCount: runtimeRegistrations.size,
3378
+ activityOwnerCount: [...runtimeRegistrations.values()].reduce((total, record) => total + record.controllers.size, 0),
3379
+ registrations: Object.freeze([...runtimeRegistrations.values()]
3380
+ .filter(record => !record.disposed)
3381
+ .map(record => Object.freeze({
3382
+ entityId: record.entityId,
3383
+ behaviorProfileId: record.behaviorProfileId,
3384
+ renderableIds: Object.freeze(record.renderables.map(renderable => renderable.renderableId).sort()),
3385
+ activitySourceIds: Object.freeze([...record.controllers.keys()].sort()),
3386
+ }))
3387
+ .sort((left, right) => left.entityId.localeCompare(right.entityId))),
3388
+ });
3389
+ const system = {
3390
+ setPlan,
3391
+ previewLightDirection,
3392
+ previewAppearance,
3393
+ getPlan: readMaterializedEffectivePlan,
3394
+ getShadowGenerator: () => generator,
3395
+ registerMesh(mesh) {
3396
+ assertUsable();
3397
+ if (pendingPlanRollbackRecovery) {
3398
+ pendingSceneMeshRegistrations.add(mesh);
3399
+ requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
3400
+ return false;
3401
+ }
3402
+ return registerMesh(mesh);
3403
+ },
3404
+ unregisterMesh(mesh) {
3405
+ assertUsable();
3406
+ if (pendingPlanRollbackRecovery) {
3407
+ pendingMeshUnregistrations.add(mesh);
3408
+ requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
3409
+ return;
3410
+ }
3411
+ requestMeshUnregistration(mesh);
3412
+ },
3413
+ setRenderableVisible(renderableId, visible) {
3414
+ assertUsable();
3415
+ if (pendingPlanRollbackRecovery) {
3416
+ pendingExplicitVisibility.set(renderableId, visible);
3417
+ pendingRenderableMembershipRetries.set(renderableId, 'caster');
3418
+ requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
3419
+ return;
3420
+ }
3421
+ const previousRecord = registry?.getRecord(renderableId) ?? null;
3422
+ let rollbackCasterMembership = null;
3423
+ registry?.setVisible(renderableId, visible);
3424
+ try {
3425
+ rollbackCasterMembership = syncCaster(renderableId);
3426
+ pendingRenderableMembershipRetries.delete(renderableId);
3427
+ }
3428
+ catch (error) {
3429
+ rollbackCasterMembership?.(error);
3430
+ if (previousRecord && registry) {
3431
+ if (previousRecord.renderableReady)
3432
+ registry.attach(renderableId, previousRecord.runtimeVisible);
3433
+ else
3434
+ registry.detach(renderableId);
3435
+ registry.setVisible(renderableId, previousRecord.runtimeVisible);
3436
+ }
3437
+ pendingExplicitVisibility.set(renderableId, visible);
3438
+ pendingRenderableMembershipRetries.set(renderableId, 'caster');
3439
+ requestRefreshFrame('shadow-map-experiment-membership-retry');
3440
+ throw error;
3441
+ }
3442
+ const entityId = objectByRenderable.get(renderableId)?.entityId;
3443
+ if (entityId)
3444
+ syncActivityEligibility(entityId);
3445
+ },
3446
+ registerCasterActivitySource(input) {
3447
+ if (pendingPlanRollbackRecovery && !allowingRecoveryActivityRegistration) {
3448
+ assertNoPlanRollbackRecovery();
3449
+ }
3450
+ return registerActivitySource(input);
3451
+ },
3452
+ registerRuntimeShadowObjects(input) {
3453
+ assertUsable();
3454
+ assertNoPlanRollbackRecovery();
3455
+ if (!Array.isArray(input.objects) || input.objects.length === 0) {
3456
+ throw new Error('shadowMapExperiment.runtimeObjectsRequired');
3457
+ }
3458
+ const batchEntityIds = new Set();
3459
+ const batchRenderableIds = new Set();
3460
+ const batchMeshes = new Set();
3461
+ const batchActivityIdentities = new Set();
3462
+ const records = input.objects.map(object => validateRuntimeRegistration(object, batchEntityIds, batchRenderableIds, batchMeshes, batchActivityIdentities));
3463
+ if (!sourcePlan)
3464
+ throw new Error('shadowMapExperiment.runtimePlanUnavailable');
3465
+ const runtimeObjects = createRuntimePlanObjects(sourcePlan, records);
3466
+ const owner = createRuntimeOwner(records);
3467
+ const nextOverlayRevision = runtimeOverlayRevision + 1;
3468
+ const previousCandidateRevision = automaticActivityCandidateRevision;
3469
+ for (const record of records) {
3470
+ if (record.activities.length > 0)
3471
+ explicitRuntimeActivityEntityIds.add(record.entityId);
3472
+ }
3473
+ let effectivePlanMutationStarted = false;
3474
+ let effectivePlanApplied = false;
3475
+ try {
3476
+ effectivePlanMutationStarted = true;
3477
+ applyRuntimeObjectDelta({
3478
+ upsertedObjects: runtimeObjects,
3479
+ removedObjects: Object.freeze([]),
3480
+ overlayRevision: nextOverlayRevision,
3481
+ meshes: collectRuntimeRecordMeshes(records),
3482
+ });
3483
+ effectivePlanApplied = true;
3484
+ for (const record of records) {
3485
+ runtimeRegistrations.set(record.entityId, record);
3486
+ for (const renderable of record.renderables)
3487
+ runtimeOwnedMeshes.add(renderable.mesh);
3488
+ }
3489
+ for (const record of records) {
3490
+ for (const activity of record.activities) {
3491
+ const controller = system.registerCasterActivitySource({
3492
+ entityId: record.entityId,
3493
+ kind: activity.kind,
3494
+ sourceId: activity.sourceId,
3495
+ });
3496
+ record.controllers.set(activity.sourceId, controller);
3497
+ if (activity.initiallyActive)
3498
+ controller.setActive(true);
3499
+ }
3500
+ }
3501
+ runtimeOverlayRevision = nextOverlayRevision;
3502
+ return owner;
3503
+ }
3504
+ catch (error) {
3505
+ const rollbackErrors = [];
3506
+ for (const record of [...records].reverse()) {
3507
+ for (const [sourceId, controller] of [...record.controllers.entries()].reverse()) {
3508
+ try {
3509
+ controller.setActive(false);
3510
+ }
3511
+ catch (rollbackError) {
3512
+ rollbackErrors.push(rollbackError);
3513
+ }
3514
+ try {
3515
+ controller.dispose();
3516
+ record.controllers.delete(sourceId);
3517
+ }
3518
+ catch (rollbackError) {
3519
+ rollbackErrors.push(rollbackError);
3520
+ }
3521
+ }
3522
+ }
3523
+ let effectivePlanRollbackFailed = false;
3524
+ if (effectivePlanMutationStarted) {
3525
+ try {
3526
+ const previousEffectivePlan = composeEffectivePlan(sourcePlan, [...runtimeRegistrations.values()].filter(candidate => (!records.includes(candidate) && !candidate.disposed)), runtimeOverlayRevision);
3527
+ if (effectivePlanApplied) {
3528
+ applyEffectivePlan(previousEffectivePlan, false, collectRuntimeRecordMeshes(records));
3529
+ }
3530
+ else {
3531
+ rollbackPlanUpdate(previousEffectivePlan, error, previousCandidateRevision);
3532
+ }
3533
+ }
3534
+ catch (rollbackError) {
3535
+ if (effectivePlanApplied || rollbackError !== error) {
3536
+ effectivePlanRollbackFailed = true;
3537
+ rollbackErrors.push(rollbackError);
3538
+ }
3539
+ }
3540
+ }
3541
+ for (const record of records) {
3542
+ runtimeRegistrations.delete(record.entityId);
3543
+ for (const renderable of record.renderables)
3544
+ runtimeOwnedMeshes.delete(renderable.mesh);
3545
+ }
3546
+ if (rollbackErrors.length > 0) {
3547
+ for (const record of records) {
3548
+ runtimeRegistrations.set(record.entityId, record);
3549
+ for (const renderable of record.renderables)
3550
+ runtimeOwnedMeshes.add(renderable.mesh);
3551
+ }
3552
+ if (effectivePlanRollbackFailed)
3553
+ runtimeOverlayRevision = nextOverlayRevision;
3554
+ throw createRuntimeRegistrationRollbackFailure(error, owner, rollbackErrors);
3555
+ }
3556
+ else {
3557
+ for (const record of records) {
3558
+ explicitRuntimeActivityEntityIds.delete(record.entityId);
3559
+ record.disposed = true;
3560
+ }
3561
+ }
3562
+ throw error;
3563
+ }
3564
+ },
3565
+ markDirty(reason, targetEntityOrRenderableId) {
3566
+ assertUsable();
3567
+ const targetedObject = targetEntityOrRenderableId
3568
+ ? objectByEntity.get(targetEntityOrRenderableId) ?? objectByRenderable.get(targetEntityOrRenderableId)
3569
+ : null;
3570
+ const targetedLayer = targetedObject?.cast
3571
+ ? shadowMapExperimentDepthLayerForUpdateClass(targetedObject.updateClass)
3572
+ : null;
3573
+ if (reason === 'static-geometry-changed') {
3574
+ pendingMembershipInvalidationLayers.add('static');
3575
+ invalidateSceneFitBounds(targetedObject ? targetedObject.renderableIds : null, true);
3576
+ }
3577
+ if (reason === 'material-alpha-test-changed' && targetedLayer) {
3578
+ markDepthDirty(targetedLayer, reason);
3579
+ return;
3580
+ }
3581
+ enqueueRefresh(reason);
3582
+ },
3583
+ getRuntimeSnapshot() {
3584
+ assertUsable();
3585
+ return createRuntimeSnapshot();
3586
+ },
3587
+ getEvidence() {
3588
+ const effectivePlanSnapshot = readMaterializedEffectivePlan();
3589
+ const bounds = plan?.generator.bounds;
3590
+ const lightSpaceSize = plan?.enabled && bounds
3591
+ ? calculateLightSpaceSize(plan.light.direction, effectiveCoverageExtent ?? bounds.extent)
3592
+ : null;
3593
+ const activity = activityScheduler.snapshot();
3594
+ const automaticBindings = [...automaticAnimationGroups.values()];
3595
+ const automaticActivity = Object.freeze({
3596
+ observedAnimationGroupCount: automaticBindings.length,
3597
+ boundAnimationGroupCount: automaticBindings.filter(binding => binding.sources.size > 0).length,
3598
+ activeAnimationGroupCount: automaticBindings.filter(binding => binding.active).length,
3599
+ registeredActivitySourceCount: automaticBindings.reduce((count, binding) => count + binding.sources.size, 0),
3600
+ unresolvedActiveAnimationGroupCount: automaticBindings.filter(binding => binding.active && binding.sources.size === 0).length,
3601
+ candidateMeshCount: automaticActivityCandidateBindings.size,
3602
+ candidateRevision: automaticActivityCandidateRevision,
3603
+ targetResolutionCount: automaticActivityTargetResolutionCount,
3604
+ cachedNegativeAnimationGroupCount: automaticBindings.filter(binding => !binding.rebindPending
3605
+ && binding.resolvedCandidateRevision === automaticActivityCandidateRevision
3606
+ && binding.sources.size === 0).length,
3607
+ });
3608
+ const depthEvidence = depthBackend?.getEvidence() ?? null;
3609
+ const thresholdDiagnostics = effectivePlanSnapshot ? createThresholdDiagnostics(effectivePlanSnapshot, depthEvidence?.static.candidateMeshCount ?? 0, depthEvidence?.dynamic.candidateMeshCount ?? 0) : [];
3610
+ return Object.freeze({
3611
+ enabled: plan?.enabled === true,
3612
+ planRevision: plan?.revision ?? null,
3613
+ planFingerprint: effectivePlanSnapshot
3614
+ ? createShadowMapExperimentPlanFingerprint(effectivePlanSnapshot)
3615
+ : null,
3616
+ effectivePlan: effectivePlanSnapshot
3617
+ ? createShadowMapExperimentPlanSnapshot(effectivePlanSnapshot)
3618
+ : null,
3619
+ lightEntityId: plan?.light.entityId ?? null,
3620
+ generatorAllocated: generator !== null,
3621
+ resolution: plan?.enabled ? plan.generator.maps.compositeResolution : null,
3622
+ configuredMapResolutions: plan?.enabled ? Object.freeze({
3623
+ static: plan.generator.maps.staticResolution,
3624
+ dynamic: plan.generator.maps.dynamicResolution,
3625
+ composite: plan.generator.maps.compositeResolution,
3626
+ }) : null,
3627
+ mapResolutions: depthEvidence ? Object.freeze({
3628
+ static: depthEvidence.static.resolution,
3629
+ dynamic: depthEvidence.dynamic.resolution,
3630
+ composite: depthEvidence.compositeResolution,
3631
+ }) : null,
3632
+ diagnosticThresholds: plan?.enabled ? plan.diagnosticThresholds : null,
3633
+ filter: plan?.enabled ? plan.generator.filter : null,
3634
+ appearance: plan?.enabled
3635
+ ? resolveShadowMapExperimentAppearance(plan.generator)
3636
+ : null,
3637
+ renderListCount: depthBackend
3638
+ ? depthBackend.getEvidence().static.renderListCount + depthBackend.getEvidence().dynamic.renderListCount
3639
+ : 0,
3640
+ receiverMeshCount: [...receiverStates.values()].filter(state => !isDisposed(state.mesh)
3641
+ && state.ownedValue
3642
+ && state.mesh.receiveShadows === state.ownedValue).length,
3643
+ boundMeshCount: bindings.size,
3644
+ activity,
3645
+ automaticActivity,
3646
+ runtime: createRuntimeSnapshot(),
3647
+ refreshCount,
3648
+ lastRefreshReasons,
3649
+ worldUnitsPerTexel: plan?.enabled && lightSpaceSize && depthEvidence
3650
+ ? Object.freeze([
3651
+ lightSpaceSize[0] / depthEvidence.compositeResolution,
3652
+ lightSpaceSize[1] / depthEvidence.compositeResolution,
3653
+ ])
3654
+ : null,
3655
+ worldUnitsPerTexelByMap: plan?.enabled && lightSpaceSize && depthEvidence ? Object.freeze({
3656
+ static: texelSize(lightSpaceSize, depthEvidence.static.resolution),
3657
+ dynamic: texelSize(lightSpaceSize, depthEvidence.dynamic.resolution),
3658
+ composite: texelSize(lightSpaceSize, depthEvidence.compositeResolution),
3659
+ }) : null,
3660
+ registry: registry?.snapshot() ?? null,
3661
+ backend: depthBackend?.mode ?? null,
3662
+ fallbackReason: depthBackend?.fallbackReason ?? pendingFallbackReason,
3663
+ fallbackWarningEmitted: depthBackend?.fallbackReason
3664
+ ? fallbackWarnings.has(depthBackend.fallbackReason)
3665
+ : false,
3666
+ depth: depthEvidence,
3667
+ culling: Object.freeze({
3668
+ ...influenceCulling.snapshot(),
3669
+ enabled: plan?.culling.enabled === true,
3670
+ conservative: influenceCulling.snapshot().conservative || conservativeCasterMeshes.size > 0,
3671
+ stable: cullingStable,
3672
+ lastInvalidationReason: lastCullingInvalidationReason,
3673
+ coverageMode: options.coverageMode === 'scene-fit' ? 'scene-fit' : 'fixed',
3674
+ authoredCenter: plan
3675
+ ? Object.freeze([...plan.generator.bounds.center])
3676
+ : null,
3677
+ effectiveCenter: plan
3678
+ ? Object.freeze([
3679
+ ...(effectiveCoverageCenter ?? plan.generator.bounds.center),
3680
+ ])
3681
+ : null,
3682
+ authoredExtent: plan
3683
+ ? Object.freeze([...plan.generator.bounds.extent])
3684
+ : null,
3685
+ effectiveExtent: plan
3686
+ ? Object.freeze([
3687
+ ...(effectiveCoverageExtent ?? plan.generator.bounds.extent),
3688
+ ])
3689
+ : null,
3690
+ anchor: null,
3691
+ }),
3692
+ diagnostics: Object.freeze([
3693
+ ...diagnostics.map(entry => Object.freeze({ ...entry })),
3694
+ ...thresholdDiagnostics,
3695
+ ...((depthBackend?.fallbackReason ?? pendingFallbackReason) ? [Object.freeze({
3696
+ code: plan?.generator.backend.fallback === 'disable'
3697
+ ? 'shadowMapExperiment.splitBackendDisabled'
3698
+ : 'shadowMapExperiment.splitBackendFallback',
3699
+ message: plan?.generator.backend.fallback === 'disable'
3700
+ ? `Split depth backend was disabled: ${depthBackend?.fallbackReason ?? pendingFallbackReason}`
3701
+ : `Split depth backend fell back to a single ShadowMap: ${depthBackend?.fallbackReason ?? pendingFallbackReason}`,
3702
+ })] : []),
3703
+ ]),
3704
+ });
3705
+ },
3706
+ dispose() {
3707
+ if (disposed)
3708
+ return;
3709
+ disposing = true;
3710
+ // Disposal is cleanup-only. A rollback recovery may be waiting to
3711
+ // reactivate an old Plan; cancel that target before runtime owners start
3712
+ // removing their overlays so no dispose retry can resurrect it.
3713
+ if (pendingPlanRollbackRecovery) {
3714
+ pendingPlanRollbackRecovery = {
3715
+ ...pendingPlanRollbackRecovery,
3716
+ targetPlan: null,
3717
+ phase: 'deactivate',
3718
+ };
3719
+ }
3720
+ plan = null;
3721
+ sourcePlan = null;
3722
+ materializedEffectivePlan = null;
3723
+ activityScheduler.applyPlan(null);
3724
+ const runtimeCleanupErrors = disposeRuntimeRecordsForSystem([...runtimeRegistrations.values()]);
3725
+ if (runtimeCleanupErrors.length > 0) {
3726
+ throw Object.assign(new Error('shadowMapExperiment.disposeFailed'), {
3727
+ cleanupErrors: Object.freeze(runtimeCleanupErrors),
3728
+ });
3729
+ }
3730
+ const cleanupErrors = [];
3731
+ const attempt = (operation, completed) => {
3732
+ try {
3733
+ operation();
3734
+ completed();
3735
+ }
3736
+ catch (error) {
3737
+ cleanupErrors.push(error);
3738
+ }
3739
+ };
3740
+ if (!disposeNewAnimationGroupObserverRemoved) {
3741
+ attempt(() => { scene.onNewAnimationGroupAddedObservable.remove(newAnimationGroupObserver); }, () => { disposeNewAnimationGroupObserverRemoved = true; });
3742
+ }
3743
+ if (!disposeRemovedAnimationGroupObserverRemoved) {
3744
+ attempt(() => { scene.onAnimationGroupRemovedObservable.remove(removedAnimationGroupObserver); }, () => { disposeRemovedAnimationGroupObserverRemoved = true; });
3745
+ }
3746
+ for (const group of [...automaticAnimationGroups.keys()].reverse()) {
3747
+ cleanupErrors.push(...detachAutomaticAnimationGroup(group));
3748
+ }
3749
+ if (!disposeNewMeshObserverRemoved) {
3750
+ attempt(() => { scene.onNewMeshAddedObservable.remove(newMeshObserver); }, () => { disposeNewMeshObserverRemoved = true; });
3751
+ }
3752
+ if (!disposeRemovedMeshObserverRemoved) {
3753
+ attempt(() => { scene.onMeshRemovedObservable.remove(removedMeshObserver); }, () => { disposeRemovedMeshObserverRemoved = true; });
3754
+ }
3755
+ if (!disposeFrameObserverRemoved) {
3756
+ attempt(() => { scene.onBeforeRenderObservable.remove(frameObserver); }, () => { disposeFrameObserverRemoved = true; });
3757
+ }
3758
+ if (hasOwnedBackendState())
3759
+ attempt(deactivate, () => undefined);
3760
+ pendingPlanRollbackRecovery = null;
3761
+ const observersRemoved = disposeNewMeshObserverRemoved
3762
+ && disposeRemovedMeshObserverRemoved
3763
+ && disposeFrameObserverRemoved
3764
+ && disposeNewAnimationGroupObserverRemoved
3765
+ && disposeRemovedAnimationGroupObserverRemoved
3766
+ && automaticAnimationGroups.size === 0;
3767
+ if (observersRemoved && !hasOwnedBackendState()) {
3768
+ if (!disposeActivitySchedulerDisposed) {
3769
+ attempt(activityScheduler.dispose, () => { disposeActivitySchedulerDisposed = true; });
3770
+ }
3771
+ if (!disposeDepthRefreshSchedulerDisposed) {
3772
+ attempt(depthRefreshScheduler.dispose, () => { disposeDepthRefreshSchedulerDisposed = true; });
3773
+ }
3774
+ if (!disposeInfluenceCullingDisposed) {
3775
+ attempt(influenceCulling.dispose, () => { disposeInfluenceCullingDisposed = true; });
3776
+ }
3777
+ }
3778
+ if (!hasOwnedBackendState()) {
3779
+ sceneOwnedMeshes.clear();
3780
+ activeDeformationSourceCountByEntity.clear();
3781
+ pendingFinalDeformationEntityIds.clear();
3782
+ pendingDeformationInvalidations.clear();
3783
+ activeCasterEntityIds.clear();
3784
+ pendingTransformMembershipRetries.clear();
3785
+ pendingSceneMeshRegistrations.clear();
3786
+ pendingMeshUnregistrations.clear();
3787
+ pendingRenderableMembershipRetries.clear();
3788
+ pendingExplicitVisibility.clear();
3789
+ pendingAutomaticAnimationGroupAttachments.clear();
3790
+ pendingAutomaticAnimationGroupReconciliations.clear();
3791
+ }
3792
+ disposed = observersRemoved
3793
+ && !hasOwnedBackendState()
3794
+ && disposeActivitySchedulerDisposed
3795
+ && disposeDepthRefreshSchedulerDisposed
3796
+ && disposeInfluenceCullingDisposed;
3797
+ if (cleanupErrors.length > 0) {
3798
+ throw Object.assign(new Error('shadowMapExperiment.disposeFailed'), { cleanupErrors });
3799
+ }
3800
+ },
3801
+ };
3802
+ if (initialPlan) {
3803
+ try {
3804
+ setPlan(initialPlan);
3805
+ }
3806
+ catch (error) {
3807
+ let newMeshObserverRemoved = false;
3808
+ let removedMeshObserverRemoved = false;
3809
+ let frameObserverRemoved = false;
3810
+ let newAnimationGroupObserverRemoved = false;
3811
+ let removedAnimationGroupObserverRemoved = false;
3812
+ let activitySchedulerDisposed = false;
3813
+ let depthRefreshSchedulerDisposed = false;
3814
+ let influenceCullingDisposed = false;
3815
+ let cleanupComplete = false;
3816
+ const cleanup = () => {
3817
+ if (cleanupComplete)
3818
+ return Object.freeze([]);
3819
+ const cleanupErrors = [];
3820
+ const attempt = (operation, completed) => {
3821
+ try {
3822
+ operation();
3823
+ completed();
3824
+ }
3825
+ catch (cleanupError) {
3826
+ cleanupErrors.push(cleanupError);
3827
+ }
3828
+ };
3829
+ if (!newAnimationGroupObserverRemoved) {
3830
+ attempt(() => { scene.onNewAnimationGroupAddedObservable.remove(newAnimationGroupObserver); }, () => { newAnimationGroupObserverRemoved = true; });
3831
+ }
3832
+ if (!removedAnimationGroupObserverRemoved) {
3833
+ attempt(() => { scene.onAnimationGroupRemovedObservable.remove(removedAnimationGroupObserver); }, () => { removedAnimationGroupObserverRemoved = true; });
3834
+ }
3835
+ for (const group of [...automaticAnimationGroups.keys()].reverse()) {
3836
+ cleanupErrors.push(...detachAutomaticAnimationGroup(group));
3837
+ }
3838
+ if (!newMeshObserverRemoved) {
3839
+ attempt(() => { scene.onNewMeshAddedObservable.remove(newMeshObserver); }, () => { newMeshObserverRemoved = true; });
3840
+ }
3841
+ if (!removedMeshObserverRemoved) {
3842
+ attempt(() => { scene.onMeshRemovedObservable.remove(removedMeshObserver); }, () => { removedMeshObserverRemoved = true; });
3843
+ }
3844
+ if (!frameObserverRemoved) {
3845
+ attempt(() => { scene.onBeforeRenderObservable.remove(frameObserver); }, () => { frameObserverRemoved = true; });
3846
+ }
3847
+ if (hasOwnedBackendState()) {
3848
+ attempt(deactivate, () => undefined);
3849
+ }
3850
+ plan = null;
3851
+ const observersRemoved = newMeshObserverRemoved
3852
+ && removedMeshObserverRemoved
3853
+ && frameObserverRemoved
3854
+ && newAnimationGroupObserverRemoved
3855
+ && removedAnimationGroupObserverRemoved
3856
+ && automaticAnimationGroups.size === 0;
3857
+ if (observersRemoved && !hasOwnedBackendState()) {
3858
+ if (!activitySchedulerDisposed) {
3859
+ attempt(activityScheduler.dispose, () => { activitySchedulerDisposed = true; });
3860
+ }
3861
+ if (!depthRefreshSchedulerDisposed) {
3862
+ attempt(depthRefreshScheduler.dispose, () => { depthRefreshSchedulerDisposed = true; });
3863
+ }
3864
+ if (!influenceCullingDisposed) {
3865
+ attempt(influenceCulling.dispose, () => { influenceCullingDisposed = true; });
3866
+ }
3867
+ }
3868
+ cleanupComplete = observersRemoved
3869
+ && !hasOwnedBackendState()
3870
+ && activitySchedulerDisposed
3871
+ && depthRefreshSchedulerDisposed
3872
+ && influenceCullingDisposed;
3873
+ if (cleanupComplete)
3874
+ disposed = true;
3875
+ return Object.freeze(cleanupErrors);
3876
+ };
3877
+ const initializationCleanupErrors = cleanup();
3878
+ if (initializationCleanupErrors.length > 0) {
3879
+ throw createInitializationFailure(error, cleanup, initializationCleanupErrors);
3880
+ }
3881
+ throw error;
3882
+ }
3883
+ }
3884
+ return system;
3885
+ }
3886
+ function createThresholdDiagnostics(plan, staticCasterCount, dynamicCasterCount) {
3887
+ const thresholds = plan.diagnosticThresholds;
3888
+ const result = [];
3889
+ if (staticCasterCount > thresholds.staticCasterCountWarning) {
3890
+ result.push({
3891
+ code: 'shadowMapExperiment.staticCasterCountWarning',
3892
+ message: `Static caster count ${staticCasterCount} exceeds the configured warning threshold ${thresholds.staticCasterCountWarning}.`,
3893
+ });
3894
+ }
3895
+ if (dynamicCasterCount > thresholds.dynamicCasterCountWarning) {
3896
+ result.push({
3897
+ code: 'shadowMapExperiment.dynamicCasterCountWarning',
3898
+ message: `Dynamic caster count ${dynamicCasterCount} exceeds the configured warning threshold ${thresholds.dynamicCasterCountWarning}.`,
3899
+ });
3900
+ }
3901
+ const renderableCount = plan.objects.reduce((total, object) => total + object.renderableIds.length, 0);
3902
+ if (renderableCount > thresholds.renderableCountWarning) {
3903
+ result.push({
3904
+ code: 'shadowMapExperiment.renderableCountWarning',
3905
+ message: `Renderable count ${renderableCount} exceeds the configured warning threshold ${thresholds.renderableCountWarning}.`,
3906
+ });
3907
+ }
3908
+ return Object.freeze(result.map(entry => Object.freeze(entry)));
3909
+ }
3910
+ function texelSize(lightSpaceSize, resolution) {
3911
+ return Object.freeze([
3912
+ lightSpaceSize[0] / resolution,
3913
+ lightSpaceSize[1] / resolution,
3914
+ ]);
3915
+ }
3916
+ function configureLight(light, plan, coverage) {
3917
+ const direction = Vector3.FromArray(plan.light.direction).normalize();
3918
+ if (direction.lengthSquared() < 1e-8)
3919
+ throw new Error('shadowMapExperiment.lightDirectionInvalid');
3920
+ const center = Vector3.FromArray(coverage.center);
3921
+ const depth = (coverage.near + coverage.far) * 0.5;
3922
+ const [width, height] = calculateLightSpaceSize(plan.light.direction, coverage.extent);
3923
+ light.direction.copyFrom(direction);
3924
+ light.position.copyFrom(center.subtract(direction.scale(depth)));
3925
+ light.autoUpdateExtends = false;
3926
+ light.orthoLeft = -width * 0.5;
3927
+ light.orthoRight = width * 0.5;
3928
+ light.orthoBottom = -height * 0.5;
3929
+ light.orthoTop = height * 0.5;
3930
+ light.shadowMinZ = coverage.near;
3931
+ light.shadowMaxZ = coverage.far;
3932
+ }
3933
+ function createSceneFitCoverage(plan, bounds) {
3934
+ const min = Vector3.FromArray(bounds.min);
3935
+ const max = Vector3.FromArray(bounds.max);
3936
+ const rawExtent = max.subtract(min);
3937
+ const maxExtent = Math.max(rawExtent.x, rawExtent.y, rawExtent.z, 1);
3938
+ const padding = Math.max(0.5, maxExtent * 0.05);
3939
+ const extent = Object.freeze([
3940
+ Math.max(4, rawExtent.x + padding * 2),
3941
+ Math.max(4, rawExtent.y + padding * 2),
3942
+ Math.max(4, rawExtent.z + padding * 2),
3943
+ ]);
3944
+ const near = Math.min(plan.generator.bounds.near, 0.1);
3945
+ const far = Math.max(plan.generator.bounds.far, Math.hypot(extent[0], extent[1], extent[2]) + padding * 2);
3946
+ const center = snapSceneFitCenterToTexels(plan, vectorTuple(min.add(max).scale(0.5)), extent, near, far);
3947
+ return Object.freeze({ center, extent, near, far });
3948
+ }
3949
+ function collectActiveCasterWorldBounds(bindings, registry) {
3950
+ let min = null;
3951
+ let max = null;
3952
+ for (const binding of bindings.values()) {
3953
+ if (registry?.getRecord(binding.renderableId)?.state !== 'active')
3954
+ continue;
3955
+ if (!isMeshRuntimeVisible(binding.mesh))
3956
+ continue;
3957
+ const bounds = readStableMeshWorldBounds(binding);
3958
+ if (!bounds)
3959
+ continue;
3960
+ const boundsMin = Vector3.FromArray(bounds.min);
3961
+ const boundsMax = Vector3.FromArray(bounds.max);
3962
+ min = min ? Vector3.Minimize(min, boundsMin) : boundsMin;
3963
+ max = max ? Vector3.Maximize(max, boundsMax) : boundsMax;
3964
+ }
3965
+ return min && max
3966
+ ? Object.freeze({ min: vectorTuple(min), max: vectorTuple(max) })
3967
+ : null;
3968
+ }
3969
+ function createLightSpaceBasis(direction) {
3970
+ // This seed only chooses an orthonormal light-space basis; it is not an
3971
+ // authored/world "forward" direction and its sign carries no world policy.
3972
+ const upSeed = Math.abs(Vector3.Dot(direction, Vector3.Up())) > 0.98 ? Vector3.Forward() : Vector3.Up();
3973
+ const right = Vector3.Cross(upSeed, direction).normalize();
3974
+ const up = Vector3.Cross(direction, right).normalize();
3975
+ return { right, up };
3976
+ }
3977
+ function snapSceneFitCenterToTexels(plan, centerValue, extentValue, near, far) {
3978
+ const direction = Vector3.FromArray(plan.light.direction).normalize();
3979
+ const { right, up } = createLightSpaceBasis(direction);
3980
+ const center = Vector3.FromArray(centerValue);
3981
+ const [width, height] = calculateLightSpaceSize(plan.light.direction, extentValue);
3982
+ const resolution = Math.max(1, plan.generator.maps.compositeResolution);
3983
+ const depth = Math.max(far - near, 1e-4);
3984
+ const snap = (value, step) => Math.round(value / step) * step;
3985
+ const rightValue = snap(Vector3.Dot(center, right), width / resolution);
3986
+ const upValue = snap(Vector3.Dot(center, up), height / resolution);
3987
+ const depthValue = snap(Vector3.Dot(center, direction), depth / resolution);
3988
+ const result = right.scale(rightValue).add(up.scale(upValue)).add(direction.scale(depthValue));
3989
+ return vectorTuple(result);
3990
+ }
3991
+ function areTuple3Equal(left, right) {
3992
+ return left.every((value, index) => Math.abs(value - right[index]) <= 1e-8);
3993
+ }
3994
+ function calculateLightSpaceSize(directionValue, extent) {
3995
+ const direction = Vector3.FromArray(directionValue).normalize();
3996
+ const { right, up } = createLightSpaceBasis(direction);
3997
+ const half = new Vector3(extent[0] * 0.5, extent[1] * 0.5, extent[2] * 0.5);
3998
+ let width = 0;
3999
+ let height = 0;
4000
+ for (const x of [-half.x, half.x]) {
4001
+ for (const y of [-half.y, half.y]) {
4002
+ for (const z of [-half.z, half.z]) {
4003
+ const corner = new Vector3(x, y, z);
4004
+ width = Math.max(width, Math.abs(Vector3.Dot(corner, right)) * 2);
4005
+ height = Math.max(height, Math.abs(Vector3.Dot(corner, up)) * 2);
4006
+ }
4007
+ }
4008
+ }
4009
+ return Object.freeze([Math.max(width, 1e-4), Math.max(height, 1e-4)]);
4010
+ }
4011
+ export function resolveBabylonDirectionalLight(scene, entityId) {
4012
+ for (const light of scene.lights) {
4013
+ if (!isBabylonDirectionalLight(light))
4014
+ continue;
4015
+ if (collectIdentityCandidates(light).has(entityId))
4016
+ return light;
4017
+ }
4018
+ return null;
4019
+ }
4020
+ function isBabylonDirectionalLight(light) {
4021
+ if (light instanceof DirectionalLight)
4022
+ return true;
4023
+ if (!isRecord(light) || typeof light.getClassName !== 'function')
4024
+ return false;
4025
+ try {
4026
+ return light.getClassName() === 'DirectionalLight';
4027
+ }
4028
+ catch {
4029
+ return false;
4030
+ }
4031
+ }
4032
+ function resolveRenderableId(mesh, objectByRenderable) {
4033
+ let current = mesh;
4034
+ while (current) {
4035
+ for (const candidate of collectIdentityCandidates(current)) {
4036
+ if (objectByRenderable.has(candidate))
4037
+ return candidate;
4038
+ }
4039
+ current = current.parent;
4040
+ }
4041
+ return null;
4042
+ }
4043
+ function collectIdentityCandidates(node) {
4044
+ const candidates = new Set();
4045
+ if (node.id)
4046
+ candidates.add(node.id);
4047
+ if (node.name)
4048
+ candidates.add(node.name);
4049
+ const metadata = isRecord(node.metadata) ? node.metadata : null;
4050
+ if (!metadata)
4051
+ return candidates;
4052
+ addIdentityCandidatesFromRecord(candidates, metadata);
4053
+ const projection = isRecord(metadata.editorProjection) ? metadata.editorProjection : null;
4054
+ if (projection)
4055
+ addIdentityCandidatesFromRecord(candidates, projection);
4056
+ const authoredSource = isRecord(metadata.__fpsEditor) ? metadata.__fpsEditor : null;
4057
+ if (authoredSource)
4058
+ addIdentityCandidatesFromRecord(candidates, authoredSource);
4059
+ const sourceBinding = isRecord(metadata.sourceBinding) ? metadata.sourceBinding : null;
4060
+ if (sourceBinding)
4061
+ addIdentityCandidatesFromRecord(candidates, sourceBinding);
4062
+ return candidates;
4063
+ }
4064
+ function addIdentityCandidatesFromRecord(candidates, record) {
4065
+ for (const key of [
4066
+ 'entityId',
4067
+ 'renderableId',
4068
+ 'runtimeNodeId',
4069
+ 'nodeId',
4070
+ 'rootNodeId',
4071
+ 'objectId',
4072
+ 'sourceId',
4073
+ ]) {
4074
+ const value = record[key];
4075
+ if (typeof value === 'string' && value.trim())
4076
+ candidates.add(value);
4077
+ }
4078
+ }
4079
+ function meshIdentityChainContains(mesh, identity) {
4080
+ let current = mesh;
4081
+ while (current) {
4082
+ if (collectIdentityCandidates(current).has(identity))
4083
+ return true;
4084
+ current = current.parent;
4085
+ }
4086
+ return false;
4087
+ }
4088
+ function isReceiverEligible(mesh) {
4089
+ const metadata = isRecord(mesh.metadata) ? mesh.metadata : null;
4090
+ const projection = metadata && isRecord(metadata.editorProjection) ? metadata.editorProjection : null;
4091
+ if (metadata?.helper === true || projection?.helper === true)
4092
+ return false;
4093
+ const material = mesh.material;
4094
+ if (!material)
4095
+ return true;
4096
+ if ('disableLighting' in material && material.disableLighting === true)
4097
+ return false;
4098
+ try {
4099
+ if (material.needAlphaBlendingForMesh(mesh) && !allowsTransparentShadowReceiving(material))
4100
+ return false;
4101
+ }
4102
+ catch {
4103
+ return false;
4104
+ }
4105
+ return true;
4106
+ }
4107
+ function deepFreezeShadowMapExperimentPlan(plan) {
4108
+ const freeze = (value) => {
4109
+ if (!value || typeof value !== 'object')
4110
+ return;
4111
+ for (const child of Object.values(value))
4112
+ freeze(child);
4113
+ if (!Object.isFrozen(value))
4114
+ Object.freeze(value);
4115
+ };
4116
+ freeze(plan);
4117
+ return plan;
4118
+ }
4119
+ function allowsTransparentShadowReceiving(material) {
4120
+ if (!material)
4121
+ return false;
4122
+ const metadata = isRecord(material.metadata) ? material.metadata : null;
4123
+ const shadowMapExperiment = metadata && isRecord(metadata.shadowMapExperiment)
4124
+ ? metadata.shadowMapExperiment
4125
+ : null;
4126
+ return shadowMapExperiment?.allowTransparentReceiver === true;
4127
+ }
4128
+ function resolveReceiverTarget(mesh) {
4129
+ const sourceMesh = mesh.sourceMesh;
4130
+ return sourceMesh && !isDisposed(sourceMesh) ? sourceMesh : mesh;
4131
+ }
4132
+ function readWorldMatrixSnapshot(mesh) {
4133
+ const matrix = mesh.getWorldMatrix?.();
4134
+ if (!matrix || typeof matrix.asArray !== 'function')
4135
+ return null;
4136
+ return [...matrix.asArray()];
4137
+ }
4138
+ function areMatrixSnapshotsEqual(previous, next) {
4139
+ if (previous === next)
4140
+ return true;
4141
+ if (!previous || !next || previous.length !== next.length)
4142
+ return false;
4143
+ for (let index = 0; index < previous.length; index += 1) {
4144
+ if (previous[index] !== next[index])
4145
+ return false;
4146
+ }
4147
+ return true;
4148
+ }
4149
+ function isMeshRuntimeVisible(mesh) {
4150
+ return !isDisposed(mesh) && mesh.isEnabled() && mesh.isVisible && mesh.visibility > 0;
4151
+ }
4152
+ function isDisposed(mesh) {
4153
+ return typeof mesh.isDisposed === 'function' && mesh.isDisposed();
4154
+ }
4155
+ function readMeshLocalBounds(mesh) {
4156
+ try {
4157
+ const bounds = mesh.getBoundingInfo?.().boundingBox;
4158
+ if (!bounds)
4159
+ return null;
4160
+ const min = bounds.minimum;
4161
+ const max = bounds.maximum;
4162
+ if (![min.x, min.y, min.z, max.x, max.y, max.z].every(Number.isFinite))
4163
+ return null;
4164
+ return Object.freeze({ min: vectorTuple(min), max: vectorTuple(max) });
4165
+ }
4166
+ catch {
4167
+ return null;
4168
+ }
4169
+ }
4170
+ function readStableMeshWorldBounds(binding) {
4171
+ const local = binding.sceneFitLocalBounds;
4172
+ if (!local)
4173
+ return readMeshWorldBounds(binding.mesh);
4174
+ try {
4175
+ binding.mesh.computeWorldMatrix(false);
4176
+ const matrix = binding.mesh.getWorldMatrix();
4177
+ const min = Vector3.FromArray(local.min);
4178
+ const max = Vector3.FromArray(local.max);
4179
+ let worldMin = null;
4180
+ let worldMax = null;
4181
+ for (const x of [min.x, max.x]) {
4182
+ for (const y of [min.y, max.y]) {
4183
+ for (const z of [min.z, max.z]) {
4184
+ const point = Vector3.TransformCoordinates(new Vector3(x, y, z), matrix);
4185
+ worldMin = worldMin ? Vector3.Minimize(worldMin, point) : point;
4186
+ worldMax = worldMax ? Vector3.Maximize(worldMax, point) : point;
4187
+ }
4188
+ }
4189
+ }
4190
+ return worldMin && worldMax
4191
+ ? Object.freeze({ min: vectorTuple(worldMin), max: vectorTuple(worldMax) })
4192
+ : null;
4193
+ }
4194
+ catch {
4195
+ return null;
4196
+ }
4197
+ }
4198
+ function readMeshWorldBounds(mesh) {
4199
+ try {
4200
+ mesh.computeWorldMatrix(false);
4201
+ const bounds = mesh.getBoundingInfo?.().boundingBox;
4202
+ if (!bounds)
4203
+ return null;
4204
+ const min = bounds.minimumWorld;
4205
+ const max = bounds.maximumWorld;
4206
+ if (![min.x, min.y, min.z, max.x, max.y, max.z].every(Number.isFinite))
4207
+ return null;
4208
+ return Object.freeze({ min: vectorTuple(min), max: vectorTuple(max) });
4209
+ }
4210
+ catch {
4211
+ return null;
4212
+ }
4213
+ }
4214
+ function isDeformableCaster(mesh, updateClass) {
4215
+ const deformable = mesh;
4216
+ return updateClass === 'skinned' || Boolean(deformable.skeleton) || Boolean(deformable.morphTargetManager);
4217
+ }
4218
+ function createMeshDataCache(mesh, caches) {
4219
+ const cache = {};
4220
+ caches.set(mesh, cache);
4221
+ return cache;
4222
+ }
4223
+ function readCasterWorldBounds(mesh, deformable, cache) {
4224
+ if (deformable) {
4225
+ const deformableMesh = mesh;
4226
+ const applySkeleton = Boolean(deformableMesh.skeleton);
4227
+ const applyMorph = Boolean(deformableMesh.morphTargetManager);
4228
+ if (applySkeleton || applyMorph) {
4229
+ try {
4230
+ mesh.refreshBoundingInfo({
4231
+ applySkeleton,
4232
+ applyMorph,
4233
+ updatePositionsArray: false,
4234
+ ...(cache ? { cache } : {}),
4235
+ });
4236
+ }
4237
+ catch {
4238
+ return null;
4239
+ }
4240
+ }
4241
+ }
4242
+ return readMeshWorldBounds(mesh);
4243
+ }
4244
+ function readCameraProjectionSignature(camera, scene) {
4245
+ const projection = camera.getProjectionMatrix().asArray()
4246
+ .map(value => Number.isFinite(value) ? value.toFixed(5) : String(value))
4247
+ .join(',');
4248
+ const engine = scene.getEngine();
4249
+ return `${camera.mode}:${engine.getRenderWidth()}:${engine.getRenderHeight()}:${projection}`;
4250
+ }
4251
+ function calculateCameraShadowCoverage(camera, plan, centerValue = plan.generator.bounds.center, extentValue = plan.generator.bounds.extent) {
4252
+ try {
4253
+ const fixedMin = Vector3.FromArray(centerValue).subtract(Vector3.FromArray(extentValue).scale(0.5));
4254
+ const fixedMax = Vector3.FromArray(centerValue).add(Vector3.FromArray(extentValue).scale(0.5));
4255
+ const inverse = Matrix.Invert(camera.getTransformationMatrix());
4256
+ const engine = camera.getScene().getEngine();
4257
+ const ndcNearZ = engine.isNDCHalfZRange ? 0 : -1;
4258
+ const frustumCorners = [];
4259
+ for (const x of [-1, 1]) {
4260
+ for (const y of [-1, 1]) {
4261
+ for (const z of [ndcNearZ, 1]) {
4262
+ frustumCorners.push(Vector3.TransformCoordinates(new Vector3(x, y, z), inverse));
4263
+ }
4264
+ }
4265
+ }
4266
+ const frustumMin = frustumCorners.reduce((result, point) => Vector3.Minimize(result, point), frustumCorners[0].clone());
4267
+ const frustumMax = frustumCorners.reduce((result, point) => Vector3.Maximize(result, point), frustumCorners[0].clone());
4268
+ const min = Vector3.Maximize(frustumMin, fixedMin);
4269
+ const max = Vector3.Minimize(frustumMax, fixedMax);
4270
+ if (min.x > max.x || min.y > max.y || min.z > max.z)
4271
+ return null;
4272
+ return Object.freeze({ min: vectorTuple(min), max: vectorTuple(max) });
4273
+ }
4274
+ catch {
4275
+ return null;
4276
+ }
4277
+ }
4278
+ function vectorTuple(value) {
4279
+ return Object.freeze([value.x, value.y, value.z]);
4280
+ }
4281
+ function resolveAutomaticActivityTarget(bindings, target, objectByRenderable, targetIndex) {
4282
+ const className = readBabylonClassName(target);
4283
+ const resolved = new Set();
4284
+ if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
4285
+ return Object.freeze({ kind: 'transform', meshes: resolved });
4286
+ }
4287
+ if (className === 'MorphTarget') {
4288
+ for (const mesh of targetIndex.meshesByMorphTarget.get(target) ?? [])
4289
+ resolved.add(mesh);
4290
+ return Object.freeze({ kind: 'morph', meshes: resolved });
4291
+ }
4292
+ const skeletons = new Set(targetIndex.skeletonsByTarget.get(target) ?? []);
4293
+ const directSkeleton = callOptional(target, 'getSkeleton')
4294
+ ?? target?._skeleton
4295
+ ?? null;
4296
+ if (directSkeleton)
4297
+ skeletons.add(directSkeleton);
4298
+ if (className === 'Bone' || hasFunction(target, 'getSkeleton') || skeletons.size > 0) {
4299
+ for (const skeleton of skeletons) {
4300
+ for (const mesh of targetIndex.meshesBySkeleton.get(skeleton) ?? [])
4301
+ resolved.add(mesh);
4302
+ }
4303
+ return Object.freeze({ kind: 'animation', meshes: resolved });
4304
+ }
4305
+ const authoredOwner = resolveBabylonNodeAuthoredOwner(target, objectByRenderable);
4306
+ if (authoredOwner) {
4307
+ for (const mesh of targetIndex.meshesByRenderable.get(authoredOwner.renderableId) ?? []) {
4308
+ if (mesh === authoredOwner.node || isBabylonNodeDescendantOf(mesh, authoredOwner.node)) {
4309
+ resolved.add(mesh);
4310
+ }
4311
+ }
4312
+ return Object.freeze({ kind: 'transform', meshes: resolved });
4313
+ }
4314
+ for (const [mesh, binding] of bindings) {
4315
+ if (mesh === target
4316
+ || isBabylonNodeDescendantOf(mesh, target)
4317
+ || isBabylonNodeDescendantOf(target, mesh)) {
4318
+ resolved.add(mesh);
4319
+ }
4320
+ }
4321
+ return Object.freeze({ kind: 'transform', meshes: resolved });
4322
+ }
4323
+ function resolveBabylonNodeAuthoredOwner(target, objectByRenderable) {
4324
+ let current = target;
4325
+ while (current && (typeof current === 'object' || typeof current === 'function')) {
4326
+ for (const candidate of collectIdentityCandidates(current)) {
4327
+ if (objectByRenderable.has(candidate)) {
4328
+ return Object.freeze({
4329
+ node: current,
4330
+ renderableId: candidate,
4331
+ });
4332
+ }
4333
+ }
4334
+ current = current.parent ?? null;
4335
+ }
4336
+ return null;
4337
+ }
4338
+ function createAutomaticActivityTargetIndex(bindings, targets) {
4339
+ const meshesByRenderable = new Map();
4340
+ const meshesBySkeleton = new Map();
4341
+ const skeletonsByTarget = new Map();
4342
+ const meshesByMorphTarget = new Map();
4343
+ const indexedSkeletons = new Set();
4344
+ for (const [mesh, binding] of bindings) {
4345
+ addMapSetValue(meshesByRenderable, binding.renderableId, mesh);
4346
+ const skeleton = mesh.skeleton;
4347
+ if (skeleton) {
4348
+ addMapSetValue(meshesBySkeleton, skeleton, mesh);
4349
+ }
4350
+ if (skeleton && !indexedSkeletons.has(skeleton)) {
4351
+ indexedSkeletons.add(skeleton);
4352
+ for (const bone of skeleton.bones ?? []) {
4353
+ if (targets.has(bone))
4354
+ addMapSetValue(skeletonsByTarget, bone, skeleton);
4355
+ const transformNode = callOptional(bone, 'getTransformNode')
4356
+ ?? bone?._linkedTransformNode
4357
+ ?? null;
4358
+ if (transformNode && targets.has(transformNode)) {
4359
+ addMapSetValue(skeletonsByTarget, transformNode, skeleton);
4360
+ }
4361
+ }
4362
+ }
4363
+ const manager = mesh.morphTargetManager;
4364
+ const count = manager?.numTargets ?? 0;
4365
+ if (!manager?.getTarget || !Number.isInteger(count) || count < 1)
4366
+ continue;
4367
+ for (let index = 0; index < count; index += 1) {
4368
+ let morphTarget;
4369
+ try {
4370
+ morphTarget = manager.getTarget(index);
4371
+ }
4372
+ catch {
4373
+ continue;
4374
+ }
4375
+ if (morphTarget && targets.has(morphTarget)) {
4376
+ addMapSetValue(meshesByMorphTarget, morphTarget, mesh);
4377
+ }
4378
+ }
4379
+ }
4380
+ return {
4381
+ meshesByRenderable,
4382
+ meshesBySkeleton,
4383
+ skeletonsByTarget,
4384
+ meshesByMorphTarget,
4385
+ };
4386
+ }
4387
+ function addMapSetValue(map, key, value) {
4388
+ const values = map.get(key);
4389
+ if (values) {
4390
+ values.add(value);
4391
+ }
4392
+ else {
4393
+ map.set(key, new Set([value]));
4394
+ }
4395
+ }
4396
+ function readBabylonClassName(value) {
4397
+ const className = callOptional(value, 'getClassName');
4398
+ return typeof className === 'string' ? className : null;
4399
+ }
4400
+ function isBabylonNodeDescendantOf(value, ancestor) {
4401
+ if (!hasFunction(value, 'isDescendantOf'))
4402
+ return false;
4403
+ try {
4404
+ return value.isDescendantOf(ancestor);
4405
+ }
4406
+ catch {
4407
+ return false;
4408
+ }
4409
+ }
4410
+ function hasFunction(value, key) {
4411
+ return Boolean(value)
4412
+ && (typeof value === 'object' || typeof value === 'function')
4413
+ && typeof value[key] === 'function';
4414
+ }
4415
+ function callOptional(value, key) {
4416
+ if (!hasFunction(value, key))
4417
+ return undefined;
4418
+ try {
4419
+ return value[key]();
4420
+ }
4421
+ catch {
4422
+ return undefined;
4423
+ }
4424
+ }
4425
+ function isStaticDirtyReason(reason) {
4426
+ return reason === 'static-transform-changed'
4427
+ || reason === 'static-geometry-changed'
4428
+ || reason === 'static-depth-invalidated';
4429
+ }
4430
+ function isDynamicDirtyReason(reason) {
4431
+ return reason === 'dynamic-transform-changed'
4432
+ || reason === 'caster-activity-started'
4433
+ || reason === 'caster-activity-stopped'
4434
+ || reason === 'dynamic-frame'
4435
+ || reason === 'skinned-animation-frame'
4436
+ || reason === 'morph-animation-frame'
4437
+ || reason === 'physics-motion-frame'
4438
+ || reason === 'dynamic-depth-invalidated';
4439
+ }
4440
+ function dirtyReasonForActivityKind(kind) {
4441
+ switch (kind) {
4442
+ case 'animation': return 'skinned-animation-frame';
4443
+ case 'morph': return 'morph-animation-frame';
4444
+ case 'physics': return 'physics-motion-frame';
4445
+ case 'transform': return 'dynamic-frame';
4446
+ }
4447
+ }
4448
+ function captureLight(light) {
4449
+ return {
4450
+ light,
4451
+ direction: light.direction.clone(),
4452
+ position: light.position.clone(),
4453
+ autoUpdateExtends: light.autoUpdateExtends,
4454
+ shadowEnabled: light.shadowEnabled,
4455
+ orthoLeft: light.orthoLeft,
4456
+ orthoRight: light.orthoRight,
4457
+ orthoTop: light.orthoTop,
4458
+ orthoBottom: light.orthoBottom,
4459
+ shadowMinZ: light.shadowMinZ,
4460
+ shadowMaxZ: light.shadowMaxZ,
4461
+ };
4462
+ }
4463
+ function restoreLight(state) {
4464
+ const light = state.light;
4465
+ if (light.isDisposed())
4466
+ return;
4467
+ light.direction.copyFrom(state.direction);
4468
+ light.position.copyFrom(state.position);
4469
+ light.autoUpdateExtends = state.autoUpdateExtends;
4470
+ light.shadowEnabled = state.shadowEnabled;
4471
+ light.orthoLeft = state.orthoLeft;
4472
+ light.orthoRight = state.orthoRight;
4473
+ light.orthoTop = state.orthoTop;
4474
+ light.orthoBottom = state.orthoBottom;
4475
+ light.shadowMinZ = state.shadowMinZ;
4476
+ light.shadowMaxZ = state.shadowMaxZ;
4477
+ }
4478
+ function readShadowGenerators(light) {
4479
+ const generators = light.getShadowGenerators?.();
4480
+ if (!generators)
4481
+ return [];
4482
+ if (Array.isArray(generators))
4483
+ return generators;
4484
+ if (generators instanceof Map)
4485
+ return [...generators.values()];
4486
+ return [];
4487
+ }
4488
+ function isRecord(value) {
4489
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
4490
+ }
4491
+ function attachFailureMetadata(error, metadata) {
4492
+ try {
4493
+ Object.assign(error, metadata);
4494
+ }
4495
+ catch {
4496
+ // Error annotation is diagnostic only; it must never replace the primary failure.
4497
+ }
4498
+ }
4499
+ function createInitializationFailure(error, cleanup, initializationCleanupErrors) {
4500
+ const metadata = {
4501
+ initializationCleanupErrors: Object.freeze([...initializationCleanupErrors]),
4502
+ shadowMapExperimentSystemCleanup: cleanup,
4503
+ };
4504
+ if (error && (typeof error === 'object' || typeof error === 'function')) {
4505
+ try {
4506
+ Object.assign(error, metadata);
4507
+ if (readBabylonShadowMapExperimentSystemCleanup(error) === cleanup)
4508
+ return error;
4509
+ }
4510
+ catch {
4511
+ // A non-extensible primary error is wrapped below so cleanup ownership remains reachable.
4512
+ }
4513
+ }
4514
+ const failure = new Error(error instanceof Error ? error.message : 'shadowMapExperiment.initializationFailed');
4515
+ return Object.assign(failure, { cause: error, ...metadata });
4516
+ }
4517
+ function createRuntimeRegistrationRollbackFailure(error, owner, rollbackErrors) {
4518
+ const metadata = {
4519
+ rollbackErrors: Object.freeze([...rollbackErrors]),
4520
+ runtimeShadowObjectsRecoveryOwner: owner,
4521
+ };
4522
+ if (error && (typeof error === 'object' || typeof error === 'function')) {
4523
+ try {
4524
+ Object.assign(error, metadata);
4525
+ if (readBabylonRuntimeShadowObjectsRecoveryOwner(error) === owner)
4526
+ return error;
4527
+ }
4528
+ catch {
4529
+ // A non-extensible primary error is wrapped below so recovery ownership remains reachable.
4530
+ }
4531
+ }
4532
+ const failure = new Error(error instanceof Error ? error.message : 'shadowMapExperiment.runtimeRegistrationFailed');
4533
+ return Object.assign(failure, { cause: error, ...metadata });
4534
+ }
4535
+ //# sourceMappingURL=shadow-map-experiment-system.js.map