@woosh/meep-engine 3.15.0 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (952) hide show
  1. package/editor/Editor.js +7 -7
  2. package/editor/actions/concrete/TransformModifyAction.d.ts +4 -4
  3. package/editor/actions/concrete/TransformModifyAction.d.ts.map +1 -1
  4. package/editor/actions/concrete/TransformModifyAction.js +4 -4
  5. package/editor/camera/camera_framing.js +7 -7
  6. package/editor/ecs/component/editors/ecs/Transform64Editor.d.ts +20 -0
  7. package/editor/ecs/component/editors/ecs/Transform64Editor.d.ts.map +1 -0
  8. package/editor/ecs/component/editors/ecs/Transform64Editor.js +70 -0
  9. package/editor/ecs/component/prototypeObjectEditor.js +2 -2
  10. package/editor/ecs/component/registerEngineComponentEditors.js +3 -3
  11. package/editor/particles/effect/ParticleEffectDocument.d.ts +396 -0
  12. package/editor/particles/effect/ParticleEffectDocument.d.ts.map +1 -0
  13. package/editor/particles/effect/ParticleEffectDocument.js +989 -0
  14. package/editor/particles/effect/ParticleReferenceSimulation.d.ts +113 -0
  15. package/editor/particles/effect/ParticleReferenceSimulation.d.ts.map +1 -0
  16. package/editor/particles/effect/ParticleReferenceSimulation.js +377 -0
  17. package/editor/particles/effect/actions/ParticleGroupDefineAction.d.ts +35 -0
  18. package/editor/particles/effect/actions/ParticleGroupDefineAction.d.ts.map +1 -0
  19. package/editor/particles/effect/actions/ParticleGroupDefineAction.js +45 -0
  20. package/editor/particles/effect/make_starter_particle_effect.d.ts +23 -0
  21. package/editor/particles/effect/make_starter_particle_effect.d.ts.map +1 -0
  22. package/editor/particles/effect/make_starter_particle_effect.js +177 -0
  23. package/editor/persistence/buildNewSceneDataset.d.ts.map +1 -1
  24. package/editor/persistence/buildNewSceneDataset.js +10 -8
  25. package/editor/process/symbolic/AudioEmitterSymbolicDisplay.js +67 -67
  26. package/editor/process/symbolic/CameraSymbolicDisplay.js +146 -146
  27. package/editor/process/symbolic/GridPositionSymbolicDisplay.js +6 -6
  28. package/editor/process/symbolic/LightSymbolicDisplay.js +172 -172
  29. package/editor/process/symbolic/ParticleEmitterSymbolicDisplay.d.ts.map +1 -1
  30. package/editor/process/symbolic/ParticleEmitterSymbolicDisplay.js +6 -3
  31. package/editor/process/symbolic/PositionedMarkerSymbolicDisplay.js +6 -6
  32. package/editor/selection/editor_pick.js +130 -130
  33. package/editor/templates/entity_templates.js +7 -7
  34. package/editor/tools/TransformToolV2.d.ts +3 -3
  35. package/editor/tools/TransformToolV2.d.ts.map +1 -1
  36. package/editor/tools/TransformToolV2.js +58 -25
  37. package/editor/tools/paint/TerrainHeightPaintTool.d.ts.map +1 -1
  38. package/editor/tools/paint/TerrainHeightPaintTool.js +11 -9
  39. package/editor/tools/v2/GizmoNode.js +2 -2
  40. package/editor/tools/v2/TransformControls.d.ts +3 -3
  41. package/editor/tools/v2/TransformControls.d.ts.map +1 -1
  42. package/editor/tools/v2/TransformControls.js +1003 -971
  43. package/editor/tools/v2/handle/gizmo_drag_plane.d.ts.map +1 -1
  44. package/editor/tools/v2/handle/gizmo_drag_plane.js +6 -1
  45. package/editor/view/ecs/components/common/AutoCanvasView.js +17 -0
  46. package/editor/view/node-graph/NodeGraphEditorView.d.ts +143 -2
  47. package/editor/view/node-graph/NodeGraphEditorView.d.ts.map +1 -1
  48. package/editor/view/node-graph/NodeGraphEditorView.js +358 -6
  49. package/editor/view/node-graph/NodeGraphMinimapView.d.ts +120 -0
  50. package/editor/view/node-graph/NodeGraphMinimapView.d.ts.map +1 -0
  51. package/editor/view/node-graph/NodeGraphMinimapView.js +859 -0
  52. package/editor/view/node-graph/NodeGraphPaletteView.d.ts +112 -0
  53. package/editor/view/node-graph/NodeGraphPaletteView.d.ts.map +1 -0
  54. package/editor/view/node-graph/NodeGraphPaletteView.js +352 -0
  55. package/editor/view/node-graph/NodeGraphSearchMenuView.d.ts +76 -0
  56. package/editor/view/node-graph/NodeGraphSearchMenuView.d.ts.map +1 -0
  57. package/editor/view/node-graph/NodeGraphSearchMenuView.js +247 -0
  58. package/editor/view/node-graph/NodeGraphView.d.ts +147 -5
  59. package/editor/view/node-graph/NodeGraphView.d.ts.map +1 -1
  60. package/editor/view/node-graph/NodeGraphView.js +809 -76
  61. package/editor/view/node-graph/NodeIconView.d.ts +33 -0
  62. package/editor/view/node-graph/NodeIconView.d.ts.map +1 -0
  63. package/editor/view/node-graph/NodeIconView.js +54 -0
  64. package/editor/view/node-graph/NodeView.d.ts +71 -1
  65. package/editor/view/node-graph/NodeView.d.ts.map +1 -1
  66. package/editor/view/node-graph/NodeView.js +318 -17
  67. package/editor/view/node-graph/PortView.d.ts +41 -1
  68. package/editor/view/node-graph/PortView.d.ts.map +1 -1
  69. package/editor/view/node-graph/PortView.js +121 -69
  70. package/editor/view/node-graph/actions/ConnectionDeleteAction.d.ts +16 -3
  71. package/editor/view/node-graph/actions/ConnectionDeleteAction.d.ts.map +1 -1
  72. package/editor/view/node-graph/actions/ConnectionDeleteAction.js +29 -3
  73. package/editor/view/node-graph/actions/NodeCreateAction.d.ts +65 -0
  74. package/editor/view/node-graph/actions/NodeCreateAction.d.ts.map +1 -0
  75. package/editor/view/node-graph/actions/NodeCreateAction.js +108 -0
  76. package/editor/view/node-graph/actions/NodeDeleteAction.d.ts +6 -0
  77. package/editor/view/node-graph/actions/NodeDeleteAction.d.ts.map +1 -1
  78. package/editor/view/node-graph/actions/NodeDeleteAction.js +24 -2
  79. package/editor/view/node-graph/connection_wire_geometry.d.ts +34 -0
  80. package/editor/view/node-graph/connection_wire_geometry.d.ts.map +1 -1
  81. package/editor/view/node-graph/connection_wire_geometry.js +79 -3
  82. package/editor/view/node-graph/node-graph.css +678 -0
  83. package/editor/view/particles/effect/ParticleCodeEditorView.d.ts +37 -0
  84. package/editor/view/particles/effect/ParticleCodeEditorView.d.ts.map +1 -0
  85. package/editor/view/particles/effect/ParticleCodeEditorView.js +479 -0
  86. package/editor/view/particles/effect/ParticleEffectEditorView.d.ts +61 -0
  87. package/editor/view/particles/effect/ParticleEffectEditorView.d.ts.map +1 -0
  88. package/editor/view/particles/effect/ParticleEffectEditorView.js +223 -0
  89. package/editor/view/particles/effect/ParticleEmitterInspectorView.d.ts +25 -0
  90. package/editor/view/particles/effect/ParticleEmitterInspectorView.d.ts.map +1 -0
  91. package/editor/view/particles/effect/ParticleEmitterInspectorView.js +606 -0
  92. package/editor/view/particles/effect/ParticleGraphEditorView.d.ts +156 -0
  93. package/editor/view/particles/effect/ParticleGraphEditorView.d.ts.map +1 -0
  94. package/editor/view/particles/effect/ParticleGraphEditorView.js +1636 -0
  95. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts +22 -0
  96. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -0
  97. package/editor/view/particles/effect/ParticleNodeParametersView.js +520 -0
  98. package/editor/view/particles/effect/ParticlePreviewView.d.ts +52 -0
  99. package/editor/view/particles/effect/ParticlePreviewView.d.ts.map +1 -0
  100. package/editor/view/particles/effect/ParticlePreviewView.js +533 -0
  101. package/editor/view/particles/effect/particle-editor.css +556 -0
  102. package/editor/view/prepareMeshLibrary.d.ts.map +1 -1
  103. package/editor/view/prepareMeshLibrary.js +8 -4
  104. package/package.json +1 -1
  105. package/samples/engine/first-scene/main.js +152 -152
  106. package/samples/generation/generators/mir_generator_place_bases.js +3 -3
  107. package/src/core/assert.d.ts +28 -24
  108. package/src/core/assert.d.ts.map +1 -1
  109. package/src/core/assert.js +34 -26
  110. package/src/core/geom/3d/shape/PosedShape3D.d.ts +17 -0
  111. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  112. package/src/core/geom/3d/shape/PosedShape3D.js +34 -2
  113. package/src/core/graph/format/graphviz/graphviz_escape_html_label.d.ts +17 -0
  114. package/src/core/graph/format/graphviz/graphviz_escape_html_label.d.ts.map +1 -0
  115. package/src/core/graph/format/graphviz/graphviz_escape_html_label.js +25 -0
  116. package/src/core/graph/format/graphviz/graphviz_escape_quoted.d.ts +16 -0
  117. package/src/core/graph/format/graphviz/graphviz_escape_quoted.d.ts.map +1 -0
  118. package/src/core/graph/format/graphviz/graphviz_escape_quoted.js +24 -0
  119. package/src/core/model/node-graph/NodeGraph.d.ts +12 -0
  120. package/src/core/model/node-graph/NodeGraph.d.ts.map +1 -1
  121. package/src/core/model/node-graph/NodeGraph.js +21 -6
  122. package/src/core/model/node-graph/node/NodeDescription.d.ts +36 -0
  123. package/src/core/model/node-graph/node/NodeDescription.d.ts.map +1 -1
  124. package/src/core/model/node-graph/node/NodeDescription.js +41 -0
  125. package/src/core/model/node-graph/visual/ConnectionVisualData.d.ts +119 -0
  126. package/src/core/model/node-graph/visual/ConnectionVisualData.d.ts.map +1 -0
  127. package/src/core/model/node-graph/visual/ConnectionVisualData.js +317 -0
  128. package/src/core/model/node-graph/visual/NodeDescriptionVisualRegistry.d.ts +4 -0
  129. package/src/core/model/node-graph/visual/NodeDescriptionVisualRegistry.d.ts.map +1 -1
  130. package/src/core/model/node-graph/visual/NodeDescriptionVisualRegistry.js +125 -22
  131. package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts +91 -2
  132. package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts.map +1 -1
  133. package/src/core/model/node-graph/visual/NodeGraphVisualData.js +164 -113
  134. package/src/core/model/node-graph/visual/layout/LayoutBoxGraph.d.ts +162 -0
  135. package/src/core/model/node-graph/visual/layout/LayoutBoxGraph.d.ts.map +1 -0
  136. package/src/core/model/node-graph/visual/layout/LayoutBoxGraph.js +288 -0
  137. package/src/core/model/node-graph/visual/layout/LayoutProblem.d.ts +109 -0
  138. package/src/core/model/node-graph/visual/layout/LayoutProblem.d.ts.map +1 -0
  139. package/src/core/model/node-graph/visual/layout/LayoutProblem.js +141 -0
  140. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts +149 -0
  141. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts.map +1 -0
  142. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.js +175 -0
  143. package/src/core/model/node-graph/visual/layout/RouteGrid.d.ts +137 -0
  144. package/src/core/model/node-graph/visual/layout/RouteGrid.d.ts.map +1 -0
  145. package/src/core/model/node-graph/visual/layout/RouteGrid.js +242 -0
  146. package/src/core/model/node-graph/visual/layout/RouteSearch.d.ts +114 -0
  147. package/src/core/model/node-graph/visual/layout/RouteSearch.d.ts.map +1 -0
  148. package/src/core/model/node-graph/visual/layout/RouteSearch.js +300 -0
  149. package/src/core/model/node-graph/visual/layout/RouteSet.d.ts +86 -0
  150. package/src/core/model/node-graph/visual/layout/RouteSet.d.ts.map +1 -0
  151. package/src/core/model/node-graph/visual/layout/RouteSet.js +168 -0
  152. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.d.ts +48 -0
  153. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.d.ts.map +1 -0
  154. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.js +555 -0
  155. package/src/core/model/node-graph/visual/layout/layout_assign_layers.d.ts +34 -0
  156. package/src/core/model/node-graph/visual/layout/layout_assign_layers.d.ts.map +1 -0
  157. package/src/core/model/node-graph/visual/layout/layout_assign_layers.js +202 -0
  158. package/src/core/model/node-graph/visual/layout/layout_break_cycles.d.ts +34 -0
  159. package/src/core/model/node-graph/visual/layout/layout_break_cycles.d.ts.map +1 -0
  160. package/src/core/model/node-graph/visual/layout/layout_break_cycles.js +254 -0
  161. package/src/core/model/node-graph/visual/layout/layout_build_problem.d.ts +29 -0
  162. package/src/core/model/node-graph/visual/layout/layout_build_problem.d.ts.map +1 -0
  163. package/src/core/model/node-graph/visual/layout/layout_build_problem.js +184 -0
  164. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts +29 -0
  165. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts.map +1 -0
  166. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.js +95 -0
  167. package/src/core/model/node-graph/visual/layout/layout_node_graph.d.ts +56 -0
  168. package/src/core/model/node-graph/visual/layout/layout_node_graph.d.ts.map +1 -0
  169. package/src/core/model/node-graph/visual/layout/layout_node_graph.js +131 -0
  170. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts +36 -0
  171. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts.map +1 -0
  172. package/src/core/model/node-graph/visual/layout/layout_order_layers.js +415 -0
  173. package/src/core/model/node-graph/visual/layout/layout_orient_links.d.ts +21 -0
  174. package/src/core/model/node-graph/visual/layout/layout_orient_links.d.ts.map +1 -0
  175. package/src/core/model/node-graph/visual/layout/layout_orient_links.js +49 -0
  176. package/src/core/model/node-graph/visual/layout/route_connections.d.ts +41 -0
  177. package/src/core/model/node-graph/visual/layout/route_connections.d.ts.map +1 -0
  178. package/src/core/model/node-graph/visual/layout/route_connections.js +309 -0
  179. package/src/core/model/node-graph/visual/layout/route_grid_build.d.ts +33 -0
  180. package/src/core/model/node-graph/visual/layout/route_grid_build.d.ts.map +1 -0
  181. package/src/core/model/node-graph/visual/layout/route_grid_build.js +237 -0
  182. package/src/core/model/node-graph/visual/layout/route_nudge.d.ts +25 -0
  183. package/src/core/model/node-graph/visual/layout/route_nudge.d.ts.map +1 -0
  184. package/src/core/model/node-graph/visual/layout/route_nudge.js +220 -0
  185. package/src/core/model/node-graph/visual/layout/route_to_spline.d.ts +29 -0
  186. package/src/core/model/node-graph/visual/layout/route_to_spline.d.ts.map +1 -0
  187. package/src/core/model/node-graph/visual/layout/route_to_spline.js +120 -0
  188. package/src/engine/EngineHarness.d.ts.map +1 -1
  189. package/src/engine/EngineHarness.js +593 -586
  190. package/src/engine/EntityCreator.js +116 -116
  191. package/src/engine/achievements/AchievementManager.js +437 -437
  192. package/src/engine/animation/AnimatedActions.js +111 -111
  193. package/src/engine/animation/AnimationUtils.d.ts.map +1 -1
  194. package/src/engine/animation/AnimationUtils.js +246 -234
  195. package/src/engine/animation/Animations.d.ts +1 -0
  196. package/src/engine/animation/Animations.d.ts.map +1 -1
  197. package/src/engine/animation/Animations.js +13 -5
  198. package/src/engine/animation/clip/bind_property_writer.d.ts.map +1 -1
  199. package/src/engine/animation/clip/bind_property_writer.js +12 -5
  200. package/src/engine/animation/curve/binding/BoundTransform64Writer.d.ts +29 -0
  201. package/src/engine/animation/curve/binding/BoundTransform64Writer.d.ts.map +1 -0
  202. package/src/engine/animation/curve/binding/BoundTransform64Writer.js +80 -0
  203. package/src/engine/animation/removeEntityWithMeshParticlesEffect.js +4 -4
  204. package/src/engine/asset/loaders/gltf_test_fixtures.d.ts.map +1 -1
  205. package/src/engine/asset/loaders/gltf_test_fixtures.js +5 -4
  206. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +12 -12
  207. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  208. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +77 -44
  209. package/src/engine/control/first-person/abilities/LedgeGrab.js +1 -1
  210. package/src/engine/control/first-person/abilities/Mantle.js +1 -1
  211. package/src/engine/control/first-person/abilities/Slide.js +1 -1
  212. package/src/engine/control/first-person/collision/KinematicMover.d.ts +5 -9
  213. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
  214. package/src/engine/control/first-person/collision/KinematicMover.js +59 -29
  215. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +2 -2
  216. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
  217. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +7 -7
  218. package/src/engine/ecs/EventType.d.ts +1 -0
  219. package/src/engine/ecs/EventType.js +35 -10
  220. package/src/engine/ecs/dynamic_actions/actions/definition/SendRequestActionDescription.d.ts.map +1 -1
  221. package/src/engine/ecs/dynamic_actions/actions/definition/SendRequestActionDescription.js +12 -4
  222. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts +4 -4
  223. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts.map +1 -1
  224. package/src/engine/ecs/fow/FogOfWarRevealerSystem.js +10 -11
  225. package/src/engine/ecs/grid/pick.js +1 -1
  226. package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.d.ts +1 -1
  227. package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.js +13 -13
  228. package/src/engine/ecs/gui/hud/testHudEdgeStick.js +3 -3
  229. package/src/engine/ecs/ik/InverseKinematicsSystem.d.ts +10 -10
  230. package/src/engine/ecs/ik/InverseKinematicsSystem.d.ts.map +1 -1
  231. package/src/engine/ecs/ik/InverseKinematicsSystem.js +8 -8
  232. package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.d.ts.map +1 -1
  233. package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.js +120 -97
  234. package/src/engine/ecs/ik/ik_bone_chain.d.ts +16 -5
  235. package/src/engine/ecs/ik/ik_bone_chain.d.ts.map +1 -1
  236. package/src/engine/ecs/ik/ik_bone_chain.js +16 -4
  237. package/src/engine/ecs/ik/ik_probe_terrain_contact.d.ts +7 -3
  238. package/src/engine/ecs/ik/ik_probe_terrain_contact.d.ts.map +1 -1
  239. package/src/engine/ecs/ik/ik_probe_terrain_contact.js +20 -16
  240. package/src/engine/ecs/ik/ik_write_local_rotation.d.ts +18 -0
  241. package/src/engine/ecs/ik/ik_write_local_rotation.d.ts.map +1 -0
  242. package/src/engine/ecs/ik/ik_write_local_rotation.js +30 -0
  243. package/src/engine/ecs/parent/EntityNode.d.ts +24 -6
  244. package/src/engine/ecs/parent/EntityNode.d.ts.map +1 -1
  245. package/src/engine/ecs/parent/EntityNode.js +54 -42
  246. package/src/engine/ecs/parent/EntityNodeFlags.d.ts +0 -1
  247. package/src/engine/ecs/parent/EntityNodeFlags.js +7 -8
  248. package/src/engine/ecs/speaker/VoiceSystem.d.ts +2 -2
  249. package/src/engine/ecs/speaker/VoiceSystem.d.ts.map +1 -1
  250. package/src/engine/ecs/speaker/VoiceSystem.js +4 -4
  251. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  252. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +2 -0
  253. package/src/engine/ecs/terrain/ecs/Terrain.d.ts +5 -2
  254. package/src/engine/ecs/terrain/ecs/Terrain.d.ts.map +1 -1
  255. package/src/engine/ecs/terrain/ecs/Terrain.js +7 -4
  256. package/src/engine/ecs/terrain/ecs/TerrainSystem.js +3 -3
  257. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts +7 -7
  258. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
  259. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +84 -36
  260. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +513 -0
  261. package/src/engine/ecs/transform/Transform64.d.ts +48 -11
  262. package/src/engine/ecs/transform/Transform64.d.ts.map +1 -1
  263. package/src/engine/ecs/transform/Transform64.js +54 -15
  264. package/src/engine/ecs/transform/Transform64SerializationAdapter.d.ts +37 -0
  265. package/src/engine/ecs/transform/Transform64SerializationAdapter.d.ts.map +1 -0
  266. package/src/engine/ecs/transform/Transform64SerializationAdapter.js +75 -0
  267. package/src/engine/ecs/transform/__probe.d.ts +4 -0
  268. package/src/engine/ecs/transform/__probe.d.ts.map +1 -0
  269. package/src/engine/ecs/transform/t64_allocation_probe.d.ts +31 -0
  270. package/src/engine/ecs/transform/t64_allocation_probe.d.ts.map +1 -0
  271. package/src/engine/ecs/transform/t64_allocation_probe.js +78 -0
  272. package/src/engine/ecs/transform/t64_announce_change.d.ts +33 -0
  273. package/src/engine/ecs/transform/t64_announce_change.d.ts.map +1 -0
  274. package/src/engine/ecs/transform/t64_announce_change.js +39 -0
  275. package/src/engine/ecs/transform/t64_equals_components.d.ts +30 -0
  276. package/src/engine/ecs/transform/t64_equals_components.d.ts.map +1 -0
  277. package/src/engine/ecs/transform/t64_equals_components.js +59 -0
  278. package/src/engine/ecs/transform/t64_read_rotation.d.ts +13 -0
  279. package/src/engine/ecs/transform/t64_read_rotation.d.ts.map +1 -0
  280. package/src/engine/ecs/transform/t64_read_rotation.js +16 -0
  281. package/src/engine/ecs/transform/t64_read_translation.d.ts +14 -0
  282. package/src/engine/ecs/transform/t64_read_translation.d.ts.map +1 -0
  283. package/src/engine/ecs/transform/t64_read_translation.js +17 -0
  284. package/src/engine/ecs/transform-attachment/TransformAttachment.d.ts +5 -5
  285. package/src/engine/ecs/transform-attachment/TransformAttachment.d.ts.map +1 -1
  286. package/src/engine/ecs/transform-attachment/TransformAttachment.js +4 -4
  287. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.d.ts +10 -10
  288. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.d.ts.map +1 -1
  289. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js +71 -20
  290. package/src/engine/graphics/debug/createDebugLabel.js +3 -3
  291. package/src/engine/graphics/ecs/camera/pp/PerfectPanner.d.ts.map +1 -1
  292. package/src/engine/graphics/ecs/camera/pp/PerfectPanner.js +5 -4
  293. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  294. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +8 -3
  295. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +6 -6
  296. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  297. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +25 -13
  298. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraLanderSystem.js +1 -1
  299. package/src/engine/graphics/ecs/mesh/setMesh.js +2 -2
  300. package/src/engine/graphics/ecs/path/entity/EntityPath.d.ts.map +1 -1
  301. package/src/engine/graphics/ecs/path/entity/EntityPath.js +21 -7
  302. package/src/engine/graphics/ecs/path/entity/EntityPathMarkerDefinition.d.ts +4 -4
  303. package/src/engine/graphics/ecs/path/entity/EntityPathMarkerDefinition.d.ts.map +1 -1
  304. package/src/engine/graphics/ecs/path/entity/EntityPathMarkerDefinition.js +4 -4
  305. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts +2 -2
  306. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts.map +1 -1
  307. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.js +145 -135
  308. package/src/engine/graphics/geometry/MikkT/Build4RuleGroups.js +2 -2
  309. package/src/engine/graphics/geometry/MikkT/DegenEpilogue.js +1 -1
  310. package/src/engine/graphics/geometry/MikkT/DegenPrologue.js +1 -1
  311. package/src/engine/graphics/geometry/MikkT/EvalTspace.js +1 -1
  312. package/src/engine/graphics/geometry/MikkT/GenerateTSpaces.js +3 -3
  313. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +659 -0
  314. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +21 -5
  315. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts +45 -0
  316. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts.map +1 -1
  317. package/src/engine/graphics/render/frame_graph/FrameGraph.js +282 -60
  318. package/src/engine/graphics/render/frame_graph/FramePassBuilder.d.ts.map +1 -1
  319. package/src/engine/graphics/render/frame_graph/FramePassBuilder.js +20 -9
  320. package/src/engine/graphics/render/frame_graph/FramePassNode.d.ts +12 -0
  321. package/src/engine/graphics/render/frame_graph/FramePassNode.d.ts.map +1 -1
  322. package/src/engine/graphics/render/frame_graph/FramePassNode.js +13 -0
  323. package/src/engine/graphics/render/frame_graph/FramePassResources.d.ts.map +1 -1
  324. package/src/engine/graphics/render/frame_graph/FramePassResources.js +5 -0
  325. package/src/engine/graphics/render/frame_graph/ResourceEntry.d.ts +19 -1
  326. package/src/engine/graphics/render/frame_graph/ResourceEntry.d.ts.map +1 -1
  327. package/src/engine/graphics/render/frame_graph/ResourceEntry.js +23 -1
  328. package/src/engine/graphics/render/frame_graph/backend/void/VoidRenderContext.d.ts +15 -1
  329. package/src/engine/graphics/render/frame_graph/backend/void/VoidRenderContext.d.ts.map +1 -1
  330. package/src/engine/graphics/render/frame_graph/backend/void/VoidRenderContext.js +33 -16
  331. package/src/engine/graphics/render/frame_graph/resource/RenderResourceManager.d.ts.map +1 -1
  332. package/src/engine/graphics/render/frame_graph/resource/RenderResourceManager.js +19 -1
  333. package/src/engine/graphics/sh3/path_tracer/populate_path_traced_scene_from_ecd.js +6 -6
  334. package/src/engine/graphics3/CameraSystem.d.ts +6 -6
  335. package/src/engine/graphics3/CameraSystem.d.ts.map +1 -1
  336. package/src/engine/graphics3/CameraSystem.js +170 -170
  337. package/src/engine/graphics3/DecalSystem.d.ts +6 -6
  338. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  339. package/src/engine/graphics3/DecalSystem.js +449 -449
  340. package/src/engine/graphics3/LightSystem.d.ts +6 -6
  341. package/src/engine/graphics3/LightSystem.d.ts.map +1 -1
  342. package/src/engine/graphics3/LightSystem.js +234 -221
  343. package/src/engine/graphics3/MeshSystem.d.ts +6 -6
  344. package/src/engine/graphics3/MeshSystem.d.ts.map +1 -1
  345. package/src/engine/graphics3/MeshSystem.js +53 -22
  346. package/src/engine/graphics3/ParticipatingMediaSystem.d.ts +20 -15
  347. package/src/engine/graphics3/ParticipatingMediaSystem.d.ts.map +1 -1
  348. package/src/engine/graphics3/ParticipatingMediaSystem.js +198 -184
  349. package/src/engine/graphics3/ParticleEmitterSystem.d.ts +6 -6
  350. package/src/engine/graphics3/ParticleEmitterSystem.d.ts.map +1 -1
  351. package/src/engine/graphics3/ParticleEmitterSystem.js +310 -289
  352. package/src/engine/graphics3/PickingSystem.d.ts +6 -6
  353. package/src/engine/graphics3/PickingSystem.d.ts.map +1 -1
  354. package/src/engine/graphics3/PickingSystem.js +273 -273
  355. package/src/engine/graphics3/ShadedGeometrySystem.d.ts +6 -6
  356. package/src/engine/graphics3/ShadedGeometrySystem.d.ts.map +1 -1
  357. package/src/engine/graphics3/ShadedGeometrySystem.js +180 -169
  358. package/src/engine/graphics3/TerrainSystem.d.ts.map +1 -1
  359. package/src/engine/graphics3/TerrainSystem.js +565 -566
  360. package/src/engine/graphics3/Trail3DSystem.d.ts +6 -6
  361. package/src/engine/graphics3/Trail3DSystem.d.ts.map +1 -1
  362. package/src/engine/graphics3/Trail3DSystem.js +221 -221
  363. package/src/engine/graphics3/camera_sync_from_transform.d.ts +2 -2
  364. package/src/engine/graphics3/camera_sync_from_transform.d.ts.map +1 -1
  365. package/src/engine/graphics3/camera_sync_from_transform.js +2 -1
  366. package/src/engine/graphics3/debug/gizmo_view_from_camera.js +4 -4
  367. package/src/engine/graphics3/particles/GPUParticularRenderer.d.ts +6 -0
  368. package/src/engine/graphics3/particles/GPUParticularRenderer.d.ts.map +1 -1
  369. package/src/engine/graphics3/particles/GPUParticularRenderer.js +82 -53
  370. package/src/engine/graphics3/particles/particle_gpu_records.d.ts +12 -8
  371. package/src/engine/graphics3/particles/particle_gpu_records.d.ts.map +1 -1
  372. package/src/engine/graphics3/particles/particle_gpu_records.js +53 -45
  373. package/src/engine/graphics3/preview/make_model_preview_scene.js +1 -1
  374. package/src/engine/graphics3/shade_camera_projection_ray.js +4 -4
  375. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts.map +1 -1
  376. package/src/engine/graphics3/shade_node_to_entity_composition.js +88 -84
  377. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts +7 -7
  378. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts.map +1 -1
  379. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.js +8 -8
  380. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.d.ts +9 -8
  381. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.d.ts.map +1 -1
  382. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.js +29 -8
  383. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +2 -2
  384. package/src/engine/intelligence/behavior/ecs/OrbitingBehavior.d.ts.map +1 -1
  385. package/src/engine/intelligence/behavior/ecs/OrbitingBehavior.js +35 -21
  386. package/src/engine/intelligence/behavior/util/RotationBehavior.js +5 -5
  387. package/src/engine/intelligence/mcts/StateNode.js +1 -1
  388. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +7 -7
  389. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -1
  390. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +14 -15
  391. package/src/engine/interpolation/pose_interpoland.d.ts +2 -2
  392. package/src/engine/interpolation/pose_interpoland.js +4 -4
  393. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts +4 -4
  394. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts.map +1 -1
  395. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +39 -14
  396. package/src/engine/network/adapters/TransformReplicationAdapter.d.ts +7 -7
  397. package/src/engine/network/adapters/TransformReplicationAdapter.d.ts.map +1 -1
  398. package/src/engine/network/adapters/TransformReplicationAdapter.js +19 -20
  399. package/src/engine/network/replication/Replicator.d.ts +5 -3
  400. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  401. package/src/engine/network/replication/Replicator.js +65 -16
  402. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +3 -3
  403. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  404. package/src/engine/physics/ccd/linear_sweep.js +9 -3
  405. package/src/engine/physics/constraint/solve_constraints.js +10 -10
  406. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +8 -8
  407. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
  408. package/src/engine/physics/ecs/ColliderObserverSystem.js +6 -6
  409. package/src/engine/physics/ecs/PhysicsSystem.d.ts +22 -32
  410. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  411. package/src/engine/physics/ecs/PhysicsSystem.js +2337 -2310
  412. package/src/engine/physics/ecs/find_non_finite_physics_state.js +6 -6
  413. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +7 -7
  414. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts.map +1 -1
  415. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.js +16 -8
  416. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +4 -4
  417. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts.map +1 -1
  418. package/src/engine/physics/fluid/ecs/FluidSystem.js +6 -6
  419. package/src/engine/physics/fluid/ecs/WorkerFluidSystem.d.ts +2 -2
  420. package/src/engine/physics/fluid/ecs/WorkerFluidSystem.d.ts.map +1 -1
  421. package/src/engine/physics/fluid/ecs/WorkerFluidSystem.js +4 -4
  422. package/src/engine/physics/fluid/ecs/fluid_reanchor_field.js +7 -7
  423. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +4 -4
  424. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +7 -7
  425. package/src/engine/physics/inertia/world_inverse_inertia.js +1 -1
  426. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  427. package/src/engine/physics/integration/integrate_position.js +13 -12
  428. package/src/engine/physics/inverse_kinematics/fabrik/fabrik_solve.d.ts.map +1 -1
  429. package/src/engine/physics/inverse_kinematics/fabrik/fabrik_solve.js +23 -5
  430. package/src/engine/physics/narrowphase/compute_penetration.js +18 -14
  431. package/src/engine/physics/narrowphase/convex_convex_manifold.js +2 -2
  432. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +9 -9
  433. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  434. package/src/engine/physics/narrowphase/narrowphase_step.js +95 -68
  435. package/src/engine/physics/narrowphase/refine_ray_concave.js +1 -1
  436. package/src/engine/physics/narrowphase/refine_ray_hit.js +3 -3
  437. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  438. package/src/engine/physics/queries/overlap_shape.js +19 -7
  439. package/src/engine/physics/queries/raycast.js +3 -3
  440. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  441. package/src/engine/physics/queries/shape_cast.js +628 -620
  442. package/src/engine/physics/solver/solve_contacts.js +10 -10
  443. package/src/engine/physics/vehicle/RaycastVehicle.d.ts +6 -5
  444. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  445. package/src/engine/physics/vehicle/RaycastVehicle.js +10 -5
  446. package/src/engine/sound/ecs/SoundListenerSystem.d.ts +16 -4
  447. package/src/engine/sound/ecs/SoundListenerSystem.d.ts.map +1 -1
  448. package/src/engine/sound/ecs/SoundListenerSystem.js +70 -11
  449. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +7 -7
  450. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
  451. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +343 -338
  452. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +2 -2
  453. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
  454. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +356 -345
  455. package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts +23 -2
  456. package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts.map +1 -1
  457. package/src/engine/sound/ecs/audio/SpatialAudioIndex.js +174 -140
  458. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.d.ts +6 -6
  459. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.d.ts.map +1 -1
  460. package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.js +185 -184
  461. package/src/engine/sound/material/detector/terrain/TerrainSoundMaterialSurfaceDetector.js +1 -1
  462. package/src/engine/sound/simulation/core/AcousticRegionIndex.d.ts +2 -1
  463. package/src/engine/sound/simulation/core/AcousticRegionIndex.d.ts.map +1 -1
  464. package/src/engine/sound/simulation/core/AcousticRegionIndex.js +4 -3
  465. package/src/engine/sound/simulation/core/AcousticVolumeRegion.d.ts +3 -2
  466. package/src/engine/sound/simulation/core/AcousticVolumeRegion.d.ts.map +1 -1
  467. package/src/engine/sound/simulation/core/AcousticVolumeRegion.js +6 -4
  468. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.d.ts +10 -6
  469. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.d.ts.map +1 -1
  470. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.js +32 -7
  471. package/src/engine/sound/simulation/ecs/AcousticVolumeSystem.d.ts +7 -7
  472. package/src/engine/sound/simulation/ecs/AcousticVolumeSystem.d.ts.map +1 -1
  473. package/src/engine/sound/simulation/ecs/AcousticVolumeSystem.js +6 -6
  474. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  475. package/src/engine/sound/sopra/runtime/EventInstance.js +8 -2
  476. package/src/format/scene/gltf/gltf_component_type.d.ts.map +1 -1
  477. package/src/format/scene/gltf/gltf_component_type.js +3 -1
  478. package/src/format/texture/ktx2/ktx2_read.d.ts.map +1 -1
  479. package/src/format/texture/ktx2/ktx2_read.js +2 -1
  480. package/src/generation/grid/generation/GridTaskDensityMarkerDistribution.d.ts.map +1 -1
  481. package/src/generation/grid/generation/GridTaskDensityMarkerDistribution.js +8 -1
  482. package/src/generation/markers/GridCellActionPlaceMarker.d.ts +4 -4
  483. package/src/generation/markers/GridCellActionPlaceMarker.d.ts.map +1 -1
  484. package/src/generation/markers/GridCellActionPlaceMarker.js +16 -6
  485. package/src/generation/markers/MarkerNode.d.ts +3 -3
  486. package/src/generation/markers/MarkerNode.d.ts.map +1 -1
  487. package/src/generation/markers/MarkerNode.js +3 -3
  488. package/src/generation/markers/actions/MarkerNodeActionEntityPlacement.d.ts +4 -4
  489. package/src/generation/markers/actions/MarkerNodeActionEntityPlacement.d.ts.map +1 -1
  490. package/src/generation/markers/actions/MarkerNodeActionEntityPlacement.js +7 -7
  491. package/src/generation/markers/actions/MarkerNodeActionImaginary.d.ts.map +1 -1
  492. package/src/generation/markers/actions/MarkerNodeActionImaginary.js +20 -2
  493. package/src/generation/markers/actions/placement/MarkerNodeEntityProcessorClingToTerrain.js +1 -1
  494. package/src/generation/markers/actions/placement/MarkerNodeEntityProcessorRandomRotation.d.ts.map +1 -1
  495. package/src/generation/markers/actions/placement/MarkerNodeEntityProcessorRandomRotation.js +13 -3
  496. package/src/generation/markers/transform/MarkerNodeTransformRotateRandom.d.ts.map +1 -1
  497. package/src/generation/markers/transform/MarkerNodeTransformRotateRandom.js +11 -1
  498. package/src/generation/markers/transform/MarkerNodeTransformerAddPositionYFromFilter.d.ts.map +1 -1
  499. package/src/generation/markers/transform/MarkerNodeTransformerAddPositionYFromFilter.js +8 -4
  500. package/src/generation/markers/transform/MarkerNodeTransformerOffsetPosition.d.ts.map +1 -1
  501. package/src/generation/markers/transform/MarkerNodeTransformerOffsetPosition.js +13 -5
  502. package/src/generation/markers/transform/MarkerNodeTransformerYRotateByFilter.d.ts.map +1 -1
  503. package/src/generation/markers/transform/MarkerNodeTransformerYRotateByFilter.js +8 -2
  504. package/src/generation/markers/transform/MarkerNodeTransformerYRotateByFilterGradient.d.ts.map +1 -1
  505. package/src/generation/markers/transform/MarkerNodeTransformerYRotateByFilterGradient.js +16 -2
  506. package/src/shade/device/graph/BufferResourceDescriptor.d.ts.map +1 -1
  507. package/src/shade/device/graph/BufferResourceDescriptor.js +193 -184
  508. package/src/shade/device/graph/TextureResourceDescriptor.d.ts.map +1 -1
  509. package/src/shade/device/graph/TextureResourceDescriptor.js +12 -0
  510. package/src/shade/device/mock/SoftwareGPUDevice.d.ts.map +1 -1
  511. package/src/shade/device/mock/SoftwareGPUDevice.js +8 -0
  512. package/src/shade/playground/avboit/main.js +3 -3
  513. package/src/shade/playground/basis_textures/main.js +1 -1
  514. package/src/shade/playground/ground_seam/main.js +1 -1
  515. package/src/shade/playground/particle_editor/README.md +175 -0
  516. package/src/shade/playground/particle_editor/index.html +76 -0
  517. package/src/shade/playground/particle_editor/main.d.ts +2 -0
  518. package/src/shade/playground/particle_editor/main.d.ts.map +1 -0
  519. package/src/shade/playground/particle_editor/main.js +139 -0
  520. package/src/shade/playground/particle_system/README.md +92 -62
  521. package/src/shade/playground/particle_system/index.html +67 -21
  522. package/src/shade/playground/particle_system/main.js +247 -102
  523. package/src/shade/{renderer/particles → playground/particle_system}/particle_prototype.d.ts +21 -18
  524. package/src/shade/playground/particle_system/particle_prototype.d.ts.map +1 -0
  525. package/src/shade/{renderer/particles → playground/particle_system}/particle_prototype.js +197 -181
  526. package/src/shade/playground/particle_system/particle_scene.d.ts +136 -20
  527. package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -1
  528. package/src/shade/playground/particle_system/particle_scene.js +487 -670
  529. package/src/shade/playground/skinned_blas_refit/sweep_views.js +2 -2
  530. package/src/shade/playground/skinned_blas_refit/verify_instance_bounds.d.ts.map +1 -1
  531. package/src/shade/playground/skinned_blas_refit/verify_instance_bounds.js +2 -1
  532. package/src/shade/playground/skinned_blas_refit/verify_skinned_blas_refit.d.ts.map +1 -1
  533. package/src/shade/playground/skinned_blas_refit/verify_skinned_blas_refit.js +2 -1
  534. package/src/shade/playground/sponza_path_trace/main.js +1 -1
  535. package/src/shade/playground/volumetrics_froxel/main.js +917 -917
  536. package/src/shade/playground/volumetrics_froxel/measure.js +559 -559
  537. package/src/shade/renderer/Renderer.d.ts +28 -0
  538. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  539. package/src/shade/renderer/Renderer.js +140 -8
  540. package/src/shade/renderer/atmosphere/GPUSky.d.ts.map +1 -1
  541. package/src/shade/renderer/atmosphere/GPUSky.js +4 -2
  542. package/src/shade/renderer/buffer/graph_read_buffer.d.ts.map +1 -1
  543. package/src/shade/renderer/buffer/graph_read_buffer.js +8 -1
  544. package/src/shade/renderer/buffer/table/GPUTypedTable.js +1 -1
  545. package/src/shade/renderer/buffer/table/GPUTypedTableDescriptor.d.ts +30 -0
  546. package/src/shade/renderer/buffer/table/GPUTypedTableDescriptor.d.ts.map +1 -1
  547. package/src/shade/renderer/buffer/table/GPUTypedTableDescriptor.js +84 -0
  548. package/src/shade/renderer/buffer/table/gpu_database_emulator_image.d.ts +27 -0
  549. package/src/shade/renderer/buffer/table/gpu_database_emulator_image.d.ts.map +1 -0
  550. package/src/shade/renderer/buffer/table/gpu_database_words.d.ts +24 -0
  551. package/src/shade/renderer/buffer/table/gpu_database_words.d.ts.map +1 -0
  552. package/src/shade/renderer/buffer/table/gpu_database_words.js +92 -0
  553. package/src/shade/renderer/camera/Camera.d.ts +2 -2
  554. package/src/shade/renderer/camera/Camera.d.ts.map +1 -1
  555. package/src/shade/renderer/camera/Camera.js +11 -5
  556. package/src/shade/renderer/camera/GPUCameraContext.d.ts.map +1 -1
  557. package/src/shade/renderer/camera/GPUCameraContext.js +2 -1
  558. package/src/shade/renderer/camera/OrbitalCameraController.d.ts +3 -3
  559. package/src/shade/renderer/camera/OrbitalCameraController.d.ts.map +1 -1
  560. package/src/shade/renderer/camera/OrbitalCameraController.js +31 -16
  561. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +2 -2
  562. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  563. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +4 -3
  564. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +10 -5
  565. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +1 -1
  566. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +8 -4
  567. package/src/shade/renderer/extension/FrameContext.d.ts.map +1 -1
  568. package/src/shade/renderer/extension/FrameContext.js +5 -0
  569. package/src/shade/renderer/extension/FrameRecord.d.ts +16 -0
  570. package/src/shade/renderer/extension/FrameRecord.d.ts.map +1 -1
  571. package/src/shade/renderer/extension/FrameRecord.js +31 -0
  572. package/src/shade/renderer/extension/GBufferTextures.d.ts +11 -0
  573. package/src/shade/renderer/extension/GBufferTextures.d.ts.map +1 -1
  574. package/src/shade/renderer/extension/GBufferTextures.js +14 -0
  575. package/src/shade/renderer/geometry/attribute/gpu_vertex_buffer_layout_from_spec.js +1 -1
  576. package/src/shade/renderer/geometry/util/geometry_stripify.js +6 -6
  577. package/src/shade/renderer/geometry/virtual/format/prototypeVGEOFormatViewer.js +8 -8
  578. package/src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.d.ts.map +1 -1
  579. package/src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.js +14 -5
  580. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.d.ts.map +1 -1
  581. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.js +17 -9
  582. package/src/shade/renderer/global_illumination/probe/octahedral/border/graph_lpv_paint_border_depth.d.ts +2 -1
  583. package/src/shade/renderer/global_illumination/probe/octahedral/border/graph_lpv_paint_border_depth.d.ts.map +1 -1
  584. package/src/shade/renderer/global_illumination/probe/octahedral/border/graph_lpv_paint_border_depth.js +4 -1
  585. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.d.ts +2 -1
  586. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.d.ts.map +1 -1
  587. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.js +11 -1
  588. package/src/shade/renderer/global_illumination/probe/octahedral/graph_lpv_shade.d.ts +1 -1
  589. package/src/shade/renderer/global_illumination/probe/octahedral/graph_lpv_shade.d.ts.map +1 -1
  590. package/src/shade/renderer/global_illumination/probe/octahedral/graph_lpv_shade.js +4 -2
  591. package/src/shade/renderer/global_illumination/sharc/SpatialHashRadianceCache.d.ts.map +1 -1
  592. package/src/shade/renderer/global_illumination/sharc/SpatialHashRadianceCache.js +5 -1
  593. package/src/shade/renderer/light/cluster/cull/graph_cull_lights.d.ts.map +1 -1
  594. package/src/shade/renderer/light/cluster/cull/graph_cull_lights.js +35 -11
  595. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  596. package/src/shade/renderer/particles/DESIGN.md +271 -112
  597. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +251 -86
  598. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  599. package/src/shade/renderer/particles/GPUParticleSystem.js +822 -146
  600. package/src/shade/renderer/particles/ParticleConstants.d.ts +96 -3
  601. package/src/shade/renderer/particles/ParticleConstants.d.ts.map +1 -1
  602. package/src/shade/renderer/particles/ParticleConstants.js +103 -4
  603. package/src/shade/renderer/particles/coherence/graph_particle_bucket.d.ts +5 -4
  604. package/src/shade/renderer/particles/coherence/graph_particle_bucket.d.ts.map +1 -1
  605. package/src/shade/renderer/particles/coherence/graph_particle_bucket.js +11 -15
  606. package/src/shade/renderer/particles/coherence/shader_particle_bucket.d.ts +2 -2
  607. package/src/shade/renderer/particles/coherence/shader_particle_bucket.d.ts.map +1 -1
  608. package/src/shade/renderer/particles/coherence/shader_particle_bucket.js +20 -15
  609. package/src/shade/renderer/particles/data/PARTICLE_COUNTERS.d.ts +7 -8
  610. package/src/shade/renderer/particles/data/PARTICLE_COUNTERS.d.ts.map +1 -1
  611. package/src/shade/renderer/particles/data/PARTICLE_COUNTERS.js +7 -17
  612. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.d.ts +56 -0
  613. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.d.ts.map +1 -0
  614. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.js +68 -0
  615. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STATE.d.ts +39 -0
  616. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STATE.d.ts.map +1 -0
  617. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STATE.js +52 -0
  618. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.d.ts +83 -0
  619. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.d.ts.map +1 -0
  620. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.js +166 -0
  621. package/src/shade/renderer/particles/data/chunk_particle_emitter.d.ts +21 -5
  622. package/src/shade/renderer/particles/data/chunk_particle_emitter.d.ts.map +1 -1
  623. package/src/shade/renderer/particles/data/chunk_particle_emitter.js +44 -85
  624. package/src/shade/renderer/particles/data/particle_header.d.ts +62 -0
  625. package/src/shade/renderer/particles/data/particle_header.d.ts.map +1 -0
  626. package/src/shade/renderer/particles/data/particle_header.js +103 -0
  627. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts +179 -0
  628. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts.map +1 -0
  629. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.js +303 -0
  630. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +120 -1
  631. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -1
  632. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +90 -1
  633. package/src/shade/renderer/particles/graph/ParticleNodeGroup.d.ts +283 -0
  634. package/src/shade/renderer/particles/graph/ParticleNodeGroup.d.ts.map +1 -0
  635. package/src/shade/renderer/particles/graph/ParticleNodeGroup.js +419 -0
  636. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +44 -2
  637. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  638. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +476 -40
  639. package/src/shade/renderer/particles/graph/ParticleValueType.d.ts +188 -0
  640. package/src/shade/renderer/particles/graph/ParticleValueType.d.ts.map +1 -0
  641. package/src/shade/renderer/particles/graph/ParticleValueType.js +287 -0
  642. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +16 -9
  643. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  644. package/src/shade/renderer/particles/graph/compile_particle_graph.js +464 -205
  645. package/src/shade/renderer/particles/graph/flatten_particle_graph.d.ts +11 -0
  646. package/src/shade/renderer/particles/graph/flatten_particle_graph.d.ts.map +1 -0
  647. package/src/shade/renderer/particles/graph/flatten_particle_graph.js +213 -0
  648. package/src/shade/renderer/particles/graph/groups/particle_builtin_groups.d.ts +27 -0
  649. package/src/shade/renderer/particles/graph/groups/particle_builtin_groups.d.ts.map +1 -0
  650. package/src/shade/renderer/particles/graph/groups/particle_builtin_groups.js +75 -0
  651. package/src/shade/renderer/particles/graph/groups/particle_groups_geometry.d.ts +83 -0
  652. package/src/shade/renderer/particles/graph/groups/particle_groups_geometry.d.ts.map +1 -0
  653. package/src/shade/renderer/particles/graph/groups/particle_groups_geometry.js +319 -0
  654. package/src/shade/renderer/particles/graph/groups/particle_groups_noise.d.ts +22 -0
  655. package/src/shade/renderer/particles/graph/groups/particle_groups_noise.d.ts.map +1 -0
  656. package/src/shade/renderer/particles/graph/groups/particle_groups_noise.js +517 -0
  657. package/src/shade/renderer/particles/graph/groups/particle_groups_utility.d.ts +70 -0
  658. package/src/shade/renderer/particles/graph/groups/particle_groups_utility.d.ts.map +1 -0
  659. package/src/shade/renderer/particles/graph/groups/particle_groups_utility.js +161 -0
  660. package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts +8 -0
  661. package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts.map +1 -0
  662. package/src/shade/renderer/particles/graph/groups/simplex_reference.js +159 -0
  663. package/src/shade/renderer/particles/graph/infer_particle_graph_types.d.ts +88 -0
  664. package/src/shade/renderer/particles/graph/infer_particle_graph_types.d.ts.map +1 -0
  665. package/src/shade/renderer/particles/graph/infer_particle_graph_types.js +234 -0
  666. package/src/shade/renderer/particles/graph/make_particle_group.d.ts +98 -0
  667. package/src/shade/renderer/particles/graph/make_particle_group.d.ts.map +1 -0
  668. package/src/shade/renderer/particles/graph/make_particle_group.js +427 -0
  669. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +11 -4
  670. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -1
  671. package/src/shade/renderer/particles/graph/particle_graph_authoring.js +121 -108
  672. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.d.ts +38 -0
  673. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.d.ts.map +1 -0
  674. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.js +25 -0
  675. package/src/shade/renderer/particles/graph/particle_graph_distributions.d.ts +29 -0
  676. package/src/shade/renderer/particles/graph/particle_graph_distributions.d.ts.map +1 -0
  677. package/src/shade/renderer/particles/graph/particle_graph_distributions.js +170 -0
  678. package/src/shade/renderer/particles/graph/particle_graph_json.d.ts +87 -0
  679. package/src/shade/renderer/particles/graph/particle_graph_json.d.ts.map +1 -0
  680. package/src/shade/renderer/particles/graph/particle_graph_json.js +196 -0
  681. package/src/shade/renderer/particles/graph/particle_group_json.d.ts +86 -0
  682. package/src/shade/renderer/particles/graph/particle_group_json.d.ts.map +1 -0
  683. package/src/shade/renderer/particles/graph/particle_group_json.js +182 -0
  684. package/src/shade/renderer/particles/graph/particle_group_library.d.ts +33 -0
  685. package/src/shade/renderer/particles/graph/particle_group_library.d.ts.map +1 -0
  686. package/src/shade/renderer/particles/graph/particle_group_library.js +75 -0
  687. package/src/shade/renderer/particles/graph/particle_node_library.d.ts +37 -0
  688. package/src/shade/renderer/particles/graph/particle_node_library.d.ts.map +1 -0
  689. package/src/shade/renderer/particles/graph/particle_node_library.js +64 -0
  690. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts +146 -0
  691. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts.map +1 -0
  692. package/src/shade/renderer/particles/graph/particle_node_parameters.js +234 -0
  693. package/src/shade/renderer/particles/graph/particle_node_presentation.d.ts +30 -0
  694. package/src/shade/renderer/particles/graph/particle_node_presentation.d.ts.map +1 -0
  695. package/src/shade/renderer/particles/graph/particle_node_presentation.js +286 -0
  696. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts +66 -0
  697. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts.map +1 -0
  698. package/src/shade/renderer/particles/graph/validate_particle_graph.js +439 -0
  699. package/src/shade/renderer/particles/graph_particles.d.ts +143 -68
  700. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  701. package/src/shade/renderer/particles/graph_particles.js +424 -71
  702. package/src/shade/renderer/particles/graph_particles_avboit.d.ts +107 -0
  703. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -0
  704. package/src/shade/renderer/particles/graph_particles_avboit.js +312 -0
  705. package/src/shade/renderer/particles/isa/InstructionStream.d.ts +41 -8
  706. package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -1
  707. package/src/shade/renderer/particles/isa/InstructionStream.js +111 -33
  708. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +13 -10
  709. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
  710. package/src/shade/renderer/particles/isa/ParticleAssembler.js +30 -26
  711. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts +27 -15
  712. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts.map +1 -1
  713. package/src/shade/renderer/particles/isa/ParticleProgram.js +139 -34
  714. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts +117 -9
  715. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
  716. package/src/shade/renderer/particles/isa/ParticleVMISA.js +296 -37
  717. package/src/shade/renderer/particles/isa/particle_assembly.d.ts +149 -0
  718. package/src/shade/renderer/particles/isa/particle_assembly.d.ts.map +1 -0
  719. package/src/shade/renderer/particles/isa/particle_assembly.js +999 -0
  720. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts +25 -3
  721. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts.map +1 -1
  722. package/src/shade/renderer/particles/layout/ParticleLayout.js +33 -5
  723. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts +160 -0
  724. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts.map +1 -0
  725. package/src/shade/renderer/particles/optimizer/optimize_particle_program.js +478 -0
  726. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts +291 -0
  727. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts.map +1 -0
  728. package/src/shade/renderer/particles/optimizer/particle_program_analysis.js +647 -0
  729. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts +95 -0
  730. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts.map +1 -0
  731. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.js +213 -0
  732. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.d.ts +16 -0
  733. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.d.ts.map +1 -0
  734. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.js +223 -0
  735. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts +121 -0
  736. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -0
  737. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +206 -0
  738. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.d.ts +11 -0
  739. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.d.ts.map +1 -0
  740. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.js +197 -0
  741. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.d.ts +4 -0
  742. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.d.ts.map +1 -0
  743. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.js +51 -0
  744. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts +4 -0
  745. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -0
  746. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +561 -0
  747. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.d.ts +4 -0
  748. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.d.ts.map +1 -0
  749. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.js +72 -0
  750. package/src/shade/renderer/particles/optimizer/pass_pack_registers.d.ts +4 -0
  751. package/src/shade/renderer/particles/optimizer/pass_pack_registers.d.ts.map +1 -0
  752. package/src/shade/renderer/particles/optimizer/pass_pack_registers.js +86 -0
  753. package/src/shade/renderer/particles/optimizer/pass_peephole.d.ts +4 -0
  754. package/src/shade/renderer/particles/optimizer/pass_peephole.d.ts.map +1 -0
  755. package/src/shade/renderer/particles/optimizer/pass_peephole.js +271 -0
  756. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.d.ts +4 -0
  757. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.d.ts.map +1 -0
  758. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.js +268 -0
  759. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.d.ts +4 -0
  760. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.d.ts.map +1 -0
  761. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.js +151 -0
  762. package/src/shade/renderer/particles/particle_test_fixtures.d.ts +131 -0
  763. package/src/shade/renderer/particles/particle_test_fixtures.d.ts.map +1 -0
  764. package/src/shade/renderer/particles/particle_test_fixtures.js +203 -0
  765. package/src/shade/renderer/particles/runtime/Emitter.d.ts +219 -0
  766. package/src/shade/renderer/particles/runtime/Emitter.d.ts.map +1 -0
  767. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts +128 -60
  768. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts.map +1 -1
  769. package/src/shade/renderer/particles/runtime/EmitterRegistry.js +286 -117
  770. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +264 -0
  771. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -0
  772. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +404 -0
  773. package/src/shade/renderer/particles/runtime/ParticleSpawnScheduler.d.ts +49 -15
  774. package/src/shade/renderer/particles/runtime/ParticleSpawnScheduler.d.ts.map +1 -1
  775. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts +51 -18
  776. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts.map +1 -1
  777. package/src/shade/renderer/particles/runtime/ProgramArena.js +72 -32
  778. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
  779. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  780. package/src/shade/renderer/particles/runtime/create_particle_effect.js +4 -4
  781. package/src/shade/renderer/particles/runtime/emitter_database_image.d.ts +15 -0
  782. package/src/shade/renderer/particles/runtime/emitter_database_image.d.ts.map +1 -0
  783. package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.d.ts +17 -0
  784. package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.d.ts.map +1 -0
  785. package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.js +75 -0
  786. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.d.ts +20 -0
  787. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.d.ts.map +1 -0
  788. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.js +94 -0
  789. package/src/shade/renderer/particles/shaders/chunk_particle_context.d.ts +15 -5
  790. package/src/shade/renderer/particles/shaders/chunk_particle_context.d.ts.map +1 -1
  791. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts +1 -2
  792. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +1 -1
  793. package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +1 -2
  794. package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.d.ts +11 -0
  795. package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.d.ts.map +1 -0
  796. package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.js +13 -0
  797. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_age.d.ts +17 -0
  798. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_age.d.ts.map +1 -0
  799. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_age.js +22 -0
  800. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts +14 -0
  801. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts.map +1 -0
  802. package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.js +23 -0
  803. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +1 -1
  804. package/src/shade/renderer/particles/shaders/chunk_particle_scene_view_depth.d.ts +18 -0
  805. package/src/shade/renderer/particles/shaders/chunk_particle_scene_view_depth.d.ts.map +1 -0
  806. package/src/shade/renderer/particles/shaders/chunk_particle_scene_view_depth.js +24 -0
  807. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.d.ts +26 -0
  808. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.d.ts.map +1 -0
  809. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.js +59 -0
  810. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts +14 -0
  811. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -0
  812. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +153 -0
  813. package/src/shade/renderer/particles/shaders/shader_particle_avboit_occupancy.d.ts +3 -0
  814. package/src/shade/renderer/particles/shaders/shader_particle_avboit_occupancy.d.ts.map +1 -0
  815. package/src/shade/renderer/particles/shaders/shader_particle_avboit_occupancy.js +124 -0
  816. package/src/shade/renderer/particles/shaders/shader_particle_avboit_splat.d.ts +19 -0
  817. package/src/shade/renderer/particles/shaders/shader_particle_avboit_splat.d.ts.map +1 -0
  818. package/src/shade/renderer/particles/shaders/shader_particle_avboit_splat.js +147 -0
  819. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts +18 -8
  820. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  821. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +72 -37
  822. package/src/shade/renderer/particles/shaders/shader_particle_emitter_tick.d.ts +5 -0
  823. package/src/shade/renderer/particles/shaders/shader_particle_emitter_tick.d.ts.map +1 -0
  824. package/src/shade/renderer/particles/shaders/shader_particle_emitter_tick.js +216 -0
  825. package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
  826. package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +7 -3
  827. package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
  828. package/src/shade/renderer/particles/shaders/shader_particle_render.js +132 -197
  829. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  830. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +78 -24
  831. package/src/shade/renderer/particles/shaders/shader_particle_spawn_commands.d.ts +26 -0
  832. package/src/shade/renderer/particles/shaders/shader_particle_spawn_commands.d.ts.map +1 -0
  833. package/src/shade/renderer/particles/shaders/shader_particle_spawn_commands.js +66 -0
  834. package/src/shade/renderer/particles/sort/shader_particle_sort.d.ts +1 -1
  835. package/src/shade/renderer/particles/sort/shader_particle_sort.d.ts.map +1 -1
  836. package/src/shade/renderer/particles/sort/shader_particle_sort.js +21 -36
  837. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +2 -2
  838. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  839. package/src/shade/renderer/particles/vm/ParticleVMReference.js +100 -90
  840. package/src/shade/renderer/particles/vm/chunk_particle_vm.d.ts.map +1 -1
  841. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +270 -138
  842. package/src/shade/renderer/postprocess/denoise/debug/graph_pass_debug_reprojection.d.ts +1 -2
  843. package/src/shade/renderer/postprocess/denoise/debug/graph_pass_debug_reprojection.d.ts.map +1 -1
  844. package/src/shade/renderer/postprocess/denoise/debug/graph_pass_debug_reprojection.js +0 -3
  845. package/src/shade/renderer/postprocess/dof/gather/DepthOfFieldU.js +1 -1
  846. package/src/shade/renderer/postprocess/dof/raymarch/DepthOfField.js +1 -1
  847. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  848. package/src/shade/renderer/postprocess/nss/NSS.d.ts.map +1 -1
  849. package/src/shade/renderer/postprocess/nss/NSS.js +3 -2
  850. package/src/shade/renderer/postprocess/nss/generate_NSS_WEIGHTS_ACTUAL.js +3 -2
  851. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +87 -0
  852. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -0
  853. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +61 -0
  854. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +179 -0
  855. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +27 -2
  856. package/src/shade/renderer/rasterize/native/avboit/avboit_draw_fragment_body.d.ts +9 -2
  857. package/src/shade/renderer/rasterize/native/avboit/avboit_draw_fragment_body.d.ts.map +1 -1
  858. package/src/shade/renderer/rasterize/native/avboit/avboit_draw_fragment_body.js +12 -3
  859. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_splat_slice.d.ts +18 -0
  860. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_splat_slice.d.ts.map +1 -0
  861. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_splat_slice.js +58 -0
  862. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts +25 -1
  863. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  864. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +69 -10
  865. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_resolve.d.ts.map +1 -1
  866. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_resolve.js +10 -4
  867. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.d.ts.map +1 -1
  868. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.js +4 -39
  869. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts +7 -2
  870. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
  871. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +16 -2
  872. package/src/shade/renderer/scene/optimization/geometry/geometry_deduplicate.d.ts +3 -3
  873. package/src/shade/renderer/scene/optimization/geometry/geometry_deduplicate.d.ts.map +1 -1
  874. package/src/shade/renderer/scene/optimization/geometry/geometry_deduplicate.js +29 -23
  875. package/src/shade/renderer/scene/optimization/geometry/internal/anchor_grow_topological.d.ts +2 -2
  876. package/src/shade/renderer/scene/optimization/geometry/internal/anchor_grow_topological.d.ts.map +1 -1
  877. package/src/shade/renderer/scene/optimization/geometry/internal/anchor_grow_topological.js +3 -3
  878. package/src/shade/renderer/scene/optimization/geometry/internal/cover_greedy.d.ts +4 -4
  879. package/src/shade/renderer/scene/optimization/geometry/internal/cover_greedy.d.ts.map +1 -1
  880. package/src/shade/renderer/scene/optimization/geometry/internal/cover_greedy.js +2 -2
  881. package/src/shade/renderer/scene/optimization/geometry/internal/match_refit_validate.d.ts +2 -2
  882. package/src/shade/renderer/scene/optimization/geometry/internal/match_refit_validate.d.ts.map +1 -1
  883. package/src/shade/renderer/scene/optimization/geometry/internal/match_refit_validate.js +2 -2
  884. package/src/shade/renderer/scene/optimization/geometry/internal/match_solve_transform.d.ts +7 -7
  885. package/src/shade/renderer/scene/optimization/geometry/internal/match_solve_transform.d.ts.map +1 -1
  886. package/src/shade/renderer/scene/optimization/geometry/internal/match_solve_transform.js +79 -71
  887. package/src/shade/renderer/shader/chunk/math/chunk_lower_bound_branchless.d.ts.map +1 -1
  888. package/src/shade/renderer/shader/chunk/math/chunk_lower_bound_branchless.js +3 -1
  889. package/src/shade/renderer/shader/chunk/random/chunk_hash_to_float01.d.ts +13 -3
  890. package/src/shade/renderer/shader/chunk/random/chunk_hash_to_float01.d.ts.map +1 -1
  891. package/src/shade/renderer/shader/chunk/random/chunk_hash_to_float01.js +15 -5
  892. package/src/shade/renderer/shader/chunk/random/chunk_random_uint.d.ts +8 -0
  893. package/src/shade/renderer/shader/chunk/random/chunk_random_uint.d.ts.map +1 -1
  894. package/src/shade/renderer/shader/chunk/random/chunk_random_uint.js +15 -5
  895. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg.d.ts +7 -0
  896. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg.d.ts.map +1 -1
  897. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg.js +17 -7
  898. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg_advance.d.ts +17 -0
  899. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg_advance.d.ts.map +1 -0
  900. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg_advance.js +26 -0
  901. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg_output.d.ts +15 -0
  902. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg_output.d.ts.map +1 -0
  903. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg_output.js +26 -0
  904. package/src/shade/renderer/shadow/map/DirectionalLightShadowmap.d.ts.map +1 -1
  905. package/src/shade/renderer/shadow/map/DirectionalLightShadowmap.js +8 -2
  906. package/src/shade/renderer/shadow/map/PointLightShadowmap.d.ts.map +1 -1
  907. package/src/shade/renderer/shadow/map/PointLightShadowmap.js +7 -2
  908. package/src/shade/renderer/shadow/map/SpotLightShadowmap.d.ts.map +1 -1
  909. package/src/shade/renderer/shadow/map/SpotLightShadowmap.js +3 -15
  910. package/src/shade/renderer/shadow/map/TetrahedronPointLightShadowmap.d.ts.map +1 -1
  911. package/src/shade/renderer/shadow/map/TetrahedronPointLightShadowmap.js +10 -4
  912. package/src/shade/renderer/shadow/vsm/GPUViewShadowMaps.d.ts.map +1 -1
  913. package/src/shade/renderer/shadow/vsm/GPUViewShadowMaps.js +7 -2
  914. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +2 -2
  915. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +3 -3
  916. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  917. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +3 -3
  918. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  919. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +66 -5
  920. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  921. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +35 -4
  922. package/src/view/minimap/Minimap.d.ts.map +1 -1
  923. package/src/view/minimap/Minimap.js +227 -225
  924. package/src/view/minimap/MinimapCanvasView.js +3 -3
  925. package/src/view/minimap/MinimapMarker.d.ts +45 -0
  926. package/src/view/minimap/MinimapMarker.d.ts.map +1 -0
  927. package/src/view/minimap/MinimapMarker.js +80 -0
  928. package/src/view/minimap/MinimapMarkerCollection.d.ts +6 -5
  929. package/src/view/minimap/MinimapMarkerCollection.d.ts.map +1 -1
  930. package/src/view/minimap/MinimapMarkerCollection.js +6 -6
  931. package/src/view/minimap/MinimapMarkerSerializationAdapter.d.ts +19 -0
  932. package/src/view/minimap/MinimapMarkerSerializationAdapter.d.ts.map +1 -0
  933. package/src/view/minimap/MinimapMarkerSerializationAdapter.js +42 -0
  934. package/src/view/minimap/dom/MinimapCameraView.d.ts +1 -1
  935. package/src/view/minimap/dom/MinimapCameraView.d.ts.map +1 -1
  936. package/src/view/minimap/dom/MinimapCameraView.js +23 -4
  937. package/src/view/minimap/dom/MinimapMarkerView.js +3 -3
  938. package/src/view/minimap/gl/MarkerGL.js +3 -3
  939. package/src/view/util/DomLayoutFrame.d.ts +53 -0
  940. package/src/view/util/DomLayoutFrame.d.ts.map +1 -0
  941. package/src/view/util/DomLayoutFrame.js +122 -0
  942. package/src/core/model/node-graph/visual/layout/BoxLayoutSpec.js +0 -19
  943. package/src/core/model/node-graph/visual/layout/ConnectedBoxLayouter.js +0 -241
  944. package/src/core/model/node-graph/visual/layout/ConnectionEndpointLayoutSpec.js +0 -40
  945. package/src/core/model/node-graph/visual/layout/ConnectionLayoutSpec.js +0 -28
  946. package/src/engine/ecs/util/hideEntityGracefully.js +0 -196
  947. package/src/shade/renderer/particles/cull/chunk_particle_cull.js +0 -38
  948. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_LAYOUT.js +0 -214
  949. package/src/shade/renderer/particles/particle_prototype.d.ts.map +0 -1
  950. package/src/shade/renderer/particles/runtime/ParticleSpawnScheduler.js +0 -86
  951. package/src/shade/renderer/particles/runtime/spawn_request_encoding.js +0 -52
  952. package/src/shade/renderer/particles/shaders/chunk_particle_context.js +0 -58
@@ -1,2310 +1,2337 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
3
- import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
4
- import { ebvh_optimize_treelet } from "../../../core/bvh2/bvh3/ebvh_optimize_treelet.js";
5
- import {
6
- bvh_query_user_data_intersects_aabb
7
- } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_aabb.js";
8
- import { returnTrue } from "../../../core/function/returnTrue.js";
9
- import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
10
- import Vector3 from "../../../core/geom/Vector3.js";
11
- import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
12
- import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
13
- import { System } from "../../ecs/System.js";
14
- import { Transform } from "../../ecs/transform/Transform.js";
15
- import { Interpolated } from "../../interpolation/Interpolated.js";
16
- import { body_id_index, BodyStorage } from "../body/BodyStorage.js";
17
- import { SBS_STRIDE, SolverBodyState } from "../body/SolverBodyState.js";
18
- import { compute_world_aabb, fatten_world_aabb } from "../broadphase/compute_fat_world_aabb.js";
19
- import { generate_pairs } from "../broadphase/generate_pairs.js";
20
- import { PairList } from "../broadphase/PairList.js";
21
- import { ccd_release_contacts, ccd_resolve } from "../ccd/linear_sweep.js";
22
- import { solve_joints } from "../constraint/solve_constraints.js";
23
- import { ManifoldStore } from "../contact/ManifoldStore.js";
24
- import { ContactEventBuffer, ContactEventKind } from "../events/ContactEventBuffer.js";
25
- import { diff_manifolds } from "../events/diff_manifolds.js";
26
- import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
27
- import { integrate_position } from "../integration/integrate_position.js";
28
- import { integrate_velocity_forces, integrate_velocity_gravity } from "../integration/integrate_velocity.js";
29
- import { IslandBuilder } from "../island/IslandBuilder.js";
30
- import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
31
- import { overlap_shape as overlap_shape_query } from "../queries/overlap_shape.js";
32
- import { raycast as raycast_query } from "../queries/raycast.js";
33
- import { shape_cast as shape_cast_query } from "../queries/shape_cast.js";
34
- import {
35
- apply_restitution,
36
- prepare_contacts,
37
- redetect_concave_contacts,
38
- refresh_contacts,
39
- solve_position,
40
- solve_velocity,
41
- warm_start_contacts,
42
- } from "../solver/solve_contacts.js";
43
- import { BodyKind } from "./BodyKind.js";
44
- import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
45
- import { find_non_finite_physics_state } from "./find_non_finite_physics_state.js";
46
- import { is_sensor } from "./is_sensor.js";
47
- import { JOINT_UNALLOCATED, JOINT_WORLD } from "./Joint.js";
48
- import { PhysicsEvents } from "./PhysicsEvents.js";
49
- import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
50
- import { RigidBodyFlags } from "./RigidBodyFlags.js";
51
- import { SleepState } from "./SleepState.js";
52
-
53
- /**
54
- * Scratch for {@link applyImpulseAt}'s angular delta calculation.
55
- * @type {Float64Array}
56
- */
57
- const scratch_angular_delta = new Float64Array(3);
58
-
59
- /**
60
- * Scratch for {@link applyImpulseAt}'s LockRot*-masked inverse-inertia
61
- * diagonal (the same mask {@link SolverBodyState#gather} applies).
62
- * @type {{x: number, y: number, z: number}}
63
- */
64
- const scratch_locked_inertia = { x: 0, y: 0, z: 0 };
65
-
66
- /**
67
- * Reusable scratch buffer for world-AABB construction so the link path is
68
- * allocation-free in steady state.
69
- * @type {Float64Array}
70
- */
71
- const scratch_world_aabb = new Float64Array(6);
72
-
73
- /**
74
- * Reusable scratch buffer for the local AABB returned by
75
- * {@link AbstractShape3D#compute_bounding_box}.
76
- * @type {Float64Array}
77
- */
78
- const scratch_local_aabb = new Float64Array(6);
79
-
80
- /**
81
- * Candidate buffer for {@link PhysicsSystem#__wake_sleepers_overlapping_leaf}.
82
- * Grows by doubling: a large removed support (a level floor) can overlap many
83
- * sleeping bodies, all of which must wake.
84
- * @type {Uint32Array}
85
- */
86
- let scratch_wake_candidates = new Uint32Array(64);
87
-
88
- /**
89
- * Scratch for a leaf's currently-stored fat AABB during the stage-2
90
- * containment test (refit skip).
91
- * @type {Float64Array}
92
- */
93
- const scratch_stored_aabb = new Float64Array(6);
94
-
95
- /**
96
- * Scratch boxes for {@link PhysicsSystem#__swept_proximity} — the mover's
97
- * velocity-swept tight AABB and the sleeper's tight AABB.
98
- * @type {Float64Array}
99
- */
100
- const scratch_prox_mover = new Float64Array(6);
101
- const scratch_prox_sleeper = new Float64Array(6);
102
-
103
- /**
104
- * Rigid-body physics system.
105
- *
106
- * v1 scope: pool + active list + two BVHs (static / dynamic), Transform sync
107
- * contract, and the user-facing API surface (gravity, force / impulse, layer
108
- * filtering, wake / sleep, contact filter). The per-step simulation pipeline
109
- * (broadphase pair generation, narrowphase, solver, islands, sleep test,
110
- * contact-event emission) is built on top of this skeleton in subsequent
111
- * iterations.
112
- *
113
- * Dependency tuple is `(RigidBody, Collider, Transform)` — every body has
114
- * exactly one collider on the same entity. Compound bodies via child collider
115
- * entities are an extension point handled by a follow-up sub-observer.
116
- *
117
- * NOTE: requires ColliderObserverSystem to be registered together with this system.
118
- *
119
- * @see ColliderObserverSystem
120
- * @author Alex Goldring
121
- * @copyright Company Named Limited (c) 2026
122
- */
123
- export class PhysicsSystem extends System {
124
-
125
- dependencies = [RigidBody, Transform];
126
- components_used = [
127
- ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Read | ResourceAccessKind.Write),
128
- ResourceAccessSpecification.from(Collider, ResourceAccessKind.Read),
129
- ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read | ResourceAccessKind.Write),
130
- ];
131
-
132
- /**
133
- * Body slots, the awake / sleeping sets, and generation counters.
134
- * @type {BodyStorage}
135
- * @readonly
136
- */
137
- storage = new BodyStorage();
138
-
139
- /**
140
- * Broadphase tree holding every static collider leaf. The step never
141
- * refits it statics do not move.
142
- * @type {BVH}
143
- */
144
- staticBvh = new BVH();
145
-
146
- /**
147
- * Broadphase tree holding every dynamic and kinematic collider leaf.
148
- * Leaves carry a velocity-fattened AABB and are refit each step.
149
- * @type {BVH}
150
- */
151
- dynamicBvh = new BVH();
152
-
153
- /**
154
- * Persistent contact-manifold cache. One slot per active pair.
155
- * @type {ManifoldStore}
156
- * @readonly
157
- */
158
- manifolds = new ManifoldStore();
159
-
160
- /**
161
- * Per-frame list of broadphase-overlapping pairs, each in
162
- * entity-canonical role order (A = the smaller entity id). Cleared at
163
- * the top of each step.
164
- * @type {PairList}
165
- */
166
- pairs = new PairList();
167
-
168
- /**
169
- * Per-frame contact-event buffer. Populated by the manifold diff
170
- * pass at end-of-step and consumed by the dispatch pass.
171
- * @type {ContactEventBuffer}
172
- */
173
- contactEvents = new ContactEventBuffer();
174
-
175
- /**
176
- * Per-frame island partitioning of the awake-body + contact graph,
177
- * consumed by the solver and the sleep test. Static and kinematic
178
- * bodies act as constraint anchors and do not enlarge islands.
179
- * @type {IslandBuilder}
180
- */
181
- islands = new IslandBuilder();
182
-
183
- /**
184
- * Data-oriented mirror of the per-body solver hot state (velocity,
185
- * inverse mass / inertia, orientation), packed into one ArrayBuffer
186
- * and indexed by body slot. Gathered from the `RigidBody` /
187
- * `Transform` components once per step (after islands are built),
188
- * mutated in place by the TGS substep loop, and the persistent
189
- * velocity scattered back at the end. Keeps the solver's hottest inner
190
- * loop free of component-object dereferences.
191
- * @type {SolverBodyState}
192
- */
193
- __solver_state = new SolverBodyState();
194
-
195
- /**
196
- * Velocity-squared threshold below which a body is eligible to start
197
- * accumulating sleep time. Combined linear + angular kinetic-ish
198
- * metric: `vx²+vy²+vz² + ωx²+ωy²+ωz²`. Default 0.01 corresponds to
199
- * ~0.1 m/s linear or ~0.1 rad/s angular.
200
- * @type {number}
201
- */
202
- sleepVelocitySqrThreshold = 0.01;
203
-
204
- /**
205
- * Seconds of below-threshold motion before a body is moved to the
206
- * sleeping set. Box2D default is 0.5 s.
207
- * @type {number}
208
- */
209
- sleepTimeThreshold = 0.5;
210
-
211
- /**
212
- * Number of TGS substeps per `fixedUpdate`. Each substep re-runs the
213
- * velocity + position solve at `dt / substeps`. Higher counts buy stack
214
- * stability, high-mass-ratio robustness and smoother trajectories, at
215
- * near-linear solver cost. `1` gives a plain single-step solve.
216
- * @type {number}
217
- */
218
- substeps = 4;
219
-
220
- /**
221
- * Velocity iterations per substep. Lower than a single-step solver
222
- * would need, because the substep loop revisits the contact set
223
- * `substeps` times.
224
- * @type {number}
225
- */
226
- velocityIterations = 4;
227
-
228
- /**
229
- * Position (split-impulse) iterations per substep.
230
- * @type {number}
231
- */
232
- positionIterations = 1;
233
-
234
- /**
235
- * Contact softness frequency, Hz. Softening is what lets resting stacks
236
- * settle instead of ringing just above the sleep threshold; `0` gives
237
- * hard contacts. Clamped per step to an eighth of the substep rate.
238
- * Pairs with a non-dynamic side use this frequency stiffer, so
239
- * piles don't sag into the ground.
240
- * @type {number}
241
- */
242
- contactHertz = 30;
243
-
244
- /**
245
- * Contact damping ratio ζ for the soft scales — heavily over-damped
246
- * by design (Box2D v3 default), so contacts never oscillate.
247
- * @type {number}
248
- */
249
- contactDampingRatio = 10;
250
-
251
- /**
252
- * Reusable contact-event payload. Listeners must copy any fields they
253
- * intend to retain past their own scope. Reset before each dispatch.
254
- * @private
255
- */
256
- __contact_payload = {
257
- entityA: -1,
258
- entityB: -1,
259
- kind: 0,
260
- depth: 0,
261
- normal: new Vector3(),
262
- point: new Vector3(),
263
- };
264
-
265
- /**
266
- * World gravity, m/s². Applied each step scaled by per-body gravityScale.
267
- * @readonly
268
- * @type {Vector3}
269
- */
270
- gravity = new Vector3(0, -9.81, 0);
271
-
272
- /**
273
- * Optional global contact filter. Called for each surviving broadphase
274
- * pair; returning `false` discards the pair.
275
- * @type {((entityA: number, entityB: number, colliderA: Collider, colliderB: Collider) => boolean) | null}
276
- * @private
277
- */
278
- __contact_filter = null;
279
-
280
- /**
281
- * Per-body component side-tables indexed by body index (NOT packed id).
282
- * Sparse arrays populated at link, cleared at unlink. The hot loop
283
- * iterates `storage.awake_at(i)` and dereferences these.
284
- * @type {RigidBody[]}
285
- */
286
- __bodies = [];
287
-
288
- /**
289
- * @type {Transform[]}
290
- * @readonly
291
- */
292
- __transforms = [];
293
-
294
- /**
295
- * Per-body list of attached colliders. Each entry stores the
296
- * Collider component, its world Transform, the entity that owns
297
- * it (the body entity for same-entity attachments, a child entity
298
- * for compound bodies), and the BVH leaf id assigned at attach
299
- * time. A body may have zero or more attached colliders.
300
- *
301
- * @type {Array<Array<{collider: Collider, transform: Transform, entity: number, bvhNode: number}>>}
302
- */
303
- __body_collider_lists = [];
304
-
305
- /**
306
- * Live {@link Joint} (6-DOF constraint) instances, in a sparse array
307
- * indexed by joint id. Solved alongside contacts inside the TGS
308
- * substep loop. Holes (unlinked joints) are `undefined`.
309
- * @type {Joint[]}
310
- */
311
- __joints = [];
312
-
313
- /**
314
- * Freed {@link __joints} slot ids, reused LIFO by {@link link_joint} so
315
- * ids stay dense. Solve order does not follow it — {@link __joints_sorted}
316
- * owns that.
317
- * @private
318
- * @type {number[]}
319
- */
320
- __joint_free = [];
321
-
322
- /**
323
- * Live joints in CANONICAL SOLVE ORDER: ascending `(entityA,
324
- * entityB)`, equal keys in link order. The id-indexed
325
- * {@link __joints} registry reflects link/unlink HISTORY (slot
326
- * reuse), which two engines holding identical joints need not
327
- * share the joint solver's Gauss-Seidel sweep must consume
328
- * joints in an order derivable from state alone or the engines
329
- * diverge. Maintained by {@link link_joint} / {@link unlink_joint};
330
- * consumed by the substep loop. Multiple joints between the SAME
331
- * entity pair keep their relative link order reconstruction
332
- * replays links in the same order, so that too is state-derived.
333
- * @private
334
- * @type {Joint[]}
335
- */
336
- __joints_sorted = [];
337
-
338
- /**
339
- * Velocity iterations per substep for the joint solver. Joints (and
340
- * especially joint chains) want a few more iterations than contacts to
341
- * propagate impulses along the chain; cheap because joints are far
342
- * fewer than contacts.
343
- * @type {number}
344
- */
345
- jointIterations = 8;
346
-
347
- /**
348
- * Per-body pseudo-velocity for the Catto split-impulse position
349
- * pass (TGS Phase 1). Flat layout, 6 doubles per body slot index:
350
- * `[lin.x, lin.y, lin.z, ang.x, ang.y, ang.z]`. Sized to
351
- * `storage.high_water_mark * 6` at the top of each fixedUpdate
352
- * and zeroed in place so unwritten slots contribute nothing to
353
- * `integrate_position`.
354
- *
355
- * The solver writes during its position pass; `integrate_position`
356
- * reads and folds into the pose update on the same tick, then
357
- * the next tick's zero-pass wipes the state. It NEVER lands in
358
- * `linearVelocity` / `angularVelocity` — that's the point of
359
- * split impulse: depth correction does not contaminate persistent
360
- * velocity (so a `restitution = 0` impact stops cleanly).
361
- *
362
- * @type {Float64Array}
363
- */
364
- __pseudo_velocity = new Float64Array(0);
365
-
366
- /**
367
- * Master switch for the continuous-collision pass. When false the
368
- * {@link RigidBodyFlags.CCD} flag is ignored and no swept queries run.
369
- * @type {boolean}
370
- */
371
- ccdEnabled = true;
372
-
373
- /**
374
- * Start-of-step world positions for CCD-flagged bodies — 3 doubles per
375
- * body slot index (`[x, y, z]`). Captured in Stage 1 before the substep
376
- * loop integrates poses; the CCD pass ({@link ccd_resolve}) sweeps from
377
- * here to the final pose. Grows to `storage.high_water_mark * 3`; only
378
- * CCD-flagged slots are written each step.
379
- * @type {Float64Array}
380
- */
381
- __ccd_start_pos = new Float64Array(0);
382
-
383
- /**
384
- * Set this to the {@link InterpolationSystem}'s log to get render
385
- * interpolation: each step then restores every awake {@link Interpolated}
386
- * body's authoritative pose before simulating, and records the result
387
- * afterwards. Leave `null` on a headless world the work is skipped
388
- * entirely.
389
- * @type {InterpolationLog|null}
390
- */
391
- interpolationLog = null;
392
-
393
- /** @private @type {number} valid entries in {@link __interp_recorded} */
394
- __interp_recorded_count = 0;
395
-
396
- constructor() {
397
- super();
398
-
399
- /**
400
- * Reusable decode buffer for restoring interpolated snapshots.
401
- * @private
402
- * @type {BinaryBuffer}
403
- */
404
- this.__interp_scratch = new BinaryBuffer();
405
- this.__interp_scratch.fromArrayBuffer(new ArrayBuffer(256));
406
-
407
- /**
408
- * Body indices snapshotted by the LAST {@link __interp_record} — i.e. the awake
409
- * Interpolated set as it was at the end of the previous tick. {@link __interp_restore}
410
- * iterates THIS set (not the current awake set) so a body that slept on the previous
411
- * tick recorded before it slept, but absent from the current awake set is still
412
- * restored off its render-polluted pose. Sparse: O(interpolated moving bodies).
413
- * @private
414
- * @type {Uint32Array}
415
- */
416
- this.__interp_recorded = new Uint32Array(16);
417
- }
418
-
419
- /**
420
- * Symmetric layer/mask check + optional user callback. Called per
421
- * candidate pair during broadphase. Returns `true` to accept the pair.
422
- *
423
- * Layer/mask rule: pair (A, B) collides iff
424
- * `(A.layer & B.mask) !== 0 && (B.layer & A.mask) !== 0`.
425
- * The user's contact-filter callback is consulted only for pairs that
426
- * pass the bitmask test (cheap gate first).
427
- *
428
- * @private
429
- * @param {number} idA packed body id
430
- * @param {number} idB packed body id
431
- * @returns {boolean}
432
- */
433
- __pair_filter(idA, idB) {
434
- const idxA = body_id_index(idA);
435
- const idxB = body_id_index(idB);
436
-
437
- const rbA = this.__bodies[idxA];
438
- const rbB = this.__bodies[idxB];
439
-
440
- if (rbA === undefined || rbB === undefined) {
441
- return false;
442
- }
443
-
444
- // Layer/mask gate (symmetric).
445
- if (((rbA.layer & rbB.mask) | 0) === 0) {
446
- return false;
447
- }
448
- if (((rbB.layer & rbA.mask) | 0) === 0) {
449
- return false;
450
- }
451
-
452
- // User callback gate, if installed.
453
- const fn = this.__contact_filter;
454
- if (fn !== null) {
455
- const entA = this.storage.entity_at(idxA);
456
- const entB = this.storage.entity_at(idxB);
457
- const colA = this.__primary_collider(idxA);
458
- const colB = this.__primary_collider(idxB);
459
- if (!fn(entA, entB, colA, colB)) return false;
460
- }
461
- return true;
462
- }
463
-
464
- /**
465
- * First attached collider of a body, or `null` if none. Used for
466
- * body-level checks that aren't yet per-collider: the sensor flag, the
467
- * concave-shape dispatch flag, and the contact-filter callback's
468
- * `colliderA` / `colliderB` arguments. Contact **materials** (friction /
469
- * restitution) are NOT read here the narrowphase tracks the specific
470
- * source collider on each side of every contact and stamps the combined
471
- * coefficients into the manifold per contact, so mixed-material compound
472
- * bodies are accurate. (Per-contact source colliders for the filter
473
- * callback / sensor flag remain a follow-up.)
474
- *
475
- * @private
476
- * @param {number} body_idx
477
- * @returns {Collider|null}
478
- */
479
- __primary_collider(body_idx) {
480
- const list = this.__body_collider_lists[body_idx];
481
- return (list !== undefined && list.length > 0) ? list[0].collider : null;
482
- }
483
-
484
- /**
485
- * Resize {@link __pseudo_velocity} to cover every live body slot and
486
- * zero its contents. Called at the top of each fixedUpdate so the
487
- * split-impulse position pass starts from a clean state bodies the
488
- * solver doesn't touch contribute zero pseudo-velocity to
489
- * {@link integrate_position}.
490
- *
491
- * Doubles on growth like the other physics scratches; never shrinks.
492
- *
493
- * @private
494
- */
495
- __reset_pseudo_velocity() {
496
- const required = this.storage.high_water_mark * 6;
497
- if (this.__pseudo_velocity.length < required) {
498
- this.__pseudo_velocity = new Float64Array(required * 2);
499
- } else if (required > 0) {
500
- this.__pseudo_velocity.fill(0, 0, required);
501
- }
502
- }
503
-
504
- /**
505
- * Replace the world gravity vector. Effective on the next step.
506
- * @param {Vector3|{x:number,y:number,z:number}} v
507
- */
508
- setGravity(v) {
509
- this.gravity.set(v.x, v.y, v.z);
510
- }
511
-
512
- /**
513
- * Install (or remove with `null`) the contact filter callback.
514
- * @param {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null} fn
515
- */
516
- setContactFilter(fn) {
517
- this.__contact_filter = fn;
518
- }
519
-
520
- /**
521
- * @returns {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null}
522
- */
523
- getContactFilter() {
524
- return this.__contact_filter;
525
- }
526
-
527
- /**
528
- * @private
529
- * @param {RigidBody} rb
530
- * @param {Collider} collider
531
- * @param {Transform} transform
532
- * @returns {number} BVH node id
533
- */
534
- __insert_into_broadphase(rb, collider, transform) {
535
- const shape = collider.shape;
536
-
537
- assert.notNull(shape, 'Collider.shape must be set before attaching');
538
-
539
- shape.compute_bounding_box(scratch_local_aabb);
540
-
541
- const p = transform.position;
542
- const q = transform.rotation;
543
-
544
- aabb3_transform_oriented(
545
- scratch_world_aabb, 0,
546
- scratch_local_aabb[0], scratch_local_aabb[1], scratch_local_aabb[2],
547
- scratch_local_aabb[3], scratch_local_aabb[4], scratch_local_aabb[5],
548
- p.x, p.y, p.z,
549
- q.x, q.y, q.z, q.w
550
- );
551
-
552
- const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
553
- const node = bvh.allocate_node();
554
-
555
- bvh.node_set_aabb_primitive(
556
- node,
557
- scratch_world_aabb[0], scratch_world_aabb[1], scratch_world_aabb[2],
558
- scratch_world_aabb[3], scratch_world_aabb[4], scratch_world_aabb[5]
559
- );
560
- bvh.node_set_user_data(node, rb._bodyId);
561
- bvh.insert_leaf(node);
562
-
563
- return node;
564
- }
565
-
566
- /**
567
- * @private
568
- * @param {RigidBody} rb
569
- * @param {number} node
570
- */
571
- __remove_from_broadphase(rb, node) {
572
- const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
573
- bvh.remove_leaf(node);
574
- bvh.release_node(node);
575
- }
576
-
577
- /**
578
- * Wake every sleeping dynamic body whose broadphase leaf overlaps the
579
- * given leaf's AABB. Called BEFORE removing a NON-Dynamic body's leaf
580
- * (unlink / collider detach): statics and kinematics join no sleep group,
581
- * their sleepers' manifolds are dormant-frozen, and the broadphase never
582
- * revisits sleeping bodies — so without this, a pile resting on the
583
- * removed support would sleep in mid-air forever.
584
- *
585
- * Dynamic removals don't need it: a sleeping pile and its dynamic support
586
- * slept as one island and share a sleep group, which unlink already
587
- * dissolves by waking the group.
588
- *
589
- * @private
590
- * @param {BVH} bvh tree currently holding `node`
591
- * @param {number} node leaf about to be removed
592
- */
593
- __wake_sleepers_overlapping_leaf(bvh, node) {
594
- bvh.node_get_aabb(node, scratch_world_aabb);
595
-
596
- let n;
597
- for (;;) {
598
- n = bvh_query_user_data_intersects_aabb(scratch_wake_candidates, 0, this.dynamicBvh, scratch_world_aabb);
599
- if (n <= scratch_wake_candidates.length) break;
600
- // TRUE count returned, overflow writes dropped — grow and re-query.
601
- scratch_wake_candidates = new Uint32Array(Math.max(n, scratch_wake_candidates.length * 2));
602
- }
603
-
604
- for (let i = 0; i < n; i++) {
605
- const body_id = scratch_wake_candidates[i];
606
- if (!this.storage.is_valid(body_id)) continue;
607
- const rb = this.__bodies[body_id_index(body_id)];
608
- if (rb === undefined) continue;
609
- if (rb.sleepState !== SleepState.Sleeping) continue;
610
- this.__wake_body(rb);
611
- }
612
- }
613
-
614
- /**
615
- * Lifecycle entry: invoked when an entity gains the (RigidBody, Transform)
616
- * tuple. Allocates the body's slot in storage and seeds an empty collider
617
- * list. Colliders are attached separately via {@link attach_collider} (the
618
- * paired {@link ColliderObserverSystem} drives this from the dataset; in
619
- * tests, call manually).
620
- *
621
- * @param {RigidBody} rigidBody
622
- * @param {Transform} transform
623
- * @param {number} entity
624
- */
625
- link(rigidBody, transform, entity) {
626
- const packed = this.storage.allocate(entity);
627
- rigidBody._bodyId = packed;
628
-
629
- const index = body_id_index(packed);
630
- this.storage.set_kind(index, rigidBody.kind);
631
- this.storage.set_flags(index, rigidBody.flags);
632
-
633
- this.__bodies[index] = rigidBody;
634
- this.__transforms[index] = transform;
635
- this.__body_collider_lists[index] = [];
636
-
637
- // The component's sleep fields are AUTHORITATIVE persistent state
638
- // (see RigidBody.sleepState): a body restored from a snapshot — or
639
- // re-linked after an unlink — resumes exactly as drowsy or asleep
640
- // as it was, instead of being forced awake to re-settle. That keeps
641
- // a reconstructed world on the original's wake/sleep schedule (the
642
- // reconstruction-determinism contract). A freshly-constructed
643
- // component defaults to Awake, so plain spawns are unchanged.
644
- //
645
- // A restored sleeper arrives as a sleep-group SINGLETON (the group
646
- // links are world-instance-tied body indices, reset here); group
647
- // membership is re-established by the contact-state import or, for
648
- // a body slept in isolation, is already correct.
649
- rigidBody.sleep_group_next = -1;
650
- rigidBody.sleep_group_prev = -1;
651
-
652
- // Static bodies do not need to live in the active list — they never move.
653
- if (rigidBody.kind === BodyKind.Static
654
- || (rigidBody.kind === BodyKind.Dynamic && rigidBody.sleepState === SleepState.Sleeping)) {
655
- this.storage.mark_sleeping(index);
656
- }
657
- }
658
-
659
- /**
660
- * Detach every collider attached to this body, free its slot, and clear
661
- * the side-tables.
662
- *
663
- * @param {RigidBody} rigidBody
664
- * @param {Transform} transform
665
- * @param {number} entity
666
- */
667
- unlink(rigidBody, transform, entity) {
668
- const packed = rigidBody._bodyId;
669
-
670
- assert.equal(this.storage.is_valid(packed), true, 'unlink: stale or absent body id');
671
-
672
- // If the body is sleeping inside a multi-member sleep group, dissolve
673
- // the group by waking every member first. Otherwise the surviving
674
- // members would hold dangling indices in their sleep-group chain
675
- // pointing at a slot that has just been freed.
676
- if (rigidBody.sleep_group_next !== -1) {
677
- this.__wake_body(rigidBody);
678
- }
679
-
680
- const index = body_id_index(packed);
681
- const list = this.__body_collider_lists[index];
682
- if (list !== undefined) {
683
- // A removed static / kinematic body may have been the support
684
- // under sleeping bodies — wake them before the leaves disappear
685
- // (their AABBs are the query keys).
686
- const wake_neighbours = rigidBody.kind !== BodyKind.Dynamic;
687
- const bvh = rigidBody.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
688
- for (let i = 0; i < list.length; i++) {
689
- const entry = list[i];
690
- if (entry.bvhNode !== COLLIDER_UNBOUND) {
691
- if (wake_neighbours) {
692
- this.__wake_sleepers_overlapping_leaf(bvh, entry.bvhNode);
693
- }
694
- this.__remove_from_broadphase(rigidBody, entry.bvhNode);
695
- }
696
- entry.collider._bvhNode = COLLIDER_UNBOUND;
697
- entry.collider._bodyId = -1;
698
- }
699
- }
700
-
701
- this.__bodies[index] = undefined;
702
- this.__transforms[index] = undefined;
703
- this.__body_collider_lists[index] = undefined;
704
-
705
- // Purge joints referencing the freed body. The solver already skips
706
- // them via the generation-checked is_valid, but the 8-bit generation
707
- // wraps after 256 reuse cycles of one slot (~seconds under a pooled
708
- // spawner) — at which point a stale joint handle ALIASES an unrelated
709
- // new body and silently reactivates against it. Removing the joint at
710
- // the source closes the ABA window outright.
711
- const joints = this.__joints;
712
- for (let i = 0; i < joints.length; i++) {
713
- const joint = joints[i];
714
- if (joint === undefined || joint === null) continue;
715
- if (joint._bodyIdA === packed || joint._bodyIdB === packed) {
716
- this.unlink_joint(joint);
717
- }
718
- }
719
-
720
- this.storage.free(packed);
721
- rigidBody._bodyId = RIGID_BODY_UNALLOCATED;
722
- }
723
-
724
- /**
725
- * Attach a collider to an existing body. The collider can live on the
726
- * same entity as the body (single-collider body) or on a child entity
727
- * (compound body). The world transform passed here is the collider's
728
- * own — for a same-entity collider it is the body's Transform; for a
729
- * child collider it is the child entity's Transform.
730
- *
731
- * Idempotent — re-attaching the same collider is a no-op.
732
- *
733
- * @param {number} body_entity entity that owns the body
734
- * @param {Collider} collider
735
- * @param {Transform} transform world transform of the collider
736
- * @param {number} [collider_entity] entity owning the collider (defaults to body_entity)
737
- */
738
- attach_collider(body_entity, collider, transform, collider_entity = body_entity) {
739
- // Find the body by walking the storage entity table. The body must
740
- // have been allocated via `link` before any colliders are attached.
741
- const body_index = this.__find_body_index_by_entity(body_entity);
742
- assert.notEqual(body_index, -1, `attach_collider: no body found for entity ${body_entity}`);
743
-
744
- const rb = this.__bodies[body_index];
745
- // Idempotent: skip if collider already attached.
746
- if (collider._bvhNode !== COLLIDER_UNBOUND) return;
747
-
748
- const node = this.__insert_into_broadphase(rb, collider, transform);
749
-
750
- collider._bvhNode = node;
751
- collider._bodyId = rb._bodyId;
752
- this.__body_collider_lists[body_index].push({
753
- collider, transform, entity: collider_entity, bvhNode: node,
754
- });
755
- }
756
-
757
- /**
758
- * Reverse of {@link attach_collider}. Idempotent.
759
- *
760
- * @param {number} body_entity
761
- * @param {Collider} collider
762
- */
763
- detach_collider(body_entity, collider) {
764
- if (collider._bvhNode === COLLIDER_UNBOUND) return;
765
-
766
- const body_index = this.__find_body_index_by_entity(body_entity);
767
- if (body_index === -1) return;
768
-
769
- const rb = this.__bodies[body_index];
770
- // Same rule as unlink: removing a static / kinematic leaf may pull
771
- // the support from under sleeping bodies — wake them first.
772
- if (rb.kind !== BodyKind.Dynamic) {
773
- const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
774
- this.__wake_sleepers_overlapping_leaf(bvh, collider._bvhNode);
775
- } else if (rb.sleepState === SleepState.Sleeping) {
776
- // A sleeping DYNAMIC support: unlike unlink, detach_collider dissolves no sleep group,
777
- // so removing the collider would strand the body (and, via the sleep-group chain that
778
- // __wake_body walks, the whole pile resting on it) asleep and frozen in mid-air. Keyed
779
- // on sleep state, not sleep_group_next, so a LONE sleeper (group === -1) wakes too.
780
- this.__wake_body(rb);
781
- }
782
- this.__remove_from_broadphase(rb, collider._bvhNode);
783
-
784
- const list = this.__body_collider_lists[body_index];
785
- for (let i = 0; i < list.length; i++) {
786
- if (list[i].collider === collider) {
787
- list.splice(i, 1);
788
- break;
789
- }
790
- }
791
-
792
- collider._bvhNode = COLLIDER_UNBOUND;
793
- collider._bodyId = -1;
794
- }
795
-
796
- /**
797
- * Resolve an entity to its body index, or -1 if no live body owns it.
798
- * O(1) via {@link BodyStorage#index_of_entity}'s entity → index map — used
799
- * on the collider attach / detach and joint link paths.
800
- *
801
- * @private
802
- * @param {number} entity
803
- * @returns {number} body index or -1
804
- */
805
- __find_body_index_by_entity(entity) {
806
- return this.storage.index_of_entity(entity);
807
- }
808
-
809
- /**
810
- * Register a {@link Joint} (6-DOF constraint). Resolves the joint's
811
- * entities to packed body ids and adds it to the active set, where it is
812
- * solved alongside contacts in the TGS substep loop. Body A must be a
813
- * linked body; body B is either a linked body or {@link JOINT_WORLD}
814
- * (anchoring A to a fixed world point — `localAnchorB` is then a world
815
- * coordinate).
816
- *
817
- * @param {Joint} joint
818
- */
819
- link_joint(joint) {
820
- const idxA = this.__find_body_index_by_entity(joint.entityA);
821
- assert.notEqual(idxA, -1, `link_joint: no body for entityA ${joint.entityA}`);
822
- joint._bodyIdA = this.__bodies[idxA]._bodyId;
823
-
824
- if (joint.entityB === JOINT_WORLD) {
825
- joint._bodyIdB = JOINT_WORLD;
826
- } else {
827
- const idxB = this.__find_body_index_by_entity(joint.entityB);
828
- assert.notEqual(idxB, -1, `link_joint: no body for entityB ${joint.entityB}`);
829
- joint._bodyIdB = this.__bodies[idxB]._bodyId;
830
- }
831
-
832
- // A (re-)linked joint starts cold: the accumulated warm-start impulse
833
- // belongs to whatever configuration the joint solved BEFORE it was
834
- // unlinked — replaying it against the new bodies is a phantom kick.
835
- joint.dofImpulse.fill(0);
836
-
837
- // Reuse a freed slot if available so joint ids stay dense-ish.
838
- let id;
839
- if (this.__joint_free.length > 0) {
840
- id = this.__joint_free.pop();
841
- } else {
842
- id = this.__joints.length;
843
- }
844
- joint._jointId = id;
845
- this.__joints[id] = joint;
846
-
847
- // Insert at the upper bound of the joint's (entityA, entityB) key in
848
- // the canonical solve-order list — equal keys keep link order.
849
- const sorted = this.__joints_sorted;
850
- let pos = sorted.length;
851
- while (pos > 0) {
852
- const prev = sorted[pos - 1];
853
- if (prev.entityA < joint.entityA
854
- || (prev.entityA === joint.entityA && prev.entityB <= joint.entityB)) {
855
- break;
856
- }
857
- pos--;
858
- }
859
- sorted.splice(pos, 0, joint);
860
- }
861
-
862
- /**
863
- * Remove a previously {@link link_joint}'d joint from the active set.
864
- * Idempotent.
865
- * @param {Joint} joint
866
- */
867
- unlink_joint(joint) {
868
- const id = joint._jointId;
869
- if (id === JOINT_UNALLOCATED || this.__joints[id] !== joint) return;
870
- this.__joints[id] = undefined;
871
- this.__joint_free.push(id);
872
- joint._jointId = JOINT_UNALLOCATED;
873
-
874
- const sorted = this.__joints_sorted;
875
- const pos = sorted.indexOf(joint);
876
- if (pos !== -1) sorted.splice(pos, 1);
877
- }
878
-
879
- /**
880
- * Resolve a packed body id to its entity, or `-1` if the id is stale.
881
- * @param {number} packed_body_id
882
- * @returns {number}
883
- */
884
- entityOf(packed_body_id) {
885
- if (!this.storage.is_valid(packed_body_id)) return -1;
886
- return this.storage.entity_at(body_id_index(packed_body_id));
887
- }
888
-
889
- /**
890
- * Number of live bodies (regardless of awake/sleeping state).
891
- * @returns {number}
892
- */
893
- get bodyCount() {
894
- return this.storage.size;
895
- }
896
-
897
- /**
898
- * Apply an instantaneous change of momentum at the body's centre of mass.
899
- * Linear-only — see {@link applyImpulseAt} for an off-centre impulse that
900
- * also produces angular response.
901
- *
902
- * Wakes the body if it is asleep.
903
- *
904
- * @param {RigidBody} rigidBody
905
- * @param {Vector3|{x:number,y:number,z:number}} impulse
906
- */
907
- applyImpulse(rigidBody, impulse) {
908
- if (rigidBody.kind !== BodyKind.Dynamic) {
909
- return;
910
- }
911
- const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
912
- rigidBody.linearVelocity.addScaled(impulse, inv_m);
913
-
914
- this.__wake_body(rigidBody);
915
- }
916
-
917
- /**
918
- * Apply an instantaneous change of momentum at a specific world-space point.
919
- * Off-centre impulses produce both linear (Δv = P/m) and angular
920
- * (Δω = I_w⁻¹·(r × P)) response.
921
- *
922
- * Wakes the body if it is asleep.
923
- *
924
- * @param {RigidBody} rigidBody
925
- * @param {Transform} transform body's current world Transform (used for r and I_w)
926
- * @param {Vector3|{x:number,y:number,z:number}} impulse
927
- * @param {Vector3|{x:number,y:number,z:number}} worldPoint
928
- */
929
- applyImpulseAt(rigidBody, transform, impulse, worldPoint) {
930
- if (rigidBody.kind !== BodyKind.Dynamic) {
931
- return;
932
- }
933
- const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
934
-
935
- rigidBody.linearVelocity.set(
936
- rigidBody.linearVelocity.x + impulse.x * inv_m,
937
- rigidBody.linearVelocity.y + impulse.y * inv_m,
938
- rigidBody.linearVelocity.z + impulse.z * inv_m
939
- );
940
-
941
- const rx = worldPoint.x - transform.position.x;
942
- const ry = worldPoint.y - transform.position.y;
943
- const rz = worldPoint.z - transform.position.z;
944
-
945
- // Δω = I_w⁻¹ · (r × P)
946
- const tx = ry * impulse.z - rz * impulse.y;
947
- const ty = rz * impulse.x - rx * impulse.z;
948
- const tz = rx * impulse.y - ry * impulse.x;
949
-
950
- // LockRot* masks the BODY-LOCAL inverse-inertia diagonal here exactly
951
- // as the solver gather does, so a user impulse cannot spin a locked
952
- // DOF either.
953
- const flags = rigidBody.flags;
954
- const ii = rigidBody.inverseInertiaLocal;
955
- scratch_locked_inertia.x = (flags & RigidBodyFlags.LockRotX) !== 0 ? 0 : ii.x;
956
- scratch_locked_inertia.y = (flags & RigidBodyFlags.LockRotY) !== 0 ? 0 : ii.y;
957
- scratch_locked_inertia.z = (flags & RigidBodyFlags.LockRotZ) !== 0 ? 0 : ii.z;
958
- world_inverse_inertia_apply(scratch_angular_delta, 0, scratch_locked_inertia, transform.rotation, tx, ty, tz);
959
-
960
- rigidBody.angularVelocity.set(
961
- rigidBody.angularVelocity.x + scratch_angular_delta[0],
962
- rigidBody.angularVelocity.y + scratch_angular_delta[1],
963
- rigidBody.angularVelocity.z + scratch_angular_delta[2]
964
- );
965
-
966
- this.__wake_body(rigidBody);
967
- }
968
-
969
- /**
970
- * Accumulate a continuous torque (world-space) for integration on the
971
- * next fixedUpdate. Pairs with {@link applyForce} for the rotational case.
972
- *
973
- * Wakes the body if asleep.
974
- *
975
- * @param {RigidBody} rigidBody
976
- * @param {Vector3|{x:number,y:number,z:number}} torque
977
- */
978
- applyTorque(rigidBody, torque) {
979
- if (rigidBody.kind !== BodyKind.Dynamic) {
980
- return;
981
- }
982
- rigidBody.accumulatedTorque.set(
983
- rigidBody.accumulatedTorque.x + torque.x,
984
- rigidBody.accumulatedTorque.y + torque.y,
985
- rigidBody.accumulatedTorque.z + torque.z
986
- );
987
- this.__wake_body(rigidBody);
988
- }
989
-
990
- /**
991
- * Apply a continuous force at a specific world-space point. The force
992
- * generates both a linear acceleration (F/m) and a torque (r × F) about
993
- * the body's centre of mass.
994
- *
995
- * Wakes the body if asleep.
996
- *
997
- * @param {RigidBody} rigidBody
998
- * @param {Transform} transform body's current world Transform
999
- * @param {Vector3|{x:number,y:number,z:number}} force
1000
- * @param {Vector3|{x:number,y:number,z:number}} worldPoint
1001
- */
1002
- applyForceAt(rigidBody, transform, force, worldPoint) {
1003
- if (rigidBody.kind !== BodyKind.Dynamic) {
1004
- return;
1005
- }
1006
- rigidBody.accumulatedForce.set(
1007
- rigidBody.accumulatedForce.x + force.x,
1008
- rigidBody.accumulatedForce.y + force.y,
1009
- rigidBody.accumulatedForce.z + force.z
1010
- );
1011
-
1012
- const rx = worldPoint.x - transform.position.x;
1013
- const ry = worldPoint.y - transform.position.y;
1014
- const rz = worldPoint.z - transform.position.z;
1015
-
1016
- rigidBody.accumulatedTorque.set(
1017
- rigidBody.accumulatedTorque.x + (ry * force.z - rz * force.y),
1018
- rigidBody.accumulatedTorque.y + (rz * force.x - rx * force.z),
1019
- rigidBody.accumulatedTorque.z + (rx * force.y - ry * force.x)
1020
- );
1021
-
1022
- this.__wake_body(rigidBody);
1023
- }
1024
-
1025
- /**
1026
- * Accumulate a continuous force to be integrated next fixedUpdate step.
1027
- * Wakes the body if asleep.
1028
- *
1029
- * @param {RigidBody} rigidBody
1030
- * @param {Vector3|{x:number,y:number,z:number}} force
1031
- */
1032
- applyForce(rigidBody, force) {
1033
- if (rigidBody.kind !== BodyKind.Dynamic) {
1034
- return;
1035
- }
1036
- rigidBody.accumulatedForce.add(force);
1037
- this.__wake_body(rigidBody);
1038
- }
1039
-
1040
- /**
1041
- * Replace the linear velocity. Wakes the body if asleep.
1042
- *
1043
- * @param {RigidBody} rigidBody
1044
- * @param {Vector3|{x:number,y:number,z:number}} v
1045
- */
1046
- setLinearVelocity(rigidBody, v) {
1047
- rigidBody.linearVelocity.copy(v);
1048
- if (rigidBody.kind === BodyKind.Dynamic) {
1049
- this.__wake_body(rigidBody);
1050
- }
1051
- }
1052
-
1053
- /**
1054
- * Replace the angular velocity. Wakes the body if asleep.
1055
- *
1056
- * Writing `rigidBody.angularVelocity` directly does not wake a sleeping
1057
- * body, so the spin is silently discarded until something else wakes it.
1058
- *
1059
- * @param {RigidBody} rigidBody
1060
- * @param {Vector3|{x:number,y:number,z:number}} v
1061
- */
1062
- setAngularVelocity(rigidBody, v) {
1063
- rigidBody.angularVelocity.copy(v);
1064
- if (rigidBody.kind === BodyKind.Dynamic) {
1065
- this.__wake_body(rigidBody);
1066
- }
1067
- }
1068
-
1069
- /**
1070
- * Teleport a body to a new pose, bypassing integration: writes the body's
1071
- * Transform directly and wakes it. For an interpolated body this also flags a
1072
- * render `snap` on its {@link Interpolated} component, so the producer keeps
1073
- * this pose (rather than restoring the previous tick over it) and the
1074
- * renderer shows the new pose without sliding across the jump.
1075
- *
1076
- * This is the authoritative way to reposition an interpolated body — a raw
1077
- * `Transform` write would be undone by the per-step restore. Velocity is left
1078
- * as-is; zero it via {@link setLinearVelocity} if the teleport should also
1079
- * stop the body.
1080
- *
1081
- * Works for STATIC bodies too: their broadphase leaves are re-homed to
1082
- * the new pose, and sleepers at both the old and the new location are
1083
- * woken a moved support drops what rested on it, and whatever it moved
1084
- * into must react.
1085
- *
1086
- * @param {RigidBody} rigidBody
1087
- * @param {Vector3|{x:number,y:number,z:number}} position world position
1088
- * @param {Quaternion|{x:number,y:number,z:number,w:number}} rotation world unit-quaternion rotation
1089
- */
1090
- setPose(rigidBody, position, rotation) {
1091
- const idx = body_id_index(rigidBody._bodyId);
1092
- const transform = this.__transforms[idx];
1093
- if (transform === undefined) {
1094
- return;
1095
- }
1096
-
1097
- transform.position.set(position.x, position.y, position.z);
1098
- transform.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w);
1099
-
1100
- if (rigidBody.kind !== BodyKind.Dynamic) {
1101
- // Static AND Kinematic: the body itself never sleeps and __wake_body is a no-op for it,
1102
- // so teleporting a support must instead wake what rested on it (old pose) and what it
1103
- // moved into (new pose) a zero-velocity teleport never trips __wake_pairs' velocity-
1104
- // based parked exemption. A moved static also keeps a stale broadphase AABB forever
1105
- // (stage 2 refits awake bodies only), so re-home its leaves; doing the same for a
1106
- // kinematic is harmless (stage 2 would refit it anyway). The leaves of a static live in
1107
- // staticBvh, a kinematic's in dynamicBvh pick the tree that actually holds them.
1108
- const bvh = rigidBody.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
1109
- const list = this.__body_collider_lists[idx];
1110
- if (list !== undefined) {
1111
- for (let i = 0; i < list.length; i++) {
1112
- const entry = list[i];
1113
- if (entry.bvhNode === COLLIDER_UNBOUND) continue;
1114
- this.__wake_sleepers_overlapping_leaf(bvh, entry.bvhNode);
1115
- this.__remove_from_broadphase(rigidBody, entry.bvhNode);
1116
- const node = this.__insert_into_broadphase(rigidBody, entry.collider, entry.transform);
1117
- entry.bvhNode = node;
1118
- entry.collider._bvhNode = node;
1119
- this.__wake_sleepers_overlapping_leaf(bvh, node);
1120
- }
1121
- }
1122
- } else {
1123
- this.__wake_body(rigidBody);
1124
- }
1125
-
1126
- // Flag a snap on the body's Interpolated component, if any, so the
1127
- // interpolation producer (restore) and consumer (blend) treat this pose
1128
- // as authoritative this frame instead of interpolating across the jump.
1129
- //
1130
- // KNOWN BUG (deferred): `snap` is a single flag with two independent consumers —
1131
- // __interp_restore (skips restore) and InterpolationSystem.update (skips blend AND
1132
- // CLEARS it). The renderer's update runs every render cycle while a fixed step only
1133
- // runs when the accumulator crosses the step size, so a render frame landing between
1134
- // this setPose and the next fixedUpdate clears `snap` before __interp_restore reads
1135
- // it restore then overwrites the teleported Transform with the pre-teleport tick's
1136
- // snapshot and the teleport is silently reverted in the SIMULATION. This only bites
1137
- // once InterpolationSystem is wired (the app does not wire it yet). The robust fix is
1138
- // to make the teleport authoritative independent of the shared flag either overwrite
1139
- // the log's latest-tick record for this body (needs new InterpolationLog API), or give
1140
- // the producer and consumer independent snap state each clears itself.
1141
- const em = this.entityManager;
1142
- if (em !== null && em !== undefined) {
1143
- const dataset = em.dataset;
1144
- if (dataset !== null && dataset !== undefined) {
1145
- const entity = this.storage.entity_at(idx);
1146
- const interpolated = dataset.getComponent(entity, Interpolated);
1147
- if (interpolated !== undefined && interpolated !== null) {
1148
- interpolated.snap = true;
1149
- }
1150
- }
1151
- }
1152
- }
1153
-
1154
- /**
1155
- * Force the body awake. Static bodies are ignored.
1156
- * @param {RigidBody} rigidBody
1157
- */
1158
- wake(rigidBody) {
1159
- this.__wake_body(rigidBody);
1160
- }
1161
-
1162
- /**
1163
- * Force the body asleep. Dynamic bodies will not re-enter the active list
1164
- * until a wake event occurs.
1165
- * @param {RigidBody} rigidBody
1166
- */
1167
- sleep(rigidBody) {
1168
- if (rigidBody.kind !== BodyKind.Dynamic) {
1169
- return;
1170
- }
1171
- if (rigidBody.sleepState === SleepState.Sleeping) {
1172
- return;
1173
- }
1174
- // An unlinked (or stale) body has no storage slot — indexing with its
1175
- // dead id would corrupt the awake-list bookkeeping of whatever body
1176
- // now owns that index.
1177
- if (!this.storage.is_valid(rigidBody._bodyId)) {
1178
- return;
1179
- }
1180
- rigidBody.sleepState = SleepState.Sleeping;
1181
- const index = body_id_index(rigidBody._bodyId);
1182
- this.storage.mark_sleeping(index);
1183
- }
1184
-
1185
- /**
1186
- * Wake a body and atomically wake every other body it was last sleeping
1187
- * with (its "sleep group"). Sleep groups are circular doubly-linked lists
1188
- * threaded through every member of an island when it sleeps atomically;
1189
- * waking any one member walks the chain and wakes the rest in the same
1190
- * call.
1191
- *
1192
- * Without this, a 100-block stack hit at the base would wake one block
1193
- * per frame as the broadphase propagated awareness up the stack — a
1194
- * visible ~1.6 s wave at 60 fps. Atomic wake eliminates the wave.
1195
- *
1196
- * No-op for non-dynamic bodies. Idempotent for already-awake bodies.
1197
- * @private
1198
- * @param {RigidBody} rb
1199
- */
1200
- __wake_body(rb) {
1201
- if (rb.kind !== BodyKind.Dynamic) return;
1202
- if (rb.sleepState === SleepState.Awake) return;
1203
- // Same stale-id guard as sleep(): an unlinked body's dead id must not
1204
- // index into (and corrupt) another body's awake-list slot.
1205
- if (!this.storage.is_valid(rb._bodyId)) return;
1206
- const index = body_id_index(rb._bodyId);
1207
-
1208
- // Remember the next-in-chain before clearing the body's own pointers;
1209
- // the rest of the group is reached by walking forward from there.
1210
- const start_next = rb.sleep_group_next;
1211
-
1212
- rb.sleepState = SleepState.Awake;
1213
- rb.sleep_timer = 0;
1214
- rb.sleep_group_next = -1;
1215
- rb.sleep_group_prev = -1;
1216
- this.storage.mark_awake(index);
1217
-
1218
- if (start_next === -1 || start_next === index) return;
1219
-
1220
- // Walk the (now-broken) chain forward until we loop back. The chain
1221
- // is circular so we know when to stop; defensive `-1` guards against
1222
- // corruption from a body being unlinked mid-sleep-group.
1223
- let cur = start_next;
1224
- while (cur !== -1 && cur !== index) {
1225
- const cur_rb = this.__bodies[cur];
1226
- if (cur_rb === undefined) break;
1227
- const nxt = cur_rb.sleep_group_next;
1228
- cur_rb.sleepState = SleepState.Awake;
1229
- cur_rb.sleep_timer = 0;
1230
- cur_rb.sleep_group_next = -1;
1231
- cur_rb.sleep_group_prev = -1;
1232
- this.storage.mark_awake(cur);
1233
- cur = nxt;
1234
- }
1235
- }
1236
-
1237
- /**
1238
- * Atomically put every body in a contiguous range of island members to
1239
- * sleep. Members are threaded into a circular doubly-linked list so any
1240
- * future `wake` on any member walks the chain and revives them all.
1241
- *
1242
- * Velocities are zeroed because the body is by definition at rest at
1243
- * this point — the alternative (storing residual velocities for "softer"
1244
- * wake) is what Bullet does, but for a deterministic game-physics target
1245
- * fully resetting is simpler and avoids drift while sleeping.
1246
- *
1247
- * @private
1248
- * @param {Uint32Array} member_array view (or full array) of body indices
1249
- * @param {number} start
1250
- * @param {number} end
1251
- */
1252
- __atomic_sleep_island_range(member_array, start, end) {
1253
- const count = end - start;
1254
- if (count === 0) return;
1255
- const bodies = this.__bodies;
1256
- const storage = this.storage;
1257
-
1258
- if (count === 1) {
1259
- const idx = member_array[start];
1260
- const rb = bodies[idx];
1261
- if (rb === undefined) return;
1262
- rb.sleep_group_next = -1;
1263
- rb.sleep_group_prev = -1;
1264
- rb.sleepState = SleepState.Sleeping;
1265
- rb.linearVelocity[0] = 0;
1266
- rb.linearVelocity[1] = 0;
1267
- rb.linearVelocity[2] = 0;
1268
- rb.angularVelocity[0] = 0;
1269
- rb.angularVelocity[1] = 0;
1270
- rb.angularVelocity[2] = 0;
1271
- storage.mark_sleeping(idx);
1272
- return;
1273
- }
1274
-
1275
- for (let i = 0; i < count; i++) {
1276
- const idx = member_array[start + i];
1277
- const rb = bodies[idx];
1278
- if (rb === undefined) continue;
1279
- const next_idx = member_array[start + ((i + 1) % count)];
1280
- const prev_idx = member_array[start + ((i - 1 + count) % count)];
1281
- rb.sleep_group_next = next_idx;
1282
- rb.sleep_group_prev = prev_idx;
1283
- rb.sleepState = SleepState.Sleeping;
1284
- rb.linearVelocity[0] = 0;
1285
- rb.linearVelocity[1] = 0;
1286
- rb.linearVelocity[2] = 0;
1287
- rb.angularVelocity[0] = 0;
1288
- rb.angularVelocity[1] = 0;
1289
- rb.angularVelocity[2] = 0;
1290
- storage.mark_sleeping(idx);
1291
- }
1292
- }
1293
-
1294
- /**
1295
- * Raycast against both broadphase trees. Fills `result` with the nearest
1296
- * hit and returns `true` on hit, `false` on miss.
1297
- *
1298
- * Resolves to the BVH leaf, not the shape: `result.t` is the distance to
1299
- * the leaf's inflated AABB and `result.normal` is that AABB's face
1300
- * normal. Exact for AABB-shaped colliders, approximate for the rest
1301
- * use {@link shapeCast} when you need the true surface.
1302
- *
1303
- * @param {Ray3} ray origin + unit direction + `tMax`
1304
- * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
1305
- * @param {(entity:number, collider:Collider)=>boolean} [filter] defaults
1306
- * to {@link returnTrue} (accept every candidate)
1307
- * @returns {boolean}
1308
- */
1309
- raycast(ray, result, filter = returnTrue) {
1310
- return raycast_query(this, ray, result, filter);
1311
- }
1312
-
1313
- /**
1314
- * Sweep a convex shape along a ray and return the first body it hits.
1315
- * The shape starts at `ray.origin` oriented by `rotation` and translates
1316
- * along `ray.direction` for up to `ray.tMax` units.
1317
- *
1318
- * Unlike {@link raycast} this resolves against the real shapes, so `t` is
1319
- * a true time of impact rather than a distance to a bounding box.
1320
- *
1321
- * @param {Ray3} ray origin + unit direction + `tMax`
1322
- * @param {AbstractShape3D} shape
1323
- * @param {{x:number,y:number,z:number,w:number}} rotation
1324
- * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
1325
- * @param {(entity:number, collider:Collider)=>boolean} [filter]
1326
- * @returns {boolean}
1327
- */
1328
- shapeCast(ray, shape, rotation, result, filter = returnTrue) {
1329
- return shape_cast_query(this, ray, shape, rotation, result, filter);
1330
- }
1331
-
1332
- /**
1333
- * Find every body whose collider overlaps the given convex shape at the
1334
- * given world pose, without mutating the simulation. Intended for
1335
- * kinematic / character controllers testing "would I collide if I moved
1336
- * here?" before committing the move.
1337
- *
1338
- * Writes overlapping bodies' packed `body_id` values into `output` from
1339
- * `output_offset`. Sizing the buffer is the caller's job: ids past its
1340
- * end are dropped silently and the returned count caps at the space
1341
- * available.
1342
- *
1343
- * `filter` runs before the shape test, which is the expensive part — use
1344
- * it to skip the caller's own body, allies, sensors.
1345
- *
1346
- * The query shape must be convex; a concave one throws.
1347
- *
1348
- * @param {AbstractShape3D} shape convex query shape, in its local frame
1349
- * @param {{x:number,y:number,z:number}} position world position of
1350
- * the query shape
1351
- * @param {{x:number,y:number,z:number,w:number}} rotation world
1352
- * rotation (unit quaternion)
1353
- * @param {Uint32Array|number[]} output buffer to receive body_ids
1354
- * @param {number} output_offset starting index in output
1355
- * @param {(entity:number, collider:Collider)=>boolean} [filter]
1356
- * defaults to {@link returnTrue}
1357
- * @returns {number} number of overlapping bodies written
1358
- */
1359
- overlap(shape, position, rotation, output, output_offset, filter = returnTrue) {
1360
- return overlap_shape_query(this, shape, position, rotation, output, output_offset, filter);
1361
- }
1362
-
1363
- /**
1364
- * Re-shape both broadphase trees so subsequent queries — {@link raycast},
1365
- * {@link shapeCast}, {@link overlap}, and the per-step broadphase walk
1366
- * fewer nodes.
1367
- *
1368
- * Call it once after a level's statics are linked. Statics keep the win
1369
- * for good (~12% fewer nodes visited); the dynamic tree's fades within
1370
- * seconds as bodies move, so repeat calls are not worth scheduling. Costs
1371
- * ~20 ms for 4000 bodies a load-time operation, not a per-frame one.
1372
- *
1373
- * @returns {void}
1374
- */
1375
- optimize() {
1376
- // empty and single-leaf trees are a no-op inside the optimizer
1377
- ebvh_optimize_treelet(this.staticBvh);
1378
- ebvh_optimize_treelet(this.dynamicBvh);
1379
- }
1380
-
1381
- /**
1382
- * Wake propagation through contact pairs. Runs BEFORE broadphase so a
1383
- * body woken here participates in THIS frame's broadphase / narrowphase
1384
- * / islands end to end it never simulates a step without its support
1385
- * contacts (free-fall penetration pop).
1386
- *
1387
- * Reads ONLY persistent manifold state (the slots whose pair the
1388
- * broadphase touched last frame `prev_touched`), never the transient
1389
- * pair list: manifold state is part of the engine's exportable contact
1390
- * state, so a reconstructed engine wakes on exactly the original's
1391
- * schedule.
1392
- *
1393
- * A sleeper wakes when:
1394
- * - its pair holds REAL contacts (`contact_count > 0`), unless the
1395
- * awake side is a PARKED non-Dynamic (a stationary kinematic platform
1396
- * or a static sleepState of either is never updated — transmits no
1397
- * motion; the moment it gets velocity, the riders wake); or
1398
- * - the pair has no contacts yet but the awake side's velocity-swept
1399
- * tight AABB just REACHED the sleeper's tight AABB (false→true edge
1400
- * of {@link __swept_proximity}) one precautionary wake so an
1401
- * incoming body never spends its first touching frame against a
1402
- * non-integrating sleeper, over-embedding by v·dt. Edge-triggered on
1403
- * a pure function of body state: pair CREATION time (the old
1404
- * trigger) depends on broadphase fat-box history, which a
1405
- * reconstructed engine does not share; and level-triggered proximity
1406
- * would re-wake a pile next to a hovering body every frame. Slot
1407
- * existence alone wakes nothing.
1408
- * @private
1409
- * @param {number} dt step size — extent of the predicate's velocity sweep
1410
- */
1411
- __wake_pairs(dt) {
1412
- const bodies = this.__bodies;
1413
- const storage = this.storage;
1414
- const manifolds = this.manifolds;
1415
- const live_count = manifolds.count;
1416
- for (let i = 0; i < live_count; i++) {
1417
- const slot = manifolds.live_at(i);
1418
- // "Was in last frame's broadphase": acquire set touched, the
1419
- // end-of-step advance rolled it to prev_touched. (A dormant
1420
- // slot can carry a stale prev_touched from its sleep frame —
1421
- // the both-asleep guard below skips it.)
1422
- if (!manifolds.was_touched_prev(slot)) continue;
1423
-
1424
- const idA = manifolds.bodyA(slot);
1425
- const idB = manifolds.bodyB(slot);
1426
- // Stale-slot guards: a body may have been unlinked since (and
1427
- // its index reused by an unrelated body).
1428
- if (!storage.is_valid(idA) || !storage.is_valid(idB)) continue;
1429
- const idxA = body_id_index(idA);
1430
- const idxB = body_id_index(idB);
1431
- const a = bodies[idxA];
1432
- const b = bodies[idxB];
1433
- if (a === undefined || b === undefined) continue;
1434
-
1435
- const aSleep = a.sleepState === SleepState.Sleeping;
1436
- const bSleep = b.sleepState === SleepState.Sleeping;
1437
- if (aSleep === bSleep) continue; // both awake, or a dormant pair
1438
-
1439
- // A sensor transmits no force and is excluded from sleep islands (is_sensor,
1440
- // IslandBuilder, the contact solver). It must not wake a sleeper either — otherwise a
1441
- // moving trigger volume re-wakes everything it overlaps every frame, holding whole
1442
- // islands permanently awake. Skip the pair, mirroring IslandBuilder.__body_is_sensor.
1443
- if (is_sensor(a, this.__primary_collider(idxA)) || is_sensor(b, this.__primary_collider(idxB))) {
1444
- continue;
1445
- }
1446
-
1447
- if (manifolds.contact_count(slot) > 0) {
1448
- const mover = aSleep ? b : a;
1449
- if (mover.kind !== BodyKind.Dynamic) {
1450
- const lv = mover.linearVelocity;
1451
- const av = mover.angularVelocity;
1452
- if (lv[0] === 0 && lv[1] === 0 && lv[2] === 0
1453
- && av[0] === 0 && av[1] === 0 && av[2] === 0) {
1454
- continue; // parked platform / static transmits no motion
1455
- }
1456
- }
1457
- this.__wake_body(aSleep ? a : b);
1458
- } else {
1459
- // No contacts: pre-contact precautionary wake on the
1460
- // proximity EDGE. The predicate needs no parked exemption —
1461
- // a zero-velocity mover's swept box is its tight box, so it
1462
- // only fires on actual tight-box overlap, once.
1463
- const mover_idx = body_id_index(aSleep ? idB : idA);
1464
- const sleeper_idx = body_id_index(aSleep ? idA : idB);
1465
- const prox = this.__swept_proximity(mover_idx, sleeper_idx, dt);
1466
- const had_prox = manifolds.is_prox(slot);
1467
- manifolds.set_prox(slot, prox);
1468
- if (prox && !had_prox) {
1469
- this.__wake_body(aSleep ? a : b);
1470
- }
1471
- }
1472
- }
1473
- }
1474
-
1475
- /**
1476
- * Stage-0 pre-contact wake predicate: does any of `mover`'s collider
1477
- * tight AABBs, swept directionally by the mover's linear velocity over
1478
- * `dt`, overlap any of `sleeper`'s collider tight AABBs?
1479
- *
1480
- * A pure function of CURRENT body state (poses, shapes, velocity) — no
1481
- * broadphase or pad history so every engine holding the same bodies
1482
- * evaluates it identically. Conservative containment: the swept box is
1483
- * inside the mover's stage-2 fat box (pad = 2·|v|·dt + slack ⊇ v·dt)
1484
- * and the sleeper's tight box is inside its stored leaf, so whenever
1485
- * the predicate is true the broadphase has the pair THIS frame in any
1486
- * engine the manifold slot the wake stage needs is guaranteed to
1487
- * exist on the frame the edge fires.
1488
- *
1489
- * Linear sweep only (matching the broadphase pad): a purely angular
1490
- * approach is woken one frame later by its first actual contact.
1491
- *
1492
- * @private
1493
- * @param {number} mover_idx body index of the awake side
1494
- * @param {number} sleeper_idx body index of the sleeping side
1495
- * @param {number} dt
1496
- * @returns {boolean}
1497
- */
1498
- __swept_proximity(mover_idx, sleeper_idx, dt) {
1499
- const mover_list = this.__body_collider_lists[mover_idx];
1500
- const sleeper_list = this.__body_collider_lists[sleeper_idx];
1501
- if (mover_list === undefined || sleeper_list === undefined) return false;
1502
-
1503
- const lv = this.__bodies[mover_idx].linearVelocity;
1504
- const px = lv[0] * dt;
1505
- const py = lv[1] * dt;
1506
- const pz = lv[2] * dt;
1507
-
1508
- for (let i = 0; i < mover_list.length; i++) {
1509
- const m = mover_list[i];
1510
- if (m.bvhNode === COLLIDER_UNBOUND) continue;
1511
- compute_world_aabb(scratch_prox_mover, 0, m.collider.shape, m.transform);
1512
- if (px < 0) scratch_prox_mover[0] += px; else scratch_prox_mover[3] += px;
1513
- if (py < 0) scratch_prox_mover[1] += py; else scratch_prox_mover[4] += py;
1514
- if (pz < 0) scratch_prox_mover[2] += pz; else scratch_prox_mover[5] += pz;
1515
-
1516
- for (let k = 0; k < sleeper_list.length; k++) {
1517
- const s = sleeper_list[k];
1518
- if (s.bvhNode === COLLIDER_UNBOUND) continue;
1519
- compute_world_aabb(scratch_prox_sleeper, 0, s.collider.shape, s.transform);
1520
- if (scratch_prox_mover[0] <= scratch_prox_sleeper[3]
1521
- && scratch_prox_mover[3] >= scratch_prox_sleeper[0]
1522
- && scratch_prox_mover[1] <= scratch_prox_sleeper[4]
1523
- && scratch_prox_mover[4] >= scratch_prox_sleeper[1]
1524
- && scratch_prox_mover[2] <= scratch_prox_sleeper[5]
1525
- && scratch_prox_mover[5] >= scratch_prox_sleeper[2]) {
1526
- return true;
1527
- }
1528
- }
1529
- }
1530
- return false;
1531
- }
1532
-
1533
- /**
1534
- * Wake propagation across joints: if a joint connects an awake body to a
1535
- * sleeping one, wake the sleeper so the constraint stays coupled (a joint
1536
- * with one body asleep and one awake would otherwise be skipped by the
1537
- * solver, letting the awake side drift). A common trigger is a
1538
- * kinematic/motor-driven body pulling on a sleeping chain.
1539
- *
1540
- * Bodies that slept together as one island share a sleep group, so the
1541
- * usual atomic wake already revives the whole chain when any member is
1542
- * hit; this catches the cases that atomic wake doesn't (joint spanning
1543
- * separate groups, kinematic driver).
1544
- * @private
1545
- */
1546
- __wake_joints() {
1547
- const joints = this.__joints;
1548
- const n = joints.length;
1549
- if (n === 0) return;
1550
- const bodies = this.__bodies;
1551
- const storage = this.storage;
1552
- for (let i = 0; i < n; i++) {
1553
- const joint = joints[i];
1554
- if (joint === undefined || joint === null) continue;
1555
- if (joint._bodyIdB === JOINT_WORLD) continue;
1556
- if (!storage.is_valid(joint._bodyIdA) || !storage.is_valid(joint._bodyIdB)) continue;
1557
- const a = bodies[body_id_index(joint._bodyIdA)];
1558
- const b = bodies[body_id_index(joint._bodyIdB)];
1559
- if (a === undefined || b === undefined) continue;
1560
- const aSleep = a.sleepState === SleepState.Sleeping;
1561
- const bSleep = b.sleepState === SleepState.Sleeping;
1562
- if (aSleep === bSleep) continue; // both awake or both asleep — leave as is
1563
-
1564
- // The "awake" side must be capable of pulling the sleeper. A
1565
- // static anchor's sleepState is never updated (it reads Awake
1566
- // forever), so without this rule a body jointed to a static could
1567
- // never sleep perpetual mismatch wake. A kinematic driver
1568
- // counts only while it is actually moving: a parked motor leaves
1569
- // its chain asleep, and starting it wakes the chain (same motion
1570
- // rule as __wake_pairs).
1571
- const mover = aSleep ? b : a;
1572
- if (mover.kind !== BodyKind.Dynamic) {
1573
- const lv = mover.linearVelocity;
1574
- const av = mover.angularVelocity;
1575
- if (lv[0] === 0 && lv[1] === 0 && lv[2] === 0
1576
- && av[0] === 0 && av[1] === 0 && av[2] === 0) {
1577
- continue;
1578
- }
1579
- }
1580
- if (aSleep) this.__wake_body(a); else this.__wake_body(b);
1581
- }
1582
- }
1583
-
1584
- /**
1585
- * Per-island atomic sleep test. Walks each island once and applies the
1586
- * decision uniformly across all members:
1587
- *
1588
- * - If any member carries {@link RigidBodyFlags.DisableSleep}, the
1589
- * entire island is exempt; every member's sleep_timer is reset.
1590
- * - If `max(|v|² + |ω|²)` across all members is below
1591
- * {@link sleepVelocitySqrThreshold}, every member's sleep_timer is
1592
- * incremented by `dt`. When the smallest member's timer crosses
1593
- * {@link sleepTimeThreshold}, the whole island sleeps atomically in
1594
- * the same step (members get threaded into a sleep-group chain so
1595
- * {@link __wake_body} can wake them all in one call).
1596
- * - Otherwise the island has at least one active member, so every
1597
- * member's timer is reset.
1598
- *
1599
- * This is the design-plan atomic-island sleep — replaces the per-body
1600
- * approximation that lived in this slot during the previous slice.
1601
- * Weakly-connected piles no longer chatter awake when a single member
1602
- * blips above threshold; piles fall asleep and wake up as one.
1603
- *
1604
- * @private
1605
- * @param {number} dt
1606
- */
1607
- __sleep_test(dt) {
1608
- const threshold_sqr = this.sleepVelocitySqrThreshold;
1609
- const time_threshold = this.sleepTimeThreshold;
1610
- const bodies = this.__bodies;
1611
- const islands = this.islands;
1612
- const island_count = islands.island_count;
1613
- const body_offsets = islands.body_offsets;
1614
- const body_data = islands.body_data;
1615
-
1616
- for (let isl = 0; isl < island_count; isl++) {
1617
- const start = body_offsets[isl];
1618
- const end = body_offsets[isl + 1];
1619
- if (end === start) continue;
1620
-
1621
- // Pass 1: find max v² + check DisableSleep across the island.
1622
- let max_v_sqr = 0;
1623
- let any_disable_sleep = false;
1624
- for (let i = start; i < end; i++) {
1625
- const idx = body_data[i];
1626
- const rb = bodies[idx];
1627
- if (rb === undefined) continue;
1628
- if ((rb.flags & RigidBodyFlags.DisableSleep) !== 0) {
1629
- any_disable_sleep = true;
1630
- break;
1631
- }
1632
- const lv = rb.linearVelocity;
1633
- const av = rb.angularVelocity;
1634
- const v_sqr = lv[0] * lv[0] + lv[1] * lv[1] + lv[2] * lv[2]
1635
- + av[0] * av[0] + av[1] * av[1] + av[2] * av[2];
1636
- if (v_sqr > max_v_sqr) max_v_sqr = v_sqr;
1637
- }
1638
-
1639
- if (any_disable_sleep) {
1640
- // Whole island is exempt — reset every member's timer.
1641
- for (let i = start; i < end; i++) {
1642
- const rb = bodies[body_data[i]];
1643
- if (rb !== undefined) rb.sleep_timer = 0;
1644
- }
1645
- continue;
1646
- }
1647
-
1648
- if (max_v_sqr < threshold_sqr) {
1649
- // Island is at rest — increment every member's timer; if the
1650
- // slowest-stabilising member has crossed the time threshold,
1651
- // every member has (they were incremented together this step),
1652
- // so atomic-sleep the island.
1653
- let min_timer = Infinity;
1654
- for (let i = start; i < end; i++) {
1655
- const rb = bodies[body_data[i]];
1656
- if (rb === undefined) continue;
1657
- rb.sleep_timer += dt;
1658
- if (rb.sleep_timer < min_timer) min_timer = rb.sleep_timer;
1659
- }
1660
- if (min_timer >= time_threshold) {
1661
- this.__atomic_sleep_island_range(body_data, start, end);
1662
- }
1663
- } else {
1664
- // At least one member is active — reset every timer.
1665
- for (let i = start; i < end; i++) {
1666
- const rb = bodies[body_data[i]];
1667
- if (rb !== undefined) rb.sleep_timer = 0;
1668
- }
1669
- }
1670
- }
1671
- }
1672
-
1673
- /**
1674
- * Dispatch every buffered contact event to the entities involved, via the
1675
- * dataset's per-entity event channel `dataset.sendEvent(entity, name,
1676
- * payload)`, once for each of the pair's entities. This is the only
1677
- * contact-event path: a system with no dataset attached has nowhere to
1678
- * deliver and emits nothing.
1679
- *
1680
- * Payload is a reused scratch object; listeners must copy anything they
1681
- * intend to retain past the listener body.
1682
- * @private
1683
- */
1684
- __dispatch_contact_events() {
1685
- const events = this.contactEvents;
1686
- const n = events.count;
1687
- if (n === 0) return;
1688
-
1689
- const ecd = (this.entityManager !== null && this.entityManager !== undefined)
1690
- ? this.entityManager.dataset
1691
- : null;
1692
- // No dataset → nowhere to deliver. The buffer is cleared by
1693
- // diff_manifolds each step regardless, so nothing leaks.
1694
- if (ecd === null || ecd === undefined) return;
1695
-
1696
- const manifolds = this.manifolds;
1697
- const data = manifolds.data_buffer;
1698
-
1699
- const payload = this.__contact_payload;
1700
-
1701
- for (let i = 0; i < n; i++) {
1702
- const kind = events.kind_at(i);
1703
- const entA = events.entityA_at(i);
1704
- const entB = events.entityB_at(i);
1705
- const slot = events.slot_at(i);
1706
-
1707
- // Use the deepest contact of the manifold as the representative
1708
- // point/normal/depth for the event. v1: contact 0 only.
1709
- const slot_off = manifolds.slot_data_offset(slot);
1710
- // Gate on the SAME predicate diff_manifolds used to decide this event, not on
1711
- // contact_count alone: an untouched slot (pair left broadphase range, or a body was
1712
- // removed) keeps its last-narrowphased contact_count non-zero because nothing
1713
- // clears the data slab for it. Re-deriving from contact_count would stamp that stale
1714
- // geometry into an End payload (a live penetration reported for bodies now far apart);
1715
- // matching the diff (is_touched && count>0) yields a zeroed payload, as the in-range
1716
- // separation path already does via clear_contacts.
1717
- const has_contact = manifolds.is_touched(slot) && manifolds.contact_count(slot) > 0;
1718
-
1719
- // Scratch payload — write the point/normal components by index to
1720
- // skip the Vector3 change-signal dispatch (nothing observes them).
1721
- const pt = payload.point;
1722
- const nm = payload.normal;
1723
- if (has_contact) {
1724
- const wax = data[slot_off], way = data[slot_off + 1], waz = data[slot_off + 2];
1725
- const wbx = data[slot_off + 3], wby = data[slot_off + 4], wbz = data[slot_off + 5];
1726
- pt[0] = (wax + wbx) * 0.5;
1727
- pt[1] = (way + wby) * 0.5;
1728
- pt[2] = (waz + wbz) * 0.5;
1729
- nm[0] = data[slot_off + 6];
1730
- nm[1] = data[slot_off + 7];
1731
- nm[2] = data[slot_off + 8];
1732
- payload.depth = data[slot_off + 9];
1733
-
1734
- // The event pair is entity-canonical (min, max); the slot's
1735
- // stored normal points from the slot's OWN body B toward its
1736
- // body A an assignment that follows packed-id order, i.e.
1737
- // slot allocation history. Mirror the normal when the
1738
- // canonicalization swapped the pair, so the payload contract
1739
- // ("normal points from entityB toward entityA") holds
1740
- // identically in every engine regardless of slot history.
1741
- const slot_idA = manifolds.bodyA(slot);
1742
- const slot_entA = this.storage.is_valid(slot_idA)
1743
- ? this.storage.entity_at(body_id_index(slot_idA))
1744
- : -1;
1745
- if (slot_entA !== entA) {
1746
- nm[0] = -nm[0];
1747
- nm[1] = -nm[1];
1748
- nm[2] = -nm[2];
1749
- }
1750
- } else {
1751
- pt[0] = 0;
1752
- pt[1] = 0;
1753
- pt[2] = 0;
1754
- nm[0] = 0;
1755
- nm[1] = 0;
1756
- nm[2] = 0;
1757
- payload.depth = 0;
1758
- }
1759
- payload.entityA = entA;
1760
- payload.entityB = entB;
1761
-
1762
- let event_name;
1763
- if (kind === ContactEventKind.Begin) {
1764
- event_name = PhysicsEvents.ContactBegin;
1765
- } else if (kind === ContactEventKind.Stay) {
1766
- event_name = PhysicsEvents.ContactStay;
1767
- } else {
1768
- event_name = PhysicsEvents.ContactEnd;
1769
- }
1770
-
1771
- if (entA >= 0) ecd.sendEvent(entA, event_name, payload);
1772
- if (entB >= 0) ecd.sendEvent(entB, event_name, payload);
1773
- }
1774
- }
1775
-
1776
- /**
1777
- * Producer — restore pass. At the top of a fixed step, reset every awake
1778
- * {@link Interpolated} body's live components to their authoritative state
1779
- * from the previous tick's snapshot, undoing any render-time interpolation
1780
- * the {@link InterpolationSystem} wrote between frames so the sim integrates
1781
- * from truth, not an interpolated pose. A body with no previous snapshot
1782
- * (first step ever, or just woken) is left as-is its live state is already
1783
- * authoritative. No-op unless {@link interpolationLog} is wired.
1784
- * @private
1785
- */
1786
- __interp_restore() {
1787
- const log = this.interpolationLog;
1788
- const em = this.entityManager;
1789
- if (log === null || em === null || em === undefined) return;
1790
- const dataset = em.dataset;
1791
- if (dataset === null || dataset === undefined) return;
1792
-
1793
- const prev_tick = em.fixedStepTick - 1;
1794
- if (prev_tick < 0) return;
1795
-
1796
- const storage = this.storage;
1797
- // Iterate the set snapshotted at prev_tick (the previous __interp_record), NOT the current
1798
- // awake set: a body that fell asleep on prev_tick was awake when it was recorded (record runs
1799
- // before the sleep test) so it carries a prev_tick snapshot, yet it is no longer in the awake
1800
- // set. Restoring only the awake set would leave that just-slept body holding the render-blended
1801
- // pose the last frame wrote which the sim then reads via __swept_proximity and integrates
1802
- // from if it wakes this step. A body woken THIS step has no prev_tick record, so log.interpolate
1803
- // returns false and it is correctly left on its (authoritative) live pose.
1804
- const recorded = this.__interp_recorded;
1805
- const count = this.__interp_recorded_count;
1806
- const scratch = this.__interp_scratch;
1807
-
1808
- for (let i = 0; i < count; i++) {
1809
- const idx = recorded[i];
1810
- const entity = storage.entity_at(idx);
1811
- if (entity < 0) continue; // slot freed since it was recorded
1812
- const interpolated = dataset.getComponent(entity, Interpolated);
1813
- if (interpolated === undefined || interpolated === null) continue;
1814
- // A teleported body (snap set, e.g. via setPose) keeps its live pose
1815
- // this step restoring the previous tick would undo the teleport.
1816
- if (interpolated.snap) continue;
1817
- const key = interpolated.key;
1818
- if (key < 0) continue;
1819
-
1820
- const interpolands = interpolated.interpolands;
1821
- for (let k = 0; k < interpolands.length; k++) {
1822
- const ip = interpolands[k];
1823
- scratch.position = 0;
1824
- // Snap to the previous tick's snapshot (both offsets equal → t irrelevant).
1825
- const ok = log.interpolate(scratch, key, ip.type_id, prev_tick, prev_tick, 0, ip.interpolation_adapter);
1826
- if (!ok) continue;
1827
- const target = dataset.getComponent(entity, ip.component_class);
1828
- if (target === undefined || target === null) continue;
1829
- scratch.position = 0;
1830
- ip.serialization_adapter.deserialize(scratch, target);
1831
- }
1832
- }
1833
- }
1834
-
1835
- /**
1836
- * Producer — record pass. At the end of a fixed step, snapshot every awake
1837
- * {@link Interpolated} body's live components into the shared log under the
1838
- * current `entityManager.fixedStepTick`. The render-time
1839
- * {@link InterpolationSystem} blends consecutive ticks from these snapshots.
1840
- * Only awake (moving) bodies are recorded, so the log stays sparse. No-op
1841
- * unless {@link interpolationLog} is wired.
1842
- *
1843
- * **A body created after this pass has no snapshot for the step it was born on, and renders
1844
- * from one snapshot for a step.** This pass runs at the end of `fixedUpdate`, and game code
1845
- * runs after it that ordering is correct, the sim must read the solved poses so a body
1846
- * spawned by game code is invisible to the tick it was spawned into. Its first record is the
1847
- * NEXT tick, and `InterpolationLog.interpolate` handles the missing half gracefully by falling
1848
- * back to whichever snapshot exists. That is the right primitive behaviour and it produces a
1849
- * bad picture: the entity is drawn at the *newer* tick for a whole frame budget, so it appears
1850
- * to jump a full step's travel and then hold still for one. Measured at 165 Hz by a port
1851
- * (GAP-036) as four frames of a rocket parked at the muzzle, reported by a player as
1852
- * "projectiles move with jerks".
1853
- *
1854
- * **Why this is documented rather than fixed, which is the part worth knowing.** The obvious
1855
- * fix record a body's pose when it links does not fit this log. {@link InterpolationLog} is
1856
- * a strictly monotonic tick ring: `begin_tick` throws while a tick is open, pages are
1857
- * contiguous ranges that the next page's bytes follow immediately, and `#ticks` is keyed by
1858
- * tick number so re-opening the closed tick to append one body would overwrite that tick's
1859
- * metadata for every other body in it. The fix that does work is to move this pass out of
1860
- * `fixedUpdate` into a system registered after every simulation system, which is what a
1861
- * consumer hitting this did; that costs a second mandatory registration, which is a trade this
1862
- * engine has decided against elsewhere. Closing it properly means either a per-body "first
1863
- * tick" the blend can consult, or a log that accepts an out-of-order record.
1864
- *
1865
- * The workaround needs no engine change: put the body on the application's own timeline via a
1866
- * pose recorder registered last, since `__interp_restore` does not filter by `sourceId` and
1867
- * `PhysicsSystem` still restores the authoritative pose before it integrates.
1868
- *
1869
- * @private
1870
- */
1871
- __interp_record() {
1872
- const log = this.interpolationLog;
1873
- const em = this.entityManager;
1874
- if (log === null || em === null || em === undefined) return;
1875
- const dataset = em.dataset;
1876
- if (dataset === null || dataset === undefined) return;
1877
-
1878
- const tick = em.fixedStepTick;
1879
- const storage = this.storage;
1880
- const count = storage.awake_count;
1881
-
1882
- // Rebuild the "recorded this tick" set that __interp_restore will iterate next tick.
1883
- let recorded = this.__interp_recorded;
1884
- let recorded_count = 0;
1885
-
1886
- log.begin_tick(tick);
1887
- for (let i = 0; i < count; i++) {
1888
- const idx = storage.awake_at(i);
1889
- const entity = storage.entity_at(idx);
1890
- const interpolated = dataset.getComponent(entity, Interpolated);
1891
- if (interpolated === undefined || interpolated === null) continue;
1892
- const key = interpolated.key;
1893
- if (key < 0) continue;
1894
-
1895
- if (recorded_count === recorded.length) {
1896
- const grown = new Uint32Array(recorded.length * 2);
1897
- grown.set(recorded);
1898
- recorded = grown;
1899
- this.__interp_recorded = grown;
1900
- }
1901
- recorded[recorded_count++] = idx;
1902
-
1903
- const interpolands = interpolated.interpolands;
1904
- for (let k = 0; k < interpolands.length; k++) {
1905
- const ip = interpolands[k];
1906
- const target = dataset.getComponent(entity, ip.component_class);
1907
- if (target === undefined || target === null) continue;
1908
- const buf = log.begin_record(key, ip.type_id);
1909
- ip.serialization_adapter.serialize(buf, target);
1910
- log.end_record();
1911
- }
1912
- }
1913
- log.end_tick();
1914
-
1915
- this.__interp_recorded_count = recorded_count;
1916
- }
1917
-
1918
- /**
1919
- * Advance the simulation by one step: forces, broadphase, narrowphase,
1920
- * islands, the substep solve, CCD, sleep, and contact-event dispatch.
1921
- * Reads and writes the linked {@link RigidBody} and {@link Transform}
1922
- * components in place.
1923
- *
1924
- * Pass the same `dt` every step — warm-starting and sleep timing both
1925
- * assume a fixed rate.
1926
- *
1927
- * @param {number} dt seconds
1928
- */
1929
- fixedUpdate(dt) {
1930
- // Producer: restore authoritative pose (undo render interpolation)
1931
- // before the sim reads any Transform this step.
1932
- this.__interp_restore();
1933
-
1934
- const gx = this.gravity.x;
1935
- const gy = this.gravity.y;
1936
- const gz = this.gravity.z;
1937
-
1938
- const storage = this.storage;
1939
- const bodies = this.__bodies;
1940
- const transforms = this.__transforms;
1941
- const joints = this.__joints;
1942
- const manifolds = this.manifolds;
1943
-
1944
- // Stage 0: wake propagation — through the persistent manifolds
1945
- // (last frame's broadphase-touched pairs), then through joints.
1946
- // Runs before everything else so a just-woken body is part of this
1947
- // frame's awake set end to end (forces, refit, broadphase,
1948
- // narrowphase, islands, solve): waking later would give it one
1949
- // unsupported step (gravity with no contacts penetration pop).
1950
- // Reads only manifold + body state both part of the exportable
1951
- // world state never the transient pair list, so a reconstructed
1952
- // engine wakes on the original's schedule.
1953
- this.__wake_pairs(dt);
1954
- this.__wake_joints();
1955
-
1956
- const count = storage.awake_count;
1957
-
1958
- // Stage 1: consume the per-frame force / torque accumulators into
1959
- // velocity at the full `dt`, exactly once (a user force is a per-frame
1960
- // budget that must land in full regardless of substep count). Gravity
1961
- // is applied per substep below, so the trajectory integrates at the
1962
- // substep rate and crucially each substep's gravity is balanced by
1963
- // that substep's contact warm-start, keeping resting stacks at zero
1964
- // velocity.
1965
- for (let i = 0; i < count; i++) {
1966
- const idx = storage.awake_at(i);
1967
-
1968
- const rb = bodies[idx];
1969
- const tr = transforms[idx];
1970
-
1971
- integrate_velocity_forces(rb, tr, dt);
1972
- }
1973
-
1974
- // Stage 2: refit each awake body's collider leaves at the current
1975
- // pose, padded by the swept extent for the body's velocity. The fat
1976
- // margin uses the post-force velocity; this frame's gravity increment
1977
- // is a sub-millimetre slack difference, safely inside the margin.
1978
- const lists = this.__body_collider_lists;
1979
- for (let i = 0; i < count; i++) {
1980
-
1981
- const idx = storage.awake_at(i);
1982
- const rb = bodies[idx];
1983
- const list = lists[idx];
1984
-
1985
- if (list === undefined) {
1986
- continue;
1987
- }
1988
-
1989
- const lv = rb.linearVelocity;
1990
-
1991
- const list_length = list.length;
1992
-
1993
- for (let k = 0; k < list_length; k++) {
1994
-
1995
- const entry = list[k];
1996
-
1997
- // Refit skip — the fat margin's whole purpose: while the
1998
- // body's FATTENED box (tight + velocity pad) stays inside the
1999
- // leaf's stored AABB, the stored box still covers everything
2000
- // the body can reach this step, so the set + refit-to-root is
2001
- // pure waste. A resting field pays only this containment test
2002
- // per leaf per frame (at v 0 the fat box is the tight box
2003
- // plus the constant slack, which the stored box includes).
2004
- //
2005
- // The containment probe MUST be the fat box, not the tight
2006
- // one: the leaf is how the broadphase and every swept query
2007
- // discover this body. A body whose velocity grew while its
2008
- // tight box still sat inside the stored leaf (freshly linked
2009
- // with a launch velocity, or accelerated by an impulse) would
2010
- // otherwise keep a leaf covering none of the path it travels
2011
- // this step invisible to pair generation and to other
2012
- // bodies' CCD sweeps for the whole step.
2013
- compute_world_aabb(scratch_world_aabb, 0, entry.collider.shape, entry.transform);
2014
- fatten_world_aabb(scratch_world_aabb, 0, lv[0], lv[1], lv[2], dt);
2015
- const node = entry.bvhNode;
2016
- this.dynamicBvh.node_get_aabb(node, scratch_stored_aabb);
2017
- if (scratch_world_aabb[0] >= scratch_stored_aabb[0]
2018
- && scratch_world_aabb[1] >= scratch_stored_aabb[1]
2019
- && scratch_world_aabb[2] >= scratch_stored_aabb[2]
2020
- && scratch_world_aabb[3] <= scratch_stored_aabb[3]
2021
- && scratch_world_aabb[4] <= scratch_stored_aabb[4]
2022
- && scratch_world_aabb[5] <= scratch_stored_aabb[5]) {
2023
- continue;
2024
- }
2025
-
2026
- this.dynamicBvh.node_move_aabb(node, scratch_world_aabb);
2027
- }
2028
- }
2029
-
2030
- // Stage 3: broadphase pair generation. The fat AABBs cover the full
2031
- // outer-step motion, so the pair set stays valid across all substeps
2032
- // broadphase runs once.
2033
- generate_pairs(
2034
- storage,
2035
- this.dynamicBvh,
2036
- this.staticBvh,
2037
- manifolds,
2038
- lists,
2039
- this.pairs,
2040
- this.__pair_filter,
2041
- this,
2042
- );
2043
-
2044
- // Stage 5: narrowphase — once per outer step. The substep loop below
2045
- // re-derives each contact's penetration analytically from the moved
2046
- // poses rather than re-running geometry.
2047
- narrowphase_step(this.pairs, manifolds, this.__body_collider_lists);
2048
-
2049
- // Stage 6: partition awake bodies + touched contacts into islands.
2050
- // Consumed by the solver (flattened contact list) and the sleep test.
2051
- this.islands.build(storage, manifolds, bodies, this.__body_collider_lists, joints);
2052
-
2053
- // Stage 7: TGS substep loop.
2054
- //
2055
- // prepare_contacts captures per-contact anchors / effective masses /
2056
- // approach velocity (no warm-start that's per-substep). Each substep
2057
- // integrates gravity by `h`, re-derives contact geometry from the
2058
- // current poses, replays warm-start, solves velocity (non-penetration
2059
- // + friction) and position (pseudo-velocity), and integrates the pose
2060
- // by `h`. Per-substep gravity + per-substep warm-start balance exactly
2061
- // at a resting contact (each cancels `h` of the other), so stacks hold
2062
- // at zero velocity and sleep; the position correction adapts as bodies
2063
- // separate between substeps the TGS stack-stability mechanism
2064
- // without re-running narrowphase. Restitution is applied once after
2065
- // the loop.
2066
- // Clamp the substep count: 0 (or a negative / fractional value) would
2067
- // feed h = dt / N = Infinity into prepare_contacts and a broken loop
2068
- // bound into the solver.
2069
- const N = this.substeps > 1 ? this.substeps | 0 : 1;
2070
- const h = dt / N;
2071
- const count_after_wake = storage.awake_count;
2072
-
2073
- // CCD: capture start-of-step positions for flagged bodies over the
2074
- // post-wake awake set (poses are unchanged until the substep loop below
2075
- // integrates them). The CCD pass after the solver sweeps from here to
2076
- // each body's final pose. Reads the primary collider's transform so the
2077
- // start matches the end the resolve pass reads. Zero-cost when no body
2078
- // is flagged.
2079
- const ccd_on = this.ccdEnabled;
2080
-
2081
- if (ccd_on) {
2082
-
2083
- const ccd_need = storage.high_water_mark * 3;
2084
-
2085
- if (this.__ccd_start_pos.length < ccd_need) {
2086
- this.__ccd_start_pos = new Float64Array(ccd_need);
2087
- }
2088
-
2089
- const ccd_start = this.__ccd_start_pos;
2090
-
2091
- for (let i = 0; i < count_after_wake; i++) {
2092
- const idx = storage.awake_at(i);
2093
- const rb = bodies[idx];
2094
-
2095
- if (rb.kind !== BodyKind.Dynamic) {
2096
- continue;
2097
- }
2098
- if ((rb.flags & RigidBodyFlags.CCD) === 0) {
2099
- continue;
2100
- }
2101
-
2102
- const list = this.__body_collider_lists[idx];
2103
-
2104
- if (list === undefined || list.length === 0) {
2105
- continue;
2106
- }
2107
-
2108
- const cp = list[0].transform.position;
2109
- const cb = idx * 3;
2110
-
2111
- ccd_start[cb] = cp[0];
2112
- ccd_start[cb + 1] = cp[1];
2113
- ccd_start[cb + 2] = cp[2];
2114
- }
2115
- }
2116
-
2117
- // Size the pseudo-velocity buffer ONCE (it may reallocate on growth),
2118
- // then capture the reference. Inside the loop we only zero its live
2119
- // region per substep re-capturing is unnecessary since it won't
2120
- // reallocate again this step.
2121
- this.__reset_pseudo_velocity();
2122
- const pseudoVel = this.__pseudo_velocity;
2123
- const pseudo_len = storage.high_water_mark * 6;
2124
-
2125
- // Gather the data-oriented solver state for every body the substep loop
2126
- // will touch: the post-wake awake set (all dynamics, at their post-force
2127
- // velocity) plus the static / kinematic anchors and jointed partners
2128
- // referenced by this step's contacts and joints. From here the substep
2129
- // loop reads / writes velocity + orientation through `ss_data` with no
2130
- // component-object dereference; persistent velocity is scattered back
2131
- // onto the RigidBodies after the solve.
2132
- const solver_state = this.__solver_state;
2133
- solver_state.begin(storage.high_water_mark);
2134
-
2135
- for (let i = 0; i < count_after_wake; i++) {
2136
- const idx = storage.awake_at(i);
2137
- solver_state.gather(idx, bodies[idx], transforms[idx]);
2138
- }
2139
-
2140
- const island_contacts = this.islands.contact_data;
2141
- const island_contact_total = this.islands.contact_offsets[this.islands.island_count];
2142
-
2143
- for (let i = 0; i < island_contact_total; i++) {
2144
- const slot = island_contacts[i];
2145
-
2146
- const ia = body_id_index(manifolds.bodyA(slot));
2147
- const ib = body_id_index(manifolds.bodyB(slot));
2148
-
2149
- solver_state.gather(ia, bodies[ia], transforms[ia]);
2150
- solver_state.gather(ib, bodies[ib], transforms[ib]);
2151
- }
2152
-
2153
- const joint_count = joints.length;
2154
- for (let i = 0; i < joint_count; i++) {
2155
- const joint = joints[i];
2156
-
2157
- if (joint === undefined || joint === null) {
2158
- continue;
2159
- }
2160
-
2161
- if (!storage.is_valid(joint._bodyIdA)) {
2162
- continue;
2163
- }
2164
-
2165
- const ia = body_id_index(joint._bodyIdA);
2166
-
2167
- solver_state.gather(ia, bodies[ia], transforms[ia]);
2168
-
2169
- if (joint._bodyIdB !== JOINT_WORLD && storage.is_valid(joint._bodyIdB)) {
2170
-
2171
- const ib = body_id_index(joint._bodyIdB);
2172
-
2173
- solver_state.gather(ib, bodies[ib], transforms[ib]);
2174
-
2175
- }
2176
- }
2177
-
2178
- const ss_data = solver_state.data;
2179
-
2180
- prepare_contacts(manifolds, this, h);
2181
-
2182
- for (let s = 0; s < N; s++) {
2183
-
2184
- // Gravity (+ damping) for this substep.
2185
- for (let i = 0; i < count_after_wake; i++) {
2186
- const idx = storage.awake_at(i);
2187
- integrate_velocity_gravity(ss_data, idx * SBS_STRIDE, bodies[idx], gx, gy, gz, h);
2188
- }
2189
-
2190
- // Re-derive contact geometry at the current poses: concave pairs
2191
- // re-run narrowphase (fresh feature/normal as the body rocks),
2192
- // convex pairs rotate frozen anchors analytically. Then replay
2193
- // the per-substep warm-start and solve velocity.
2194
- redetect_concave_contacts(manifolds, this);
2195
- refresh_contacts(manifolds, this.__transforms);
2196
- warm_start_contacts(manifolds, this);
2197
- solve_velocity(manifolds, this, this.velocityIterations);
2198
-
2199
- // Joints share the substep: warm-start + velocity-solve the 6-DOF
2200
- // constraints on real velocity, coupled with the contacts above
2201
- // (a body touched by both sees one substep of Gauss-Seidel across
2202
- // contacts then joints). Position correction for locked DOFs is a
2203
- // SPOOK bias inside this solve, so no separate joint position pass.
2204
- // Solved in the entity-canonical order (NOT joint-id order, which
2205
- // encodes link/unlink history) see __joints_sorted.
2206
- if (this.__joints_sorted.length > 0) {
2207
- // Alternate the joint sweep direction on odd substeps so the chain
2208
- // is solved root→tip and tip→root across the step (symmetric
2209
- // Gauss-Seidel), cancelling the one-direction propagation bias.
2210
- solve_joints(this.__joints_sorted, this, h, this.jointIterations, (s & 1) === 1);
2211
- }
2212
-
2213
- // Position correction writes pseudo-velocity (zeroed first so it
2214
- // is a fresh per-substep correction), folded into the pose by the
2215
- // position integrate and then discarded. SOFT contacts skip it:
2216
- // their penetration recovery is the velocity-bias term, and the
2217
- // relax pass below strips the momentum that bias injected
2218
- // (Box2D-v3 substep order).
2219
- const contacts_soft = this.contactHertz > 0;
2220
- pseudoVel.fill(0, 0, pseudo_len);
2221
- if (!contacts_soft) {
2222
- solve_position(manifolds, this, this.positionIterations);
2223
- }
2224
-
2225
- for (let i = 0; i < count_after_wake; i++) {
2226
- const idx = storage.awake_at(i);
2227
- const rb = bodies[idx];
2228
- const tr = transforms[idx];
2229
- const base = idx * 6;
2230
- integrate_position(ss_data, idx * SBS_STRIDE, rb, tr, h,
2231
- pseudoVel[base], pseudoVel[base + 1], pseudoVel[base + 2],
2232
- pseudoVel[base + 3], pseudoVel[base + 4], pseudoVel[base + 5]);
2233
- }
2234
-
2235
- // Relax pass (soft contacts only): one biasless sweep over the
2236
- // contact rows AFTER position integration, removing the real
2237
- // momentum the velocity-bias push injected this substep
2238
- // (Box2D-v3 substep order: solve -> integrate -> relax).
2239
- if (contacts_soft) {
2240
- solve_velocity(manifolds, this, 1, false);
2241
- }
2242
- }
2243
-
2244
- // Stage 8: one-shot restitution, after the substep loop, keyed off
2245
- // the approach velocity captured at prepare time.
2246
- apply_restitution(manifolds, this);
2247
-
2248
- // Scatter the solved persistent linear / angular velocity back onto the
2249
- // RigidBody components (pose was written through to the Transforms each
2250
- // substep). After this the bodies are authoritative again for CCD,
2251
- // interpolation recording, and the sleep test below.
2252
- solver_state.scatter(bodies);
2253
-
2254
- // Stage 8.5: continuous collision — sweep CCD-flagged fast movers along
2255
- // their net step translation and stop them at the first blocker, so they
2256
- // can't tunnel through thin geometry between discrete steps. Runs on the
2257
- // final post-solve poses, before the sleep test sees the clamped
2258
- // velocities. No-op when no awake body is flagged.
2259
- if (ccd_on) {
2260
- ccd_resolve(this);
2261
- }
2262
-
2263
- // Producer: record the post-step authoritative pose of every awake
2264
- // interpolated body under this step's tick. Before the sleep test, so a
2265
- // body that settles this step still records its final pose for the last
2266
- // interpolation interval.
2267
- this.__interp_record();
2268
-
2269
- // Stage 9: sleep test.
2270
- this.__sleep_test(dt);
2271
-
2272
- // Stage 10: diff manifolds against the previous frame and dispatch
2273
- // Begin / Stay / End events. MUST run before advance_frame, which
2274
- // rolls the touched flags.
2275
- diff_manifolds(manifolds, storage, this.contactEvents);
2276
- this.__dispatch_contact_events();
2277
-
2278
- // Stage 11 (end-of-step): roll touched → prev_touched and the
2279
- // had-contacts flag, evict slots whose pair has not been touched
2280
- // within the grace window. Dormant pairs (both bodies asleep) are
2281
- // frozencontacts and warm-start impulses survive the nap.
2282
- manifolds.advance_frame(storage);
2283
-
2284
- // Stage 11.5: take back the contacts the CCD pass wrote for its own
2285
- // impacts. They were a report for the diff above, not manifold state —
2286
- // the advance has already rolled them into their pair's had-contacts
2287
- // bit (so the next step resolves to Stay, not a second Begin), and
2288
- // leaving them in the slabs would let __wake_pairs, which runs before
2289
- // the narrowphase can clear them, wake a sleeper on a contact the
2290
- // solver never acts on.
2291
- if (ccd_on) {
2292
- ccd_release_contacts(this);
2293
- }
2294
-
2295
- // Dev tripwire (assert.* is compiled out of prod): no NaN / Infinity
2296
- // may survive a step in body state or contact manifolds. NaN is
2297
- // self-concealing downstream — it fails every depth comparison and
2298
- // sleep threshold — so it must fail loudly here, at the step boundary.
2299
- assert.equal(find_non_finite_physics_state(this), "", "non-finite physics state after fixedUpdate");
2300
- }
2301
- }
2302
-
2303
- /**
2304
- * @readonly
2305
- * @type {boolean}
2306
- */
2307
- PhysicsSystem.prototype.isPhysicsSystem = true;
2308
-
2309
- // Re-export for convenience.
2310
- export { BodyKind, RigidBodyFlags };
1
+ import { assert } from "../../../core/assert.js";
2
+ import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
3
+ import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
4
+ import { ebvh_optimize_treelet } from "../../../core/bvh2/bvh3/ebvh_optimize_treelet.js";
5
+ import {
6
+ bvh_query_user_data_intersects_aabb
7
+ } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_aabb.js";
8
+ import { returnTrue } from "../../../core/function/returnTrue.js";
9
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
10
+ import Vector3 from "../../../core/geom/Vector3.js";
11
+ import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
12
+ import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
13
+ import { System } from "../../ecs/System.js";
14
+ import { Transform64 } from "../../ecs/transform/Transform64.js";
15
+ import { Interpolated } from "../../interpolation/Interpolated.js";
16
+ import { body_id_index, BodyStorage } from "../body/BodyStorage.js";
17
+ import { SBS_STRIDE, SolverBodyState } from "../body/SolverBodyState.js";
18
+ import { compute_world_aabb, fatten_world_aabb } from "../broadphase/compute_fat_world_aabb.js";
19
+ import { generate_pairs } from "../broadphase/generate_pairs.js";
20
+ import { PairList } from "../broadphase/PairList.js";
21
+ import { ccd_release_contacts, ccd_resolve } from "../ccd/linear_sweep.js";
22
+ import { solve_joints } from "../constraint/solve_constraints.js";
23
+ import { ManifoldStore } from "../contact/ManifoldStore.js";
24
+ import { ContactEventBuffer, ContactEventKind } from "../events/ContactEventBuffer.js";
25
+ import { diff_manifolds } from "../events/diff_manifolds.js";
26
+ import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
27
+ import { integrate_position } from "../integration/integrate_position.js";
28
+ import { integrate_velocity_forces, integrate_velocity_gravity } from "../integration/integrate_velocity.js";
29
+ import { IslandBuilder } from "../island/IslandBuilder.js";
30
+ import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
31
+ import { overlap_shape as overlap_shape_query } from "../queries/overlap_shape.js";
32
+ import { raycast as raycast_query } from "../queries/raycast.js";
33
+ import { shape_cast as shape_cast_query } from "../queries/shape_cast.js";
34
+ import {
35
+ apply_restitution,
36
+ prepare_contacts,
37
+ redetect_concave_contacts,
38
+ refresh_contacts,
39
+ solve_position,
40
+ solve_velocity,
41
+ warm_start_contacts,
42
+ } from "../solver/solve_contacts.js";
43
+ import { BodyKind } from "./BodyKind.js";
44
+ import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
45
+ import { find_non_finite_physics_state } from "./find_non_finite_physics_state.js";
46
+ import { is_sensor } from "./is_sensor.js";
47
+ import { JOINT_UNALLOCATED, JOINT_WORLD } from "./Joint.js";
48
+ import { PhysicsEvents } from "./PhysicsEvents.js";
49
+ import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
50
+ import { RigidBodyFlags } from "./RigidBodyFlags.js";
51
+ import { SleepState } from "./SleepState.js";
52
+ import { EventType } from "../../ecs/EventType.js";
53
+
54
+ /**
55
+ * Scratch for {@link applyImpulseAt}'s angular delta calculation.
56
+ * @type {Float64Array}
57
+ */
58
+ const scratch_angular_delta = new Float64Array(3);
59
+
60
+ /**
61
+ * Scratch for {@link applyImpulseAt}'s LockRot*-masked inverse-inertia
62
+ * diagonal (the same mask {@link SolverBodyState#gather} applies).
63
+ * @type {{x: number, y: number, z: number}}
64
+ */
65
+ const scratch_locked_inertia = { x: 0, y: 0, z: 0 };
66
+
67
+ /**
68
+ * Reusable scratch buffer for world-AABB construction so the link path is
69
+ * allocation-free in steady state.
70
+ * @type {Float64Array}
71
+ */
72
+ const scratch_world_aabb = new Float64Array(6);
73
+
74
+ /**
75
+ * Reusable scratch buffer for the local AABB returned by
76
+ * {@link AbstractShape3D#compute_bounding_box}.
77
+ * @type {Float64Array}
78
+ */
79
+ const scratch_local_aabb = new Float64Array(6);
80
+
81
+ /**
82
+ * Candidate buffer for {@link PhysicsSystem#__wake_sleepers_overlapping_leaf}.
83
+ * Grows by doubling: a large removed support (a level floor) can overlap many
84
+ * sleeping bodies, all of which must wake.
85
+ * @type {Uint32Array}
86
+ */
87
+ let scratch_wake_candidates = new Uint32Array(64);
88
+
89
+ /**
90
+ * Scratch for a leaf's currently-stored fat AABB during the stage-2
91
+ * containment test (refit skip).
92
+ * @type {Float64Array}
93
+ */
94
+ const scratch_stored_aabb = new Float64Array(6);
95
+
96
+ /**
97
+ * Scratch boxes for {@link PhysicsSystem#__swept_proximity} — the mover's
98
+ * velocity-swept tight AABB and the sleeper's tight AABB.
99
+ * @type {Float64Array}
100
+ */
101
+ const scratch_prox_mover = new Float64Array(6);
102
+ const scratch_prox_sleeper = new Float64Array(6);
103
+
104
+ /**
105
+ * Rigid-body physics system.
106
+ *
107
+ * v1 scope: pool + active list + two BVHs (static / dynamic), Transform sync
108
+ * contract, and the user-facing API surface (gravity, force / impulse, layer
109
+ * filtering, wake / sleep, contact filter). The per-step simulation pipeline
110
+ * (broadphase pair generation, narrowphase, solver, islands, sleep test,
111
+ * contact-event emission) is built on top of this skeleton in subsequent
112
+ * iterations.
113
+ *
114
+ * Dependency tuple is `(RigidBody, Collider, Transform64)` every body has
115
+ * exactly one collider on the same entity. Compound bodies via child collider
116
+ * entities are an extension point handled by a follow-up sub-observer.
117
+ *
118
+ * NOTE: requires ColliderObserverSystem to be registered together with this system.
119
+ *
120
+ * @see ColliderObserverSystem
121
+ * @author Alex Goldring
122
+ * @copyright Company Named Limited (c) 2026
123
+ */
124
+ export class PhysicsSystem extends System {
125
+
126
+ dependencies = [RigidBody, Transform64];
127
+ components_used = [
128
+ ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Read | ResourceAccessKind.Write),
129
+ ResourceAccessSpecification.from(Collider, ResourceAccessKind.Read),
130
+ ResourceAccessSpecification.from(Transform64, ResourceAccessKind.Read | ResourceAccessKind.Write),
131
+ ];
132
+
133
+ /**
134
+ * Body slots, the awake / sleeping sets, and generation counters.
135
+ * @type {BodyStorage}
136
+ * @readonly
137
+ */
138
+ storage = new BodyStorage();
139
+
140
+ /**
141
+ * Broadphase tree holding every static collider leaf. The step never
142
+ * refits it — statics do not move.
143
+ * @type {BVH}
144
+ */
145
+ staticBvh = new BVH();
146
+
147
+ /**
148
+ * Broadphase tree holding every dynamic and kinematic collider leaf.
149
+ * Leaves carry a velocity-fattened AABB and are refit each step.
150
+ * @type {BVH}
151
+ */
152
+ dynamicBvh = new BVH();
153
+
154
+ /**
155
+ * Persistent contact-manifold cache. One slot per active pair.
156
+ * @type {ManifoldStore}
157
+ * @readonly
158
+ */
159
+ manifolds = new ManifoldStore();
160
+
161
+ /**
162
+ * Per-frame list of broadphase-overlapping pairs, each in
163
+ * entity-canonical role order (A = the smaller entity id). Cleared at
164
+ * the top of each step.
165
+ * @type {PairList}
166
+ */
167
+ pairs = new PairList();
168
+
169
+ /**
170
+ * Per-frame contact-event buffer. Populated by the manifold diff
171
+ * pass at end-of-step and consumed by the dispatch pass.
172
+ * @type {ContactEventBuffer}
173
+ */
174
+ contactEvents = new ContactEventBuffer();
175
+
176
+ /**
177
+ * Per-frame island partitioning of the awake-body + contact graph,
178
+ * consumed by the solver and the sleep test. Static and kinematic
179
+ * bodies act as constraint anchors and do not enlarge islands.
180
+ * @type {IslandBuilder}
181
+ */
182
+ islands = new IslandBuilder();
183
+
184
+ /**
185
+ * Data-oriented mirror of the per-body solver hot state (velocity,
186
+ * inverse mass / inertia, orientation), packed into one ArrayBuffer
187
+ * and indexed by body slot. Gathered from the `RigidBody` /
188
+ * `Transform` components once per step (after islands are built),
189
+ * mutated in place by the TGS substep loop, and the persistent
190
+ * velocity scattered back at the end. Keeps the solver's hottest inner
191
+ * loop free of component-object dereferences.
192
+ * @type {SolverBodyState}
193
+ */
194
+ __solver_state = new SolverBodyState();
195
+
196
+ /**
197
+ * Velocity-squared threshold below which a body is eligible to start
198
+ * accumulating sleep time. Combined linear + angular kinetic-ish
199
+ * metric: `vx²+vy²+vz² + ωx²+ωy²+ωz²`. Default 0.01 corresponds to
200
+ * ~0.1 m/s linear or ~0.1 rad/s angular.
201
+ * @type {number}
202
+ */
203
+ sleepVelocitySqrThreshold = 0.01;
204
+
205
+ /**
206
+ * Seconds of below-threshold motion before a body is moved to the
207
+ * sleeping set. Box2D default is 0.5 s.
208
+ * @type {number}
209
+ */
210
+ sleepTimeThreshold = 0.5;
211
+
212
+ /**
213
+ * Number of TGS substeps per `fixedUpdate`. Each substep re-runs the
214
+ * velocity + position solve at `dt / substeps`. Higher counts buy stack
215
+ * stability, high-mass-ratio robustness and smoother trajectories, at
216
+ * near-linear solver cost. `1` gives a plain single-step solve.
217
+ * @type {number}
218
+ */
219
+ substeps = 4;
220
+
221
+ /**
222
+ * Velocity iterations per substep. Lower than a single-step solver
223
+ * would need, because the substep loop revisits the contact set
224
+ * `substeps` times.
225
+ * @type {number}
226
+ */
227
+ velocityIterations = 4;
228
+
229
+ /**
230
+ * Position (split-impulse) iterations per substep.
231
+ * @type {number}
232
+ */
233
+ positionIterations = 1;
234
+
235
+ /**
236
+ * Contact softness frequency, Hz. Softening is what lets resting stacks
237
+ * settle instead of ringing just above the sleep threshold; `0` gives
238
+ * hard contacts. Clamped per step to an eighth of the substep rate.
239
+ * Pairs with a non-dynamic side use 2× this frequency — stiffer, so
240
+ * piles don't sag into the ground.
241
+ * @type {number}
242
+ */
243
+ contactHertz = 30;
244
+
245
+ /**
246
+ * Contact damping ratio ζ for the soft scales — heavily over-damped
247
+ * by design (Box2D v3 default), so contacts never oscillate.
248
+ * @type {number}
249
+ */
250
+ contactDampingRatio = 10;
251
+
252
+ /**
253
+ * Reusable contact-event payload. Listeners must copy any fields they
254
+ * intend to retain past their own scope. Reset before each dispatch.
255
+ * @private
256
+ */
257
+ __contact_payload = {
258
+ entityA: -1,
259
+ entityB: -1,
260
+ kind: 0,
261
+ depth: 0,
262
+ normal: new Vector3(),
263
+ point: new Vector3(),
264
+ };
265
+
266
+ /**
267
+ * World gravity, m/s². Applied each step scaled by per-body gravityScale.
268
+ * @readonly
269
+ * @type {Vector3}
270
+ */
271
+ gravity = new Vector3(0, -9.81, 0);
272
+
273
+ /**
274
+ * Optional global contact filter. Called for each surviving broadphase
275
+ * pair; returning `false` discards the pair.
276
+ * @type {((entityA: number, entityB: number, colliderA: Collider, colliderB: Collider) => boolean) | null}
277
+ * @private
278
+ */
279
+ __contact_filter = null;
280
+
281
+ /**
282
+ * Per-body component side-tables indexed by body index (NOT packed id).
283
+ * Sparse arrays — populated at link, cleared at unlink. The hot loop
284
+ * iterates `storage.awake_at(i)` and dereferences these.
285
+ * @type {RigidBody[]}
286
+ */
287
+ __bodies = [];
288
+
289
+ /**
290
+ * @type {Transform64[]}
291
+ * @readonly
292
+ */
293
+ __transforms = [];
294
+
295
+ /**
296
+ * Per-body list of attached colliders. Each entry stores the
297
+ * Collider component, its world Transform, the entity that owns
298
+ * it (the body entity for same-entity attachments, a child entity
299
+ * for compound bodies), and the BVH leaf id assigned at attach
300
+ * time. A body may have zero or more attached colliders.
301
+ *
302
+ * @type {Array<Array<{collider: Collider, transform: Transform, entity: number, bvhNode: number}>>}
303
+ */
304
+ __body_collider_lists = [];
305
+
306
+ /**
307
+ * Live {@link Joint} (6-DOF constraint) instances, in a sparse array
308
+ * indexed by joint id. Solved alongside contacts inside the TGS
309
+ * substep loop. Holes (unlinked joints) are `undefined`.
310
+ * @type {Joint[]}
311
+ */
312
+ __joints = [];
313
+
314
+ /**
315
+ * Freed {@link __joints} slot ids, reused LIFO by {@link link_joint} so
316
+ * ids stay dense. Solve order does not follow it — {@link __joints_sorted}
317
+ * owns that.
318
+ * @private
319
+ * @type {number[]}
320
+ */
321
+ __joint_free = [];
322
+
323
+ /**
324
+ * Live joints in CANONICAL SOLVE ORDER: ascending `(entityA,
325
+ * entityB)`, equal keys in link order. The id-indexed
326
+ * {@link __joints} registry reflects link/unlink HISTORY (slot
327
+ * reuse), which two engines holding identical joints need not
328
+ * share the joint solver's Gauss-Seidel sweep must consume
329
+ * joints in an order derivable from state alone or the engines
330
+ * diverge. Maintained by {@link link_joint} / {@link unlink_joint};
331
+ * consumed by the substep loop. Multiple joints between the SAME
332
+ * entity pair keep their relative link order reconstruction
333
+ * replays links in the same order, so that too is state-derived.
334
+ * @private
335
+ * @type {Joint[]}
336
+ */
337
+ __joints_sorted = [];
338
+
339
+ /**
340
+ * Velocity iterations per substep for the joint solver. Joints (and
341
+ * especially joint chains) want a few more iterations than contacts to
342
+ * propagate impulses along the chain; cheap because joints are far
343
+ * fewer than contacts.
344
+ * @type {number}
345
+ */
346
+ jointIterations = 8;
347
+
348
+ /**
349
+ * Per-body pseudo-velocity for the Catto split-impulse position
350
+ * pass (TGS Phase 1). Flat layout, 6 doubles per body slot index:
351
+ * `[lin.x, lin.y, lin.z, ang.x, ang.y, ang.z]`. Sized to
352
+ * `storage.high_water_mark * 6` at the top of each fixedUpdate
353
+ * and zeroed in place so unwritten slots contribute nothing to
354
+ * `integrate_position`.
355
+ *
356
+ * The solver writes during its position pass; `integrate_position`
357
+ * reads and folds into the pose update on the same tick, then
358
+ * the next tick's zero-pass wipes the state. It NEVER lands in
359
+ * `linearVelocity` / `angularVelocity` that's the point of
360
+ * split impulse: depth correction does not contaminate persistent
361
+ * velocity (so a `restitution = 0` impact stops cleanly).
362
+ *
363
+ * @type {Float64Array}
364
+ */
365
+ __pseudo_velocity = new Float64Array(0);
366
+
367
+ /**
368
+ * Master switch for the continuous-collision pass. When false the
369
+ * {@link RigidBodyFlags.CCD} flag is ignored and no swept queries run.
370
+ * @type {boolean}
371
+ */
372
+ ccdEnabled = true;
373
+
374
+ /**
375
+ * Start-of-step world positions for CCD-flagged bodies 3 doubles per
376
+ * body slot index (`[x, y, z]`). Captured in Stage 1 before the substep
377
+ * loop integrates poses; the CCD pass ({@link ccd_resolve}) sweeps from
378
+ * here to the final pose. Grows to `storage.high_water_mark * 3`; only
379
+ * CCD-flagged slots are written each step.
380
+ * @type {Float64Array}
381
+ */
382
+ __ccd_start_pos = new Float64Array(0);
383
+
384
+ /**
385
+ * Set this to the {@link InterpolationSystem}'s log to get render
386
+ * interpolation: each step then restores every awake {@link Interpolated}
387
+ * body's authoritative pose before simulating, and records the result
388
+ * afterwards. Leave `null` on a headless world — the work is skipped
389
+ * entirely.
390
+ * @type {InterpolationLog|null}
391
+ */
392
+ interpolationLog = null;
393
+
394
+ /** @private @type {number} valid entries in {@link __interp_recorded} */
395
+ __interp_recorded_count = 0;
396
+
397
+ constructor() {
398
+ super();
399
+
400
+ /**
401
+ * Reusable decode buffer for restoring interpolated snapshots.
402
+ * @private
403
+ * @type {BinaryBuffer}
404
+ */
405
+ this.__interp_scratch = new BinaryBuffer();
406
+ this.__interp_scratch.fromArrayBuffer(new ArrayBuffer(256));
407
+
408
+ /**
409
+ * Body indices snapshotted by the LAST {@link __interp_record} i.e. the awake
410
+ * Interpolated set as it was at the end of the previous tick. {@link __interp_restore}
411
+ * iterates THIS set (not the current awake set) so a body that slept on the previous
412
+ * tick recorded before it slept, but absent from the current awake set — is still
413
+ * restored off its render-polluted pose. Sparse: O(interpolated moving bodies).
414
+ * @private
415
+ * @type {Uint32Array}
416
+ */
417
+ this.__interp_recorded = new Uint32Array(16);
418
+ }
419
+
420
+ /**
421
+ * Symmetric layer/mask check + optional user callback. Called per
422
+ * candidate pair during broadphase. Returns `true` to accept the pair.
423
+ *
424
+ * Layer/mask rule: pair (A, B) collides iff
425
+ * `(A.layer & B.mask) !== 0 && (B.layer & A.mask) !== 0`.
426
+ * The user's contact-filter callback is consulted only for pairs that
427
+ * pass the bitmask test (cheap gate first).
428
+ *
429
+ * @private
430
+ * @param {number} idA packed body id
431
+ * @param {number} idB packed body id
432
+ * @returns {boolean}
433
+ */
434
+ __pair_filter(idA, idB) {
435
+ const idxA = body_id_index(idA);
436
+ const idxB = body_id_index(idB);
437
+
438
+ const rbA = this.__bodies[idxA];
439
+ const rbB = this.__bodies[idxB];
440
+
441
+ if (rbA === undefined || rbB === undefined) {
442
+ return false;
443
+ }
444
+
445
+ // Layer/mask gate (symmetric).
446
+ if (((rbA.layer & rbB.mask) | 0) === 0) {
447
+ return false;
448
+ }
449
+ if (((rbB.layer & rbA.mask) | 0) === 0) {
450
+ return false;
451
+ }
452
+
453
+ // User callback gate, if installed.
454
+ const fn = this.__contact_filter;
455
+ if (fn !== null) {
456
+ const entA = this.storage.entity_at(idxA);
457
+ const entB = this.storage.entity_at(idxB);
458
+ const colA = this.__primary_collider(idxA);
459
+ const colB = this.__primary_collider(idxB);
460
+ if (!fn(entA, entB, colA, colB)) return false;
461
+ }
462
+ return true;
463
+ }
464
+
465
+ /**
466
+ * First attached collider of a body, or `null` if none. Used for
467
+ * body-level checks that aren't yet per-collider: the sensor flag, the
468
+ * concave-shape dispatch flag, and the contact-filter callback's
469
+ * `colliderA` / `colliderB` arguments. Contact **materials** (friction /
470
+ * restitution) are NOT read here the narrowphase tracks the specific
471
+ * source collider on each side of every contact and stamps the combined
472
+ * coefficients into the manifold per contact, so mixed-material compound
473
+ * bodies are accurate. (Per-contact source colliders for the filter
474
+ * callback / sensor flag remain a follow-up.)
475
+ *
476
+ * @private
477
+ * @param {number} body_idx
478
+ * @returns {Collider|null}
479
+ */
480
+ __primary_collider(body_idx) {
481
+ const list = this.__body_collider_lists[body_idx];
482
+ return (list !== undefined && list.length > 0) ? list[0].collider : null;
483
+ }
484
+
485
+ /**
486
+ * Resize {@link __pseudo_velocity} to cover every live body slot and
487
+ * zero its contents. Called at the top of each fixedUpdate so the
488
+ * split-impulse position pass starts from a clean state — bodies the
489
+ * solver doesn't touch contribute zero pseudo-velocity to
490
+ * {@link integrate_position}.
491
+ *
492
+ * Doubles on growth like the other physics scratches; never shrinks.
493
+ *
494
+ * @private
495
+ */
496
+ __reset_pseudo_velocity() {
497
+ const required = this.storage.high_water_mark * 6;
498
+ if (this.__pseudo_velocity.length < required) {
499
+ this.__pseudo_velocity = new Float64Array(required * 2);
500
+ } else if (required > 0) {
501
+ this.__pseudo_velocity.fill(0, 0, required);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Replace the world gravity vector. Effective on the next step.
507
+ * @param {Vector3|{x:number,y:number,z:number}} v
508
+ */
509
+ setGravity(v) {
510
+ this.gravity.set(v.x, v.y, v.z);
511
+ }
512
+
513
+ /**
514
+ * Install (or remove with `null`) the contact filter callback.
515
+ * @param {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null} fn
516
+ */
517
+ setContactFilter(fn) {
518
+ this.__contact_filter = fn;
519
+ }
520
+
521
+ /**
522
+ * @returns {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null}
523
+ */
524
+ getContactFilter() {
525
+ return this.__contact_filter;
526
+ }
527
+
528
+ /**
529
+ * @private
530
+ * @param {RigidBody} rb
531
+ * @param {Collider} collider
532
+ * @param {Transform64} transform
533
+ * @returns {number} BVH node id
534
+ */
535
+ __insert_into_broadphase(rb, collider, transform) {
536
+ const shape = collider.shape;
537
+
538
+ assert.notNull(shape, 'Collider.shape must be set before attaching');
539
+
540
+ shape.compute_bounding_box(scratch_local_aabb);
541
+
542
+ // read as scalars: the view accessors allocate, and this runs per body
543
+
544
+ aabb3_transform_oriented(
545
+ scratch_world_aabb, 0,
546
+ scratch_local_aabb[0], scratch_local_aabb[1], scratch_local_aabb[2],
547
+ scratch_local_aabb[3], scratch_local_aabb[4], scratch_local_aabb[5],
548
+ transform.translation_x, transform.translation_y, transform.translation_z,
549
+ transform.rotation_x, transform.rotation_y, transform.rotation_z, transform.rotation_w
550
+ );
551
+
552
+ const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
553
+ const node = bvh.allocate_node();
554
+
555
+ bvh.node_set_aabb_primitive(
556
+ node,
557
+ scratch_world_aabb[0], scratch_world_aabb[1], scratch_world_aabb[2],
558
+ scratch_world_aabb[3], scratch_world_aabb[4], scratch_world_aabb[5]
559
+ );
560
+ bvh.node_set_user_data(node, rb._bodyId);
561
+ bvh.insert_leaf(node);
562
+
563
+ return node;
564
+ }
565
+
566
+ /**
567
+ * @private
568
+ * @param {RigidBody} rb
569
+ * @param {number} node
570
+ */
571
+ __remove_from_broadphase(rb, node) {
572
+ const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
573
+ bvh.remove_leaf(node);
574
+ bvh.release_node(node);
575
+ }
576
+
577
+ /**
578
+ * Wake every sleeping dynamic body whose broadphase leaf overlaps the
579
+ * given leaf's AABB. Called BEFORE removing a NON-Dynamic body's leaf
580
+ * (unlink / collider detach): statics and kinematics join no sleep group,
581
+ * their sleepers' manifolds are dormant-frozen, and the broadphase never
582
+ * revisits sleeping bodies — so without this, a pile resting on the
583
+ * removed support would sleep in mid-air forever.
584
+ *
585
+ * Dynamic removals don't need it: a sleeping pile and its dynamic support
586
+ * slept as one island and share a sleep group, which unlink already
587
+ * dissolves by waking the group.
588
+ *
589
+ * @private
590
+ * @param {BVH} bvh tree currently holding `node`
591
+ * @param {number} node leaf about to be removed
592
+ */
593
+ __wake_sleepers_overlapping_leaf(bvh, node) {
594
+ bvh.node_get_aabb(node, scratch_world_aabb);
595
+
596
+ let n;
597
+ for (;;) {
598
+ n = bvh_query_user_data_intersects_aabb(scratch_wake_candidates, 0, this.dynamicBvh, scratch_world_aabb);
599
+ if (n <= scratch_wake_candidates.length) break;
600
+ // TRUE count returned, overflow writes dropped — grow and re-query.
601
+ scratch_wake_candidates = new Uint32Array(Math.max(n, scratch_wake_candidates.length * 2));
602
+ }
603
+
604
+ for (let i = 0; i < n; i++) {
605
+ const body_id = scratch_wake_candidates[i];
606
+ if (!this.storage.is_valid(body_id)) continue;
607
+ const rb = this.__bodies[body_id_index(body_id)];
608
+ if (rb === undefined) continue;
609
+ if (rb.sleepState !== SleepState.Sleeping) continue;
610
+ this.__wake_body(rb);
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Lifecycle entry: invoked when an entity gains the (RigidBody, Transform64)
616
+ * tuple. Allocates the body's slot in storage and seeds an empty collider
617
+ * list. Colliders are attached separately via {@link attach_collider} (the
618
+ * paired {@link ColliderObserverSystem} drives this from the dataset; in
619
+ * tests, call manually).
620
+ *
621
+ * @param {RigidBody} rigidBody
622
+ * @param {Transform64} transform
623
+ * @param {number} entity
624
+ */
625
+ link(rigidBody, transform, entity) {
626
+ const packed = this.storage.allocate(entity);
627
+ rigidBody._bodyId = packed;
628
+
629
+ const index = body_id_index(packed);
630
+ this.storage.set_kind(index, rigidBody.kind);
631
+ this.storage.set_flags(index, rigidBody.flags);
632
+
633
+ this.__bodies[index] = rigidBody;
634
+ this.__transforms[index] = transform;
635
+ this.__body_collider_lists[index] = [];
636
+
637
+ // The component's sleep fields are AUTHORITATIVE persistent state
638
+ // (see RigidBody.sleepState): a body restored from a snapshot — or
639
+ // re-linked after an unlink — resumes exactly as drowsy or asleep
640
+ // as it was, instead of being forced awake to re-settle. That keeps
641
+ // a reconstructed world on the original's wake/sleep schedule (the
642
+ // reconstruction-determinism contract). A freshly-constructed
643
+ // component defaults to Awake, so plain spawns are unchanged.
644
+ //
645
+ // A restored sleeper arrives as a sleep-group SINGLETON (the group
646
+ // links are world-instance-tied body indices, reset here); group
647
+ // membership is re-established by the contact-state import or, for
648
+ // a body slept in isolation, is already correct.
649
+ rigidBody.sleep_group_next = -1;
650
+ rigidBody.sleep_group_prev = -1;
651
+
652
+ // Static bodies do not need to live in the active list — they never move.
653
+ if (rigidBody.kind === BodyKind.Static
654
+ || (rigidBody.kind === BodyKind.Dynamic && rigidBody.sleepState === SleepState.Sleeping)) {
655
+ this.storage.mark_sleeping(index);
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Detach every collider attached to this body, free its slot, and clear
661
+ * the side-tables.
662
+ *
663
+ * @param {RigidBody} rigidBody
664
+ * @param {Transform64} transform
665
+ * @param {number} entity
666
+ */
667
+ unlink(rigidBody, transform, entity) {
668
+ const packed = rigidBody._bodyId;
669
+
670
+ assert.equal(this.storage.is_valid(packed), true, 'unlink: stale or absent body id');
671
+
672
+ // If the body is sleeping inside a multi-member sleep group, dissolve
673
+ // the group by waking every member first. Otherwise the surviving
674
+ // members would hold dangling indices in their sleep-group chain
675
+ // pointing at a slot that has just been freed.
676
+ if (rigidBody.sleep_group_next !== -1) {
677
+ this.__wake_body(rigidBody);
678
+ }
679
+
680
+ const index = body_id_index(packed);
681
+ const list = this.__body_collider_lists[index];
682
+ if (list !== undefined) {
683
+ // A removed static / kinematic body may have been the support
684
+ // under sleeping bodies — wake them before the leaves disappear
685
+ // (their AABBs are the query keys).
686
+ const wake_neighbours = rigidBody.kind !== BodyKind.Dynamic;
687
+ const bvh = rigidBody.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
688
+ for (let i = 0; i < list.length; i++) {
689
+ const entry = list[i];
690
+ if (entry.bvhNode !== COLLIDER_UNBOUND) {
691
+ if (wake_neighbours) {
692
+ this.__wake_sleepers_overlapping_leaf(bvh, entry.bvhNode);
693
+ }
694
+ this.__remove_from_broadphase(rigidBody, entry.bvhNode);
695
+ }
696
+ entry.collider._bvhNode = COLLIDER_UNBOUND;
697
+ entry.collider._bodyId = -1;
698
+ }
699
+ }
700
+
701
+ this.__bodies[index] = undefined;
702
+ this.__transforms[index] = undefined;
703
+ this.__body_collider_lists[index] = undefined;
704
+
705
+ // Purge joints referencing the freed body. The solver already skips
706
+ // them via the generation-checked is_valid, but the 8-bit generation
707
+ // wraps after 256 reuse cycles of one slot (~seconds under a pooled
708
+ // spawner) — at which point a stale joint handle ALIASES an unrelated
709
+ // new body and silently reactivates against it. Removing the joint at
710
+ // the source closes the ABA window outright.
711
+ const joints = this.__joints;
712
+ for (let i = 0; i < joints.length; i++) {
713
+ const joint = joints[i];
714
+ if (joint === undefined || joint === null) continue;
715
+ if (joint._bodyIdA === packed || joint._bodyIdB === packed) {
716
+ this.unlink_joint(joint);
717
+ }
718
+ }
719
+
720
+ this.storage.free(packed);
721
+ rigidBody._bodyId = RIGID_BODY_UNALLOCATED;
722
+ }
723
+
724
+ /**
725
+ * Attach a collider to an existing body. The collider can live on the
726
+ * same entity as the body (single-collider body) or on a child entity
727
+ * (compound body). The world transform passed here is the collider's
728
+ * own — for a same-entity collider it is the body's Transform; for a
729
+ * child collider it is the child entity's Transform.
730
+ *
731
+ * Idempotent — re-attaching the same collider is a no-op.
732
+ *
733
+ * @param {number} body_entity entity that owns the body
734
+ * @param {Collider} collider
735
+ * @param {Transform64} transform world transform of the collider
736
+ * @param {number} [collider_entity] entity owning the collider (defaults to body_entity)
737
+ */
738
+ attach_collider(body_entity, collider, transform, collider_entity = body_entity) {
739
+ // Find the body by walking the storage entity table. The body must
740
+ // have been allocated via `link` before any colliders are attached.
741
+ const body_index = this.__find_body_index_by_entity(body_entity);
742
+ assert.notEqual(body_index, -1, `attach_collider: no body found for entity ${body_entity}`);
743
+
744
+ const rb = this.__bodies[body_index];
745
+ // Idempotent: skip if collider already attached.
746
+ if (collider._bvhNode !== COLLIDER_UNBOUND) return;
747
+
748
+ const node = this.__insert_into_broadphase(rb, collider, transform);
749
+
750
+ collider._bvhNode = node;
751
+ collider._bodyId = rb._bodyId;
752
+ this.__body_collider_lists[body_index].push({
753
+ collider, transform, entity: collider_entity, bvhNode: node,
754
+ });
755
+ }
756
+
757
+ /**
758
+ * Reverse of {@link attach_collider}. Idempotent.
759
+ *
760
+ * @param {number} body_entity
761
+ * @param {Collider} collider
762
+ */
763
+ detach_collider(body_entity, collider) {
764
+ if (collider._bvhNode === COLLIDER_UNBOUND) return;
765
+
766
+ const body_index = this.__find_body_index_by_entity(body_entity);
767
+ if (body_index === -1) return;
768
+
769
+ const rb = this.__bodies[body_index];
770
+ // Same rule as unlink: removing a static / kinematic leaf may pull
771
+ // the support from under sleeping bodies — wake them first.
772
+ if (rb.kind !== BodyKind.Dynamic) {
773
+ const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
774
+ this.__wake_sleepers_overlapping_leaf(bvh, collider._bvhNode);
775
+ } else if (rb.sleepState === SleepState.Sleeping) {
776
+ // A sleeping DYNAMIC support: unlike unlink, detach_collider dissolves no sleep group,
777
+ // so removing the collider would strand the body (and, via the sleep-group chain that
778
+ // __wake_body walks, the whole pile resting on it) asleep and frozen in mid-air. Keyed
779
+ // on sleep state, not sleep_group_next, so a LONE sleeper (group === -1) wakes too.
780
+ this.__wake_body(rb);
781
+ }
782
+ this.__remove_from_broadphase(rb, collider._bvhNode);
783
+
784
+ const list = this.__body_collider_lists[body_index];
785
+ for (let i = 0; i < list.length; i++) {
786
+ if (list[i].collider === collider) {
787
+ list.splice(i, 1);
788
+ break;
789
+ }
790
+ }
791
+
792
+ collider._bvhNode = COLLIDER_UNBOUND;
793
+ collider._bodyId = -1;
794
+ }
795
+
796
+ /**
797
+ * Resolve an entity to its body index, or -1 if no live body owns it.
798
+ * O(1) via {@link BodyStorage#index_of_entity}'s entity → index map — used
799
+ * on the collider attach / detach and joint link paths.
800
+ *
801
+ * @private
802
+ * @param {number} entity
803
+ * @returns {number} body index or -1
804
+ */
805
+ __find_body_index_by_entity(entity) {
806
+ return this.storage.index_of_entity(entity);
807
+ }
808
+
809
+ /**
810
+ * Register a {@link Joint} (6-DOF constraint). Resolves the joint's
811
+ * entities to packed body ids and adds it to the active set, where it is
812
+ * solved alongside contacts in the TGS substep loop. Body A must be a
813
+ * linked body; body B is either a linked body or {@link JOINT_WORLD}
814
+ * (anchoring A to a fixed world point — `localAnchorB` is then a world
815
+ * coordinate).
816
+ *
817
+ * @param {Joint} joint
818
+ */
819
+ link_joint(joint) {
820
+ const idxA = this.__find_body_index_by_entity(joint.entityA);
821
+ assert.notEqual(idxA, -1, `link_joint: no body for entityA ${joint.entityA}`);
822
+ joint._bodyIdA = this.__bodies[idxA]._bodyId;
823
+
824
+ if (joint.entityB === JOINT_WORLD) {
825
+ joint._bodyIdB = JOINT_WORLD;
826
+ } else {
827
+ const idxB = this.__find_body_index_by_entity(joint.entityB);
828
+ assert.notEqual(idxB, -1, `link_joint: no body for entityB ${joint.entityB}`);
829
+ joint._bodyIdB = this.__bodies[idxB]._bodyId;
830
+ }
831
+
832
+ // A (re-)linked joint starts cold: the accumulated warm-start impulse
833
+ // belongs to whatever configuration the joint solved BEFORE it was
834
+ // unlinked — replaying it against the new bodies is a phantom kick.
835
+ joint.dofImpulse.fill(0);
836
+
837
+ // Reuse a freed slot if available so joint ids stay dense-ish.
838
+ let id;
839
+ if (this.__joint_free.length > 0) {
840
+ id = this.__joint_free.pop();
841
+ } else {
842
+ id = this.__joints.length;
843
+ }
844
+ joint._jointId = id;
845
+ this.__joints[id] = joint;
846
+
847
+ // Insert at the upper bound of the joint's (entityA, entityB) key in
848
+ // the canonical solve-order list — equal keys keep link order.
849
+ const sorted = this.__joints_sorted;
850
+ let pos = sorted.length;
851
+ while (pos > 0) {
852
+ const prev = sorted[pos - 1];
853
+ if (prev.entityA < joint.entityA
854
+ || (prev.entityA === joint.entityA && prev.entityB <= joint.entityB)) {
855
+ break;
856
+ }
857
+ pos--;
858
+ }
859
+ sorted.splice(pos, 0, joint);
860
+ }
861
+
862
+ /**
863
+ * Remove a previously {@link link_joint}'d joint from the active set.
864
+ * Idempotent.
865
+ * @param {Joint} joint
866
+ */
867
+ unlink_joint(joint) {
868
+ const id = joint._jointId;
869
+ if (id === JOINT_UNALLOCATED || this.__joints[id] !== joint) return;
870
+ this.__joints[id] = undefined;
871
+ this.__joint_free.push(id);
872
+ joint._jointId = JOINT_UNALLOCATED;
873
+
874
+ const sorted = this.__joints_sorted;
875
+ const pos = sorted.indexOf(joint);
876
+ if (pos !== -1) sorted.splice(pos, 1);
877
+ }
878
+
879
+ /**
880
+ * Resolve a packed body id to its entity, or `-1` if the id is stale.
881
+ * @param {number} packed_body_id
882
+ * @returns {number}
883
+ */
884
+ entityOf(packed_body_id) {
885
+ if (!this.storage.is_valid(packed_body_id)) return -1;
886
+ return this.storage.entity_at(body_id_index(packed_body_id));
887
+ }
888
+
889
+ /**
890
+ * Number of live bodies (regardless of awake/sleeping state).
891
+ * @returns {number}
892
+ */
893
+ get bodyCount() {
894
+ return this.storage.size;
895
+ }
896
+
897
+ /**
898
+ * Apply an instantaneous change of momentum at the body's centre of mass.
899
+ * Linear-only — see {@link applyImpulseAt} for an off-centre impulse that
900
+ * also produces angular response.
901
+ *
902
+ * Wakes the body if it is asleep.
903
+ *
904
+ * @param {RigidBody} rigidBody
905
+ * @param {Vector3|{x:number,y:number,z:number}} impulse
906
+ */
907
+ applyImpulse(rigidBody, impulse) {
908
+ if (rigidBody.kind !== BodyKind.Dynamic) {
909
+ return;
910
+ }
911
+ const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
912
+ rigidBody.linearVelocity.addScaled(impulse, inv_m);
913
+
914
+ this.__wake_body(rigidBody);
915
+ }
916
+
917
+ /**
918
+ * Apply an instantaneous change of momentum at a specific world-space point.
919
+ * Off-centre impulses produce both linear (Δv = P/m) and angular
920
+ * (Δω = I_w⁻¹·(r × P)) response.
921
+ *
922
+ * Wakes the body if it is asleep.
923
+ *
924
+ * @param {RigidBody} rigidBody
925
+ * @param {Transform64} transform body's current world Transform (used for r and I_w)
926
+ * @param {Vector3|{x:number,y:number,z:number}} impulse
927
+ * @param {Vector3|{x:number,y:number,z:number}} worldPoint
928
+ */
929
+ applyImpulseAt(rigidBody, transform, impulse, worldPoint) {
930
+ if (rigidBody.kind !== BodyKind.Dynamic) {
931
+ return;
932
+ }
933
+ const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
934
+
935
+ rigidBody.linearVelocity.set(
936
+ rigidBody.linearVelocity.x + impulse.x * inv_m,
937
+ rigidBody.linearVelocity.y + impulse.y * inv_m,
938
+ rigidBody.linearVelocity.z + impulse.z * inv_m
939
+ );
940
+
941
+ // read by index: the world point is commonly a transform translation view, which has no .x
942
+ const rx = worldPoint[0] - transform.translation_x;
943
+ const ry = worldPoint[1] - transform.translation_y;
944
+ const rz = worldPoint[2] - transform.translation_z;
945
+
946
+ // Δω = I_w⁻¹ · (r × P)
947
+ const tx = ry * impulse.z - rz * impulse.y;
948
+ const ty = rz * impulse.x - rx * impulse.z;
949
+ const tz = rx * impulse.y - ry * impulse.x;
950
+
951
+ // LockRot* masks the BODY-LOCAL inverse-inertia diagonal here exactly
952
+ // as the solver gather does, so a user impulse cannot spin a locked
953
+ // DOF either.
954
+ const flags = rigidBody.flags;
955
+ const ii = rigidBody.inverseInertiaLocal;
956
+ scratch_locked_inertia.x = (flags & RigidBodyFlags.LockRotX) !== 0 ? 0 : ii.x;
957
+ scratch_locked_inertia.y = (flags & RigidBodyFlags.LockRotY) !== 0 ? 0 : ii.y;
958
+ scratch_locked_inertia.z = (flags & RigidBodyFlags.LockRotZ) !== 0 ? 0 : ii.z;
959
+ world_inverse_inertia_apply(scratch_angular_delta, 0, scratch_locked_inertia, transform.rotation, tx, ty, tz);
960
+
961
+ rigidBody.angularVelocity.set(
962
+ rigidBody.angularVelocity.x + scratch_angular_delta[0],
963
+ rigidBody.angularVelocity.y + scratch_angular_delta[1],
964
+ rigidBody.angularVelocity.z + scratch_angular_delta[2]
965
+ );
966
+
967
+ this.__wake_body(rigidBody);
968
+ }
969
+
970
+ /**
971
+ * Accumulate a continuous torque (world-space) for integration on the
972
+ * next fixedUpdate. Pairs with {@link applyForce} for the rotational case.
973
+ *
974
+ * Wakes the body if asleep.
975
+ *
976
+ * @param {RigidBody} rigidBody
977
+ * @param {Vector3|{x:number,y:number,z:number}} torque
978
+ */
979
+ applyTorque(rigidBody, torque) {
980
+ if (rigidBody.kind !== BodyKind.Dynamic) {
981
+ return;
982
+ }
983
+ rigidBody.accumulatedTorque.set(
984
+ rigidBody.accumulatedTorque.x + torque.x,
985
+ rigidBody.accumulatedTorque.y + torque.y,
986
+ rigidBody.accumulatedTorque.z + torque.z
987
+ );
988
+ this.__wake_body(rigidBody);
989
+ }
990
+
991
+ /**
992
+ * Apply a continuous force at a specific world-space point. The force
993
+ * generates both a linear acceleration (F/m) and a torque (r × F) about
994
+ * the body's centre of mass.
995
+ *
996
+ * Wakes the body if asleep.
997
+ *
998
+ * @param {RigidBody} rigidBody
999
+ * @param {Transform64} transform body's current world Transform
1000
+ * @param {Vector3|{x:number,y:number,z:number}} force
1001
+ * @param {Vector3|{x:number,y:number,z:number}} worldPoint
1002
+ */
1003
+ applyForceAt(rigidBody, transform, force, worldPoint) {
1004
+ if (rigidBody.kind !== BodyKind.Dynamic) {
1005
+ return;
1006
+ }
1007
+ rigidBody.accumulatedForce.set(
1008
+ rigidBody.accumulatedForce.x + force.x,
1009
+ rigidBody.accumulatedForce.y + force.y,
1010
+ rigidBody.accumulatedForce.z + force.z
1011
+ );
1012
+
1013
+ // read by index: the world point is commonly a transform translation view, which has no .x
1014
+ const rx = worldPoint[0] - transform.translation_x;
1015
+ const ry = worldPoint[1] - transform.translation_y;
1016
+ const rz = worldPoint[2] - transform.translation_z;
1017
+
1018
+ rigidBody.accumulatedTorque.set(
1019
+ rigidBody.accumulatedTorque.x + (ry * force.z - rz * force.y),
1020
+ rigidBody.accumulatedTorque.y + (rz * force.x - rx * force.z),
1021
+ rigidBody.accumulatedTorque.z + (rx * force.y - ry * force.x)
1022
+ );
1023
+
1024
+ this.__wake_body(rigidBody);
1025
+ }
1026
+
1027
+ /**
1028
+ * Accumulate a continuous force to be integrated next fixedUpdate step.
1029
+ * Wakes the body if asleep.
1030
+ *
1031
+ * @param {RigidBody} rigidBody
1032
+ * @param {Vector3|{x:number,y:number,z:number}} force
1033
+ */
1034
+ applyForce(rigidBody, force) {
1035
+ if (rigidBody.kind !== BodyKind.Dynamic) {
1036
+ return;
1037
+ }
1038
+ rigidBody.accumulatedForce.add(force);
1039
+ this.__wake_body(rigidBody);
1040
+ }
1041
+
1042
+ /**
1043
+ * Replace the linear velocity. Wakes the body if asleep.
1044
+ *
1045
+ * @param {RigidBody} rigidBody
1046
+ * @param {Vector3|{x:number,y:number,z:number}} v
1047
+ */
1048
+ setLinearVelocity(rigidBody, v) {
1049
+ rigidBody.linearVelocity.copy(v);
1050
+ if (rigidBody.kind === BodyKind.Dynamic) {
1051
+ this.__wake_body(rigidBody);
1052
+ }
1053
+ }
1054
+
1055
+ /**
1056
+ * Replace the angular velocity. Wakes the body if asleep.
1057
+ *
1058
+ * Writing `rigidBody.angularVelocity` directly does not wake a sleeping
1059
+ * body, so the spin is silently discarded until something else wakes it.
1060
+ *
1061
+ * @param {RigidBody} rigidBody
1062
+ * @param {Vector3|{x:number,y:number,z:number}} v
1063
+ */
1064
+ setAngularVelocity(rigidBody, v) {
1065
+ rigidBody.angularVelocity.copy(v);
1066
+ if (rigidBody.kind === BodyKind.Dynamic) {
1067
+ this.__wake_body(rigidBody);
1068
+ }
1069
+ }
1070
+
1071
+ /**
1072
+ * Teleport a body to a new pose, bypassing integration: writes the body's
1073
+ * Transform directly and wakes it. For an interpolated body this also flags a
1074
+ * render `snap` on its {@link Interpolated} component, so the producer keeps
1075
+ * this pose (rather than restoring the previous tick over it) and the
1076
+ * renderer shows the new pose without sliding across the jump.
1077
+ *
1078
+ * This is the authoritative way to reposition an interpolated body a raw
1079
+ * `Transform` write would be undone by the per-step restore. Velocity is left
1080
+ * as-is; zero it via {@link setLinearVelocity} if the teleport should also
1081
+ * stop the body.
1082
+ *
1083
+ * Works for STATIC bodies too: their broadphase leaves are re-homed to
1084
+ * the new pose, and sleepers at both the old and the new location are
1085
+ * woken — a moved support drops what rested on it, and whatever it moved
1086
+ * into must react.
1087
+ *
1088
+ * @param {RigidBody} rigidBody
1089
+ * @param {Vector3|{x:number,y:number,z:number}} position world position
1090
+ * @param {Quaternion|{x:number,y:number,z:number,w:number}} rotation world unit-quaternion rotation
1091
+ */
1092
+ setPose(rigidBody, position, rotation) {
1093
+ const idx = body_id_index(rigidBody._bodyId);
1094
+ const transform = this.__transforms[idx];
1095
+ if (transform === undefined) {
1096
+ return;
1097
+ }
1098
+
1099
+ transform.setTranslation(position.x, position.y, position.z);
1100
+ transform.setRotation(rotation.x, rotation.y, rotation.z, rotation.w);
1101
+
1102
+ if (rigidBody.kind !== BodyKind.Dynamic) {
1103
+ // Static AND Kinematic: the body itself never sleeps and __wake_body is a no-op for it,
1104
+ // so teleporting a support must instead wake what rested on it (old pose) and what it
1105
+ // moved into (new pose) a zero-velocity teleport never trips __wake_pairs' velocity-
1106
+ // based parked exemption. A moved static also keeps a stale broadphase AABB forever
1107
+ // (stage 2 refits awake bodies only), so re-home its leaves; doing the same for a
1108
+ // kinematic is harmless (stage 2 would refit it anyway). The leaves of a static live in
1109
+ // staticBvh, a kinematic's in dynamicBvh — pick the tree that actually holds them.
1110
+ const bvh = rigidBody.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
1111
+ const list = this.__body_collider_lists[idx];
1112
+ if (list !== undefined) {
1113
+ for (let i = 0; i < list.length; i++) {
1114
+ const entry = list[i];
1115
+ if (entry.bvhNode === COLLIDER_UNBOUND) continue;
1116
+ this.__wake_sleepers_overlapping_leaf(bvh, entry.bvhNode);
1117
+ this.__remove_from_broadphase(rigidBody, entry.bvhNode);
1118
+ const node = this.__insert_into_broadphase(rigidBody, entry.collider, entry.transform);
1119
+ entry.bvhNode = node;
1120
+ entry.collider._bvhNode = node;
1121
+ this.__wake_sleepers_overlapping_leaf(bvh, node);
1122
+ }
1123
+ }
1124
+ } else {
1125
+ this.__wake_body(rigidBody);
1126
+ }
1127
+
1128
+ // Flag a snap on the body's Interpolated component, if any, so the
1129
+ // interpolation producer (restore) and consumer (blend) treat this pose
1130
+ // as authoritative this frame instead of interpolating across the jump.
1131
+ //
1132
+ // KNOWN BUG (deferred): `snap` is a single flag with two independent consumers
1133
+ // __interp_restore (skips restore) and InterpolationSystem.update (skips blend AND
1134
+ // CLEARS it). The renderer's update runs every render cycle while a fixed step only
1135
+ // runs when the accumulator crosses the step size, so a render frame landing between
1136
+ // this setPose and the next fixedUpdate clears `snap` before __interp_restore reads
1137
+ // it restore then overwrites the teleported Transform with the pre-teleport tick's
1138
+ // snapshot and the teleport is silently reverted in the SIMULATION. This only bites
1139
+ // once InterpolationSystem is wired (the app does not wire it yet). The robust fix is
1140
+ // to make the teleport authoritative independent of the shared flag — either overwrite
1141
+ // the log's latest-tick record for this body (needs new InterpolationLog API), or give
1142
+ // the producer and consumer independent snap state each clears itself.
1143
+ const em = this.entityManager;
1144
+ if (em !== null && em !== undefined) {
1145
+ const dataset = em.dataset;
1146
+ if (dataset !== null && dataset !== undefined) {
1147
+ const entity = this.storage.entity_at(idx);
1148
+ const interpolated = dataset.getComponent(entity, Interpolated);
1149
+ if (interpolated !== undefined && interpolated !== null) {
1150
+ interpolated.snap = true;
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ /**
1157
+ * Force the body awake. Static bodies are ignored.
1158
+ * @param {RigidBody} rigidBody
1159
+ */
1160
+ wake(rigidBody) {
1161
+ this.__wake_body(rigidBody);
1162
+ }
1163
+
1164
+ /**
1165
+ * Force the body asleep. Dynamic bodies will not re-enter the active list
1166
+ * until a wake event occurs.
1167
+ * @param {RigidBody} rigidBody
1168
+ */
1169
+ sleep(rigidBody) {
1170
+ if (rigidBody.kind !== BodyKind.Dynamic) {
1171
+ return;
1172
+ }
1173
+ if (rigidBody.sleepState === SleepState.Sleeping) {
1174
+ return;
1175
+ }
1176
+ // An unlinked (or stale) body has no storage slot — indexing with its
1177
+ // dead id would corrupt the awake-list bookkeeping of whatever body
1178
+ // now owns that index.
1179
+ if (!this.storage.is_valid(rigidBody._bodyId)) {
1180
+ return;
1181
+ }
1182
+ rigidBody.sleepState = SleepState.Sleeping;
1183
+ const index = body_id_index(rigidBody._bodyId);
1184
+ this.storage.mark_sleeping(index);
1185
+ }
1186
+
1187
+ /**
1188
+ * Wake a body and atomically wake every other body it was last sleeping
1189
+ * with (its "sleep group"). Sleep groups are circular doubly-linked lists
1190
+ * threaded through every member of an island when it sleeps atomically;
1191
+ * waking any one member walks the chain and wakes the rest in the same
1192
+ * call.
1193
+ *
1194
+ * Without this, a 100-block stack hit at the base would wake one block
1195
+ * per frame as the broadphase propagated awareness up the stack — a
1196
+ * visible ~1.6 s wave at 60 fps. Atomic wake eliminates the wave.
1197
+ *
1198
+ * No-op for non-dynamic bodies. Idempotent for already-awake bodies.
1199
+ * @private
1200
+ * @param {RigidBody} rb
1201
+ */
1202
+ __wake_body(rb) {
1203
+ if (rb.kind !== BodyKind.Dynamic) return;
1204
+ if (rb.sleepState === SleepState.Awake) return;
1205
+ // Same stale-id guard as sleep(): an unlinked body's dead id must not
1206
+ // index into (and corrupt) another body's awake-list slot.
1207
+ if (!this.storage.is_valid(rb._bodyId)) return;
1208
+ const index = body_id_index(rb._bodyId);
1209
+
1210
+ // Remember the next-in-chain before clearing the body's own pointers;
1211
+ // the rest of the group is reached by walking forward from there.
1212
+ const start_next = rb.sleep_group_next;
1213
+
1214
+ rb.sleepState = SleepState.Awake;
1215
+ rb.sleep_timer = 0;
1216
+ rb.sleep_group_next = -1;
1217
+ rb.sleep_group_prev = -1;
1218
+ this.storage.mark_awake(index);
1219
+
1220
+ if (start_next === -1 || start_next === index) return;
1221
+
1222
+ // Walk the (now-broken) chain forward until we loop back. The chain
1223
+ // is circular so we know when to stop; defensive `-1` guards against
1224
+ // corruption from a body being unlinked mid-sleep-group.
1225
+ let cur = start_next;
1226
+ while (cur !== -1 && cur !== index) {
1227
+ const cur_rb = this.__bodies[cur];
1228
+ if (cur_rb === undefined) break;
1229
+ const nxt = cur_rb.sleep_group_next;
1230
+ cur_rb.sleepState = SleepState.Awake;
1231
+ cur_rb.sleep_timer = 0;
1232
+ cur_rb.sleep_group_next = -1;
1233
+ cur_rb.sleep_group_prev = -1;
1234
+ this.storage.mark_awake(cur);
1235
+ cur = nxt;
1236
+ }
1237
+ }
1238
+
1239
+ /**
1240
+ * Atomically put every body in a contiguous range of island members to
1241
+ * sleep. Members are threaded into a circular doubly-linked list so any
1242
+ * future `wake` on any member walks the chain and revives them all.
1243
+ *
1244
+ * Velocities are zeroed because the body is by definition at rest at
1245
+ * this point the alternative (storing residual velocities for "softer"
1246
+ * wake) is what Bullet does, but for a deterministic game-physics target
1247
+ * fully resetting is simpler and avoids drift while sleeping.
1248
+ *
1249
+ * @private
1250
+ * @param {Uint32Array} member_array view (or full array) of body indices
1251
+ * @param {number} start
1252
+ * @param {number} end
1253
+ */
1254
+ __atomic_sleep_island_range(member_array, start, end) {
1255
+ const count = end - start;
1256
+ if (count === 0) return;
1257
+ const bodies = this.__bodies;
1258
+ const storage = this.storage;
1259
+
1260
+ if (count === 1) {
1261
+ const idx = member_array[start];
1262
+ const rb = bodies[idx];
1263
+ if (rb === undefined) return;
1264
+ rb.sleep_group_next = -1;
1265
+ rb.sleep_group_prev = -1;
1266
+ rb.sleepState = SleepState.Sleeping;
1267
+ rb.linearVelocity[0] = 0;
1268
+ rb.linearVelocity[1] = 0;
1269
+ rb.linearVelocity[2] = 0;
1270
+ rb.angularVelocity[0] = 0;
1271
+ rb.angularVelocity[1] = 0;
1272
+ rb.angularVelocity[2] = 0;
1273
+ storage.mark_sleeping(idx);
1274
+ return;
1275
+ }
1276
+
1277
+ for (let i = 0; i < count; i++) {
1278
+ const idx = member_array[start + i];
1279
+ const rb = bodies[idx];
1280
+ if (rb === undefined) continue;
1281
+ const next_idx = member_array[start + ((i + 1) % count)];
1282
+ const prev_idx = member_array[start + ((i - 1 + count) % count)];
1283
+ rb.sleep_group_next = next_idx;
1284
+ rb.sleep_group_prev = prev_idx;
1285
+ rb.sleepState = SleepState.Sleeping;
1286
+ rb.linearVelocity[0] = 0;
1287
+ rb.linearVelocity[1] = 0;
1288
+ rb.linearVelocity[2] = 0;
1289
+ rb.angularVelocity[0] = 0;
1290
+ rb.angularVelocity[1] = 0;
1291
+ rb.angularVelocity[2] = 0;
1292
+ storage.mark_sleeping(idx);
1293
+ }
1294
+ }
1295
+
1296
+ /**
1297
+ * Raycast against both broadphase trees. Fills `result` with the nearest
1298
+ * hit and returns `true` on hit, `false` on miss.
1299
+ *
1300
+ * Resolves to the BVH leaf, not the shape: `result.t` is the distance to
1301
+ * the leaf's inflated AABB and `result.normal` is that AABB's face
1302
+ * normal. Exact for AABB-shaped colliders, approximate for the rest —
1303
+ * use {@link shapeCast} when you need the true surface.
1304
+ *
1305
+ * @param {Ray3} ray origin + unit direction + `tMax`
1306
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
1307
+ * @param {(entity:number, collider:Collider)=>boolean} [filter] defaults
1308
+ * to {@link returnTrue} (accept every candidate)
1309
+ * @returns {boolean}
1310
+ */
1311
+ raycast(ray, result, filter = returnTrue) {
1312
+ return raycast_query(this, ray, result, filter);
1313
+ }
1314
+
1315
+ /**
1316
+ * Sweep a convex shape along a ray and return the first body it hits.
1317
+ * The shape starts at `ray.origin` oriented by `rotation` and translates
1318
+ * along `ray.direction` for up to `ray.tMax` units.
1319
+ *
1320
+ * Unlike {@link raycast} this resolves against the real shapes, so `t` is
1321
+ * a true time of impact rather than a distance to a bounding box.
1322
+ *
1323
+ * @param {Ray3} ray origin + unit direction + `tMax`
1324
+ * @param {AbstractShape3D} shape
1325
+ * @param {{x:number,y:number,z:number,w:number}} rotation
1326
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
1327
+ * @param {(entity:number, collider:Collider)=>boolean} [filter]
1328
+ * @returns {boolean}
1329
+ */
1330
+ shapeCast(ray, shape, rotation, result, filter = returnTrue) {
1331
+ return shape_cast_query(this, ray, shape, rotation, result, filter);
1332
+ }
1333
+
1334
+ /**
1335
+ * Find every body whose collider overlaps the given convex shape at the
1336
+ * given world pose, without mutating the simulation. Intended for
1337
+ * kinematic / character controllers testing "would I collide if I moved
1338
+ * here?" before committing the move.
1339
+ *
1340
+ * Writes overlapping bodies' packed `body_id` values into `output` from
1341
+ * `output_offset`. Sizing the buffer is the caller's job: ids past its
1342
+ * end are dropped silently and the returned count caps at the space
1343
+ * available.
1344
+ *
1345
+ * `filter` runs before the shape test, which is the expensive part — use
1346
+ * it to skip the caller's own body, allies, sensors.
1347
+ *
1348
+ * The query shape must be convex; a concave one throws.
1349
+ *
1350
+ * @param {AbstractShape3D} shape convex query shape, in its local frame
1351
+ * @param {ArrayLike<number>} position world position of the query shape, as `[x, y, z]` -
1352
+ * which a Transform64's translation already is
1353
+ * @param {ArrayLike<number>} rotation world rotation as `[x, y, z, w]`, a unit quaternion
1354
+ * @param {Uint32Array|number[]} output buffer to receive body_ids
1355
+ * @param {number} output_offset starting index in output
1356
+ * @param {(entity:number, collider:Collider)=>boolean} [filter]
1357
+ * defaults to {@link returnTrue}
1358
+ * @returns {number} number of overlapping bodies written
1359
+ */
1360
+ overlap(shape, position, rotation, output, output_offset, filter = returnTrue) {
1361
+ return overlap_shape_query(this, shape, position, rotation, output, output_offset, filter);
1362
+ }
1363
+
1364
+ /**
1365
+ * Re-shape both broadphase trees so subsequent queries{@link raycast},
1366
+ * {@link shapeCast}, {@link overlap}, and the per-step broadphase — walk
1367
+ * fewer nodes.
1368
+ *
1369
+ * Call it once after a level's statics are linked. Statics keep the win
1370
+ * for good (~12% fewer nodes visited); the dynamic tree's fades within
1371
+ * seconds as bodies move, so repeat calls are not worth scheduling. Costs
1372
+ * ~20 ms for 4000 bodies — a load-time operation, not a per-frame one.
1373
+ *
1374
+ * @returns {void}
1375
+ */
1376
+ optimize() {
1377
+ // empty and single-leaf trees are a no-op inside the optimizer
1378
+ ebvh_optimize_treelet(this.staticBvh);
1379
+ ebvh_optimize_treelet(this.dynamicBvh);
1380
+ }
1381
+
1382
+ /**
1383
+ * Wake propagation through contact pairs. Runs BEFORE broadphase so a
1384
+ * body woken here participates in THIS frame's broadphase / narrowphase
1385
+ * / islands end to end — it never simulates a step without its support
1386
+ * contacts (free-fall penetration pop).
1387
+ *
1388
+ * Reads ONLY persistent manifold state (the slots whose pair the
1389
+ * broadphase touched last frame `prev_touched`), never the transient
1390
+ * pair list: manifold state is part of the engine's exportable contact
1391
+ * state, so a reconstructed engine wakes on exactly the original's
1392
+ * schedule.
1393
+ *
1394
+ * A sleeper wakes when:
1395
+ * - its pair holds REAL contacts (`contact_count > 0`), unless the
1396
+ * awake side is a PARKED non-Dynamic (a stationary kinematic platform
1397
+ * or a static sleepState of either is never updated — transmits no
1398
+ * motion; the moment it gets velocity, the riders wake); or
1399
+ * - the pair has no contacts yet but the awake side's velocity-swept
1400
+ * tight AABB just REACHED the sleeper's tight AABB (false→true edge
1401
+ * of {@link __swept_proximity}) one precautionary wake so an
1402
+ * incoming body never spends its first touching frame against a
1403
+ * non-integrating sleeper, over-embedding by v·dt. Edge-triggered on
1404
+ * a pure function of body state: pair CREATION time (the old
1405
+ * trigger) depends on broadphase fat-box history, which a
1406
+ * reconstructed engine does not share; and level-triggered proximity
1407
+ * would re-wake a pile next to a hovering body every frame. Slot
1408
+ * existence alone wakes nothing.
1409
+ * @private
1410
+ * @param {number} dt step size — extent of the predicate's velocity sweep
1411
+ */
1412
+ __wake_pairs(dt) {
1413
+ const bodies = this.__bodies;
1414
+ const storage = this.storage;
1415
+ const manifolds = this.manifolds;
1416
+ const live_count = manifolds.count;
1417
+ for (let i = 0; i < live_count; i++) {
1418
+ const slot = manifolds.live_at(i);
1419
+ // "Was in last frame's broadphase": acquire set touched, the
1420
+ // end-of-step advance rolled it to prev_touched. (A dormant
1421
+ // slot can carry a stale prev_touched from its sleep frame —
1422
+ // the both-asleep guard below skips it.)
1423
+ if (!manifolds.was_touched_prev(slot)) continue;
1424
+
1425
+ const idA = manifolds.bodyA(slot);
1426
+ const idB = manifolds.bodyB(slot);
1427
+ // Stale-slot guards: a body may have been unlinked since (and
1428
+ // its index reused by an unrelated body).
1429
+ if (!storage.is_valid(idA) || !storage.is_valid(idB)) continue;
1430
+ const idxA = body_id_index(idA);
1431
+ const idxB = body_id_index(idB);
1432
+ const a = bodies[idxA];
1433
+ const b = bodies[idxB];
1434
+ if (a === undefined || b === undefined) continue;
1435
+
1436
+ const aSleep = a.sleepState === SleepState.Sleeping;
1437
+ const bSleep = b.sleepState === SleepState.Sleeping;
1438
+ if (aSleep === bSleep) continue; // both awake, or a dormant pair
1439
+
1440
+ // A sensor transmits no force and is excluded from sleep islands (is_sensor,
1441
+ // IslandBuilder, the contact solver). It must not wake a sleeper either — otherwise a
1442
+ // moving trigger volume re-wakes everything it overlaps every frame, holding whole
1443
+ // islands permanently awake. Skip the pair, mirroring IslandBuilder.__body_is_sensor.
1444
+ if (is_sensor(a, this.__primary_collider(idxA)) || is_sensor(b, this.__primary_collider(idxB))) {
1445
+ continue;
1446
+ }
1447
+
1448
+ if (manifolds.contact_count(slot) > 0) {
1449
+ const mover = aSleep ? b : a;
1450
+ if (mover.kind !== BodyKind.Dynamic) {
1451
+ const lv = mover.linearVelocity;
1452
+ const av = mover.angularVelocity;
1453
+ if (lv[0] === 0 && lv[1] === 0 && lv[2] === 0
1454
+ && av[0] === 0 && av[1] === 0 && av[2] === 0) {
1455
+ continue; // parked platform / static — transmits no motion
1456
+ }
1457
+ }
1458
+ this.__wake_body(aSleep ? a : b);
1459
+ } else {
1460
+ // No contacts: pre-contact precautionary wake on the
1461
+ // proximity EDGE. The predicate needs no parked exemption
1462
+ // a zero-velocity mover's swept box is its tight box, so it
1463
+ // only fires on actual tight-box overlap, once.
1464
+ const mover_idx = body_id_index(aSleep ? idB : idA);
1465
+ const sleeper_idx = body_id_index(aSleep ? idA : idB);
1466
+ const prox = this.__swept_proximity(mover_idx, sleeper_idx, dt);
1467
+ const had_prox = manifolds.is_prox(slot);
1468
+ manifolds.set_prox(slot, prox);
1469
+ if (prox && !had_prox) {
1470
+ this.__wake_body(aSleep ? a : b);
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ /**
1477
+ * Stage-0 pre-contact wake predicate: does any of `mover`'s collider
1478
+ * tight AABBs, swept directionally by the mover's linear velocity over
1479
+ * `dt`, overlap any of `sleeper`'s collider tight AABBs?
1480
+ *
1481
+ * A pure function of CURRENT body state (poses, shapes, velocity) no
1482
+ * broadphase or pad history so every engine holding the same bodies
1483
+ * evaluates it identically. Conservative containment: the swept box is
1484
+ * inside the mover's stage-2 fat box (pad = 2·|v|·dt + slack v·dt)
1485
+ * and the sleeper's tight box is inside its stored leaf, so whenever
1486
+ * the predicate is true the broadphase has the pair THIS frame in any
1487
+ * engine the manifold slot the wake stage needs is guaranteed to
1488
+ * exist on the frame the edge fires.
1489
+ *
1490
+ * Linear sweep only (matching the broadphase pad): a purely angular
1491
+ * approach is woken one frame later by its first actual contact.
1492
+ *
1493
+ * @private
1494
+ * @param {number} mover_idx body index of the awake side
1495
+ * @param {number} sleeper_idx body index of the sleeping side
1496
+ * @param {number} dt
1497
+ * @returns {boolean}
1498
+ */
1499
+ __swept_proximity(mover_idx, sleeper_idx, dt) {
1500
+ const mover_list = this.__body_collider_lists[mover_idx];
1501
+ const sleeper_list = this.__body_collider_lists[sleeper_idx];
1502
+ if (mover_list === undefined || sleeper_list === undefined) return false;
1503
+
1504
+ const lv = this.__bodies[mover_idx].linearVelocity;
1505
+ const px = lv[0] * dt;
1506
+ const py = lv[1] * dt;
1507
+ const pz = lv[2] * dt;
1508
+
1509
+ for (let i = 0; i < mover_list.length; i++) {
1510
+ const m = mover_list[i];
1511
+ if (m.bvhNode === COLLIDER_UNBOUND) continue;
1512
+ compute_world_aabb(scratch_prox_mover, 0, m.collider.shape, m.transform);
1513
+ if (px < 0) scratch_prox_mover[0] += px; else scratch_prox_mover[3] += px;
1514
+ if (py < 0) scratch_prox_mover[1] += py; else scratch_prox_mover[4] += py;
1515
+ if (pz < 0) scratch_prox_mover[2] += pz; else scratch_prox_mover[5] += pz;
1516
+
1517
+ for (let k = 0; k < sleeper_list.length; k++) {
1518
+ const s = sleeper_list[k];
1519
+ if (s.bvhNode === COLLIDER_UNBOUND) continue;
1520
+ compute_world_aabb(scratch_prox_sleeper, 0, s.collider.shape, s.transform);
1521
+ if (scratch_prox_mover[0] <= scratch_prox_sleeper[3]
1522
+ && scratch_prox_mover[3] >= scratch_prox_sleeper[0]
1523
+ && scratch_prox_mover[1] <= scratch_prox_sleeper[4]
1524
+ && scratch_prox_mover[4] >= scratch_prox_sleeper[1]
1525
+ && scratch_prox_mover[2] <= scratch_prox_sleeper[5]
1526
+ && scratch_prox_mover[5] >= scratch_prox_sleeper[2]) {
1527
+ return true;
1528
+ }
1529
+ }
1530
+ }
1531
+ return false;
1532
+ }
1533
+
1534
+ /**
1535
+ * Wake propagation across joints: if a joint connects an awake body to a
1536
+ * sleeping one, wake the sleeper so the constraint stays coupled (a joint
1537
+ * with one body asleep and one awake would otherwise be skipped by the
1538
+ * solver, letting the awake side drift). A common trigger is a
1539
+ * kinematic/motor-driven body pulling on a sleeping chain.
1540
+ *
1541
+ * Bodies that slept together as one island share a sleep group, so the
1542
+ * usual atomic wake already revives the whole chain when any member is
1543
+ * hit; this catches the cases that atomic wake doesn't (joint spanning
1544
+ * separate groups, kinematic driver).
1545
+ * @private
1546
+ */
1547
+ __wake_joints() {
1548
+ const joints = this.__joints;
1549
+ const n = joints.length;
1550
+ if (n === 0) return;
1551
+ const bodies = this.__bodies;
1552
+ const storage = this.storage;
1553
+ for (let i = 0; i < n; i++) {
1554
+ const joint = joints[i];
1555
+ if (joint === undefined || joint === null) continue;
1556
+ if (joint._bodyIdB === JOINT_WORLD) continue;
1557
+ if (!storage.is_valid(joint._bodyIdA) || !storage.is_valid(joint._bodyIdB)) continue;
1558
+ const a = bodies[body_id_index(joint._bodyIdA)];
1559
+ const b = bodies[body_id_index(joint._bodyIdB)];
1560
+ if (a === undefined || b === undefined) continue;
1561
+ const aSleep = a.sleepState === SleepState.Sleeping;
1562
+ const bSleep = b.sleepState === SleepState.Sleeping;
1563
+ if (aSleep === bSleep) continue; // both awake or both asleep — leave as is
1564
+
1565
+ // The "awake" side must be capable of pulling the sleeper. A
1566
+ // static anchor's sleepState is never updated (it reads Awake
1567
+ // forever), so without this rule a body jointed to a static could
1568
+ // never sleep perpetual mismatch wake. A kinematic driver
1569
+ // counts only while it is actually moving: a parked motor leaves
1570
+ // its chain asleep, and starting it wakes the chain (same motion
1571
+ // rule as __wake_pairs).
1572
+ const mover = aSleep ? b : a;
1573
+ if (mover.kind !== BodyKind.Dynamic) {
1574
+ const lv = mover.linearVelocity;
1575
+ const av = mover.angularVelocity;
1576
+ if (lv[0] === 0 && lv[1] === 0 && lv[2] === 0
1577
+ && av[0] === 0 && av[1] === 0 && av[2] === 0) {
1578
+ continue;
1579
+ }
1580
+ }
1581
+ if (aSleep) this.__wake_body(a); else this.__wake_body(b);
1582
+ }
1583
+ }
1584
+
1585
+ /**
1586
+ * Per-island atomic sleep test. Walks each island once and applies the
1587
+ * decision uniformly across all members:
1588
+ *
1589
+ * - If any member carries {@link RigidBodyFlags.DisableSleep}, the
1590
+ * entire island is exempt; every member's sleep_timer is reset.
1591
+ * - If `max(|v|² + |ω|²)` across all members is below
1592
+ * {@link sleepVelocitySqrThreshold}, every member's sleep_timer is
1593
+ * incremented by `dt`. When the smallest member's timer crosses
1594
+ * {@link sleepTimeThreshold}, the whole island sleeps atomically in
1595
+ * the same step (members get threaded into a sleep-group chain so
1596
+ * {@link __wake_body} can wake them all in one call).
1597
+ * - Otherwise the island has at least one active member, so every
1598
+ * member's timer is reset.
1599
+ *
1600
+ * This is the design-plan atomic-island sleep replaces the per-body
1601
+ * approximation that lived in this slot during the previous slice.
1602
+ * Weakly-connected piles no longer chatter awake when a single member
1603
+ * blips above threshold; piles fall asleep and wake up as one.
1604
+ *
1605
+ * @private
1606
+ * @param {number} dt
1607
+ */
1608
+ __sleep_test(dt) {
1609
+ const threshold_sqr = this.sleepVelocitySqrThreshold;
1610
+ const time_threshold = this.sleepTimeThreshold;
1611
+ const bodies = this.__bodies;
1612
+ const islands = this.islands;
1613
+ const island_count = islands.island_count;
1614
+ const body_offsets = islands.body_offsets;
1615
+ const body_data = islands.body_data;
1616
+
1617
+ for (let isl = 0; isl < island_count; isl++) {
1618
+ const start = body_offsets[isl];
1619
+ const end = body_offsets[isl + 1];
1620
+ if (end === start) continue;
1621
+
1622
+ // Pass 1: find max v² + check DisableSleep across the island.
1623
+ let max_v_sqr = 0;
1624
+ let any_disable_sleep = false;
1625
+ for (let i = start; i < end; i++) {
1626
+ const idx = body_data[i];
1627
+ const rb = bodies[idx];
1628
+ if (rb === undefined) continue;
1629
+ if ((rb.flags & RigidBodyFlags.DisableSleep) !== 0) {
1630
+ any_disable_sleep = true;
1631
+ break;
1632
+ }
1633
+ const lv = rb.linearVelocity;
1634
+ const av = rb.angularVelocity;
1635
+ const v_sqr = lv[0] * lv[0] + lv[1] * lv[1] + lv[2] * lv[2]
1636
+ + av[0] * av[0] + av[1] * av[1] + av[2] * av[2];
1637
+ if (v_sqr > max_v_sqr) max_v_sqr = v_sqr;
1638
+ }
1639
+
1640
+ if (any_disable_sleep) {
1641
+ // Whole island is exempt reset every member's timer.
1642
+ for (let i = start; i < end; i++) {
1643
+ const rb = bodies[body_data[i]];
1644
+ if (rb !== undefined) rb.sleep_timer = 0;
1645
+ }
1646
+ continue;
1647
+ }
1648
+
1649
+ if (max_v_sqr < threshold_sqr) {
1650
+ // Island is at rest — increment every member's timer; if the
1651
+ // slowest-stabilising member has crossed the time threshold,
1652
+ // every member has (they were incremented together this step),
1653
+ // so atomic-sleep the island.
1654
+ let min_timer = Infinity;
1655
+ for (let i = start; i < end; i++) {
1656
+ const rb = bodies[body_data[i]];
1657
+ if (rb === undefined) continue;
1658
+ rb.sleep_timer += dt;
1659
+ if (rb.sleep_timer < min_timer) min_timer = rb.sleep_timer;
1660
+ }
1661
+ if (min_timer >= time_threshold) {
1662
+ this.__atomic_sleep_island_range(body_data, start, end);
1663
+ }
1664
+ } else {
1665
+ // At least one member is active reset every timer.
1666
+ for (let i = start; i < end; i++) {
1667
+ const rb = bodies[body_data[i]];
1668
+ if (rb !== undefined) rb.sleep_timer = 0;
1669
+ }
1670
+ }
1671
+ }
1672
+ }
1673
+
1674
+ /**
1675
+ * Dispatch every buffered contact event to the entities involved, via the
1676
+ * dataset's per-entity event channel `dataset.sendEvent(entity, name,
1677
+ * payload)`, once for each of the pair's entities. This is the only
1678
+ * contact-event path: a system with no dataset attached has nowhere to
1679
+ * deliver and emits nothing.
1680
+ *
1681
+ * Payload is a reused scratch object; listeners must copy anything they
1682
+ * intend to retain past the listener body.
1683
+ * @private
1684
+ */
1685
+ __dispatch_contact_events() {
1686
+ const events = this.contactEvents;
1687
+ const n = events.count;
1688
+ if (n === 0) return;
1689
+
1690
+ const ecd = (this.entityManager !== null && this.entityManager !== undefined)
1691
+ ? this.entityManager.dataset
1692
+ : null;
1693
+ // No dataset nowhere to deliver. The buffer is cleared by
1694
+ // diff_manifolds each step regardless, so nothing leaks.
1695
+ if (ecd === null || ecd === undefined) return;
1696
+
1697
+ const manifolds = this.manifolds;
1698
+ const data = manifolds.data_buffer;
1699
+
1700
+ const payload = this.__contact_payload;
1701
+
1702
+ for (let i = 0; i < n; i++) {
1703
+ const kind = events.kind_at(i);
1704
+ const entA = events.entityA_at(i);
1705
+ const entB = events.entityB_at(i);
1706
+ const slot = events.slot_at(i);
1707
+
1708
+ // Use the deepest contact of the manifold as the representative
1709
+ // point/normal/depth for the event. v1: contact 0 only.
1710
+ const slot_off = manifolds.slot_data_offset(slot);
1711
+ // Gate on the SAME predicate diff_manifolds used to decide this event, not on
1712
+ // contact_count alone: an untouched slot (pair left broadphase range, or a body was
1713
+ // removed) keeps its last-narrowphased contact_count non-zero because nothing
1714
+ // clears the data slab for it. Re-deriving from contact_count would stamp that stale
1715
+ // geometry into an End payload (a live penetration reported for bodies now far apart);
1716
+ // matching the diff (is_touched && count>0) yields a zeroed payload, as the in-range
1717
+ // separation path already does via clear_contacts.
1718
+ const has_contact = manifolds.is_touched(slot) && manifolds.contact_count(slot) > 0;
1719
+
1720
+ // Scratch payload — write the point/normal components by index to
1721
+ // skip the Vector3 change-signal dispatch (nothing observes them).
1722
+ const pt = payload.point;
1723
+ const nm = payload.normal;
1724
+ if (has_contact) {
1725
+ const wax = data[slot_off], way = data[slot_off + 1], waz = data[slot_off + 2];
1726
+ const wbx = data[slot_off + 3], wby = data[slot_off + 4], wbz = data[slot_off + 5];
1727
+ pt[0] = (wax + wbx) * 0.5;
1728
+ pt[1] = (way + wby) * 0.5;
1729
+ pt[2] = (waz + wbz) * 0.5;
1730
+ nm[0] = data[slot_off + 6];
1731
+ nm[1] = data[slot_off + 7];
1732
+ nm[2] = data[slot_off + 8];
1733
+ payload.depth = data[slot_off + 9];
1734
+
1735
+ // The event pair is entity-canonical (min, max); the slot's
1736
+ // stored normal points from the slot's OWN body B toward its
1737
+ // body A an assignment that follows packed-id order, i.e.
1738
+ // slot allocation history. Mirror the normal when the
1739
+ // canonicalization swapped the pair, so the payload contract
1740
+ // ("normal points from entityB toward entityA") holds
1741
+ // identically in every engine regardless of slot history.
1742
+ const slot_idA = manifolds.bodyA(slot);
1743
+ const slot_entA = this.storage.is_valid(slot_idA)
1744
+ ? this.storage.entity_at(body_id_index(slot_idA))
1745
+ : -1;
1746
+ if (slot_entA !== entA) {
1747
+ nm[0] = -nm[0];
1748
+ nm[1] = -nm[1];
1749
+ nm[2] = -nm[2];
1750
+ }
1751
+ } else {
1752
+ pt[0] = 0;
1753
+ pt[1] = 0;
1754
+ pt[2] = 0;
1755
+ nm[0] = 0;
1756
+ nm[1] = 0;
1757
+ nm[2] = 0;
1758
+ payload.depth = 0;
1759
+ }
1760
+ payload.entityA = entA;
1761
+ payload.entityB = entB;
1762
+
1763
+ let event_name;
1764
+ if (kind === ContactEventKind.Begin) {
1765
+ event_name = PhysicsEvents.ContactBegin;
1766
+ } else if (kind === ContactEventKind.Stay) {
1767
+ event_name = PhysicsEvents.ContactStay;
1768
+ } else {
1769
+ event_name = PhysicsEvents.ContactEnd;
1770
+ }
1771
+
1772
+ if (entA >= 0) ecd.sendEvent(entA, event_name, payload);
1773
+ if (entB >= 0) ecd.sendEvent(entB, event_name, payload);
1774
+ }
1775
+ }
1776
+
1777
+ /**
1778
+ * Producer restore pass. At the top of a fixed step, reset every awake
1779
+ * {@link Interpolated} body's live components to their authoritative state
1780
+ * from the previous tick's snapshot, undoing any render-time interpolation
1781
+ * the {@link InterpolationSystem} wrote between frames so the sim integrates
1782
+ * from truth, not an interpolated pose. A body with no previous snapshot
1783
+ * (first step ever, or just woken) is left as-is its live state is already
1784
+ * authoritative. No-op unless {@link interpolationLog} is wired.
1785
+ * @private
1786
+ */
1787
+ __interp_restore() {
1788
+ const log = this.interpolationLog;
1789
+ const em = this.entityManager;
1790
+ if (log === null || em === null || em === undefined) return;
1791
+ const dataset = em.dataset;
1792
+ if (dataset === null || dataset === undefined) return;
1793
+
1794
+ const prev_tick = em.fixedStepTick - 1;
1795
+ if (prev_tick < 0) return;
1796
+
1797
+ const storage = this.storage;
1798
+ // Iterate the set snapshotted at prev_tick (the previous __interp_record), NOT the current
1799
+ // awake set: a body that fell asleep on prev_tick was awake when it was recorded (record runs
1800
+ // before the sleep test) so it carries a prev_tick snapshot, yet it is no longer in the awake
1801
+ // set. Restoring only the awake set would leave that just-slept body holding the render-blended
1802
+ // pose the last frame wrote which the sim then reads via __swept_proximity and integrates
1803
+ // from if it wakes this step. A body woken THIS step has no prev_tick record, so log.interpolate
1804
+ // returns false and it is correctly left on its (authoritative) live pose.
1805
+ const recorded = this.__interp_recorded;
1806
+ const count = this.__interp_recorded_count;
1807
+ const scratch = this.__interp_scratch;
1808
+
1809
+ for (let i = 0; i < count; i++) {
1810
+ const idx = recorded[i];
1811
+ const entity = storage.entity_at(idx);
1812
+ if (entity < 0) continue; // slot freed since it was recorded
1813
+ const interpolated = dataset.getComponent(entity, Interpolated);
1814
+ if (interpolated === undefined || interpolated === null) continue;
1815
+ // A teleported body (snap set, e.g. via setPose) keeps its live pose
1816
+ // this step — restoring the previous tick would undo the teleport.
1817
+ if (interpolated.snap) continue;
1818
+ const key = interpolated.key;
1819
+ if (key < 0) continue;
1820
+
1821
+ const interpolands = interpolated.interpolands;
1822
+ for (let k = 0; k < interpolands.length; k++) {
1823
+ const ip = interpolands[k];
1824
+ scratch.position = 0;
1825
+ // Snap to the previous tick's snapshot (both offsets equal t irrelevant).
1826
+ const ok = log.interpolate(scratch, key, ip.type_id, prev_tick, prev_tick, 0, ip.interpolation_adapter);
1827
+ if (!ok) continue;
1828
+ const target = dataset.getComponent(entity, ip.component_class);
1829
+ if (target === undefined || target === null) continue;
1830
+ scratch.position = 0;
1831
+ ip.serialization_adapter.deserialize(scratch, target);
1832
+ }
1833
+ }
1834
+ }
1835
+
1836
+ /**
1837
+ * Producer record pass. At the end of a fixed step, snapshot every awake
1838
+ * {@link Interpolated} body's live components into the shared log under the
1839
+ * current `entityManager.fixedStepTick`. The render-time
1840
+ * {@link InterpolationSystem} blends consecutive ticks from these snapshots.
1841
+ * Only awake (moving) bodies are recorded, so the log stays sparse. No-op
1842
+ * unless {@link interpolationLog} is wired.
1843
+ *
1844
+ * **A body created after this pass has no snapshot for the step it was born on, and renders
1845
+ * from one snapshot for a step.** This pass runs at the end of `fixedUpdate`, and game code
1846
+ * runs after it that ordering is correct, the sim must read the solved poses so a body
1847
+ * spawned by game code is invisible to the tick it was spawned into. Its first record is the
1848
+ * NEXT tick, and `InterpolationLog.interpolate` handles the missing half gracefully by falling
1849
+ * back to whichever snapshot exists. That is the right primitive behaviour and it produces a
1850
+ * bad picture: the entity is drawn at the *newer* tick for a whole frame budget, so it appears
1851
+ * to jump a full step's travel and then hold still for one. Measured at 165 Hz by a port
1852
+ * (GAP-036) as four frames of a rocket parked at the muzzle, reported by a player as
1853
+ * "projectiles move with jerks".
1854
+ *
1855
+ * **Why this is documented rather than fixed, which is the part worth knowing.** The obvious
1856
+ * fix — record a body's pose when it links does not fit this log. {@link InterpolationLog} is
1857
+ * a strictly monotonic tick ring: `begin_tick` throws while a tick is open, pages are
1858
+ * contiguous ranges that the next page's bytes follow immediately, and `#ticks` is keyed by
1859
+ * tick number so re-opening the closed tick to append one body would overwrite that tick's
1860
+ * metadata for every other body in it. The fix that does work is to move this pass out of
1861
+ * `fixedUpdate` into a system registered after every simulation system, which is what a
1862
+ * consumer hitting this did; that costs a second mandatory registration, which is a trade this
1863
+ * engine has decided against elsewhere. Closing it properly means either a per-body "first
1864
+ * tick" the blend can consult, or a log that accepts an out-of-order record.
1865
+ *
1866
+ * The workaround needs no engine change: put the body on the application's own timeline via a
1867
+ * pose recorder registered last, since `__interp_restore` does not filter by `sourceId` and
1868
+ * `PhysicsSystem` still restores the authoritative pose before it integrates.
1869
+ *
1870
+ * @private
1871
+ */
1872
+ __interp_record() {
1873
+ const log = this.interpolationLog;
1874
+ const em = this.entityManager;
1875
+ if (log === null || em === null || em === undefined) return;
1876
+ const dataset = em.dataset;
1877
+ if (dataset === null || dataset === undefined) return;
1878
+
1879
+ const tick = em.fixedStepTick;
1880
+ const storage = this.storage;
1881
+ const count = storage.awake_count;
1882
+
1883
+ // Rebuild the "recorded this tick" set that __interp_restore will iterate next tick.
1884
+ let recorded = this.__interp_recorded;
1885
+ let recorded_count = 0;
1886
+
1887
+ log.begin_tick(tick);
1888
+ for (let i = 0; i < count; i++) {
1889
+ const idx = storage.awake_at(i);
1890
+ const entity = storage.entity_at(idx);
1891
+ const interpolated = dataset.getComponent(entity, Interpolated);
1892
+ if (interpolated === undefined || interpolated === null) continue;
1893
+ const key = interpolated.key;
1894
+ if (key < 0) continue;
1895
+
1896
+ if (recorded_count === recorded.length) {
1897
+ const grown = new Uint32Array(recorded.length * 2);
1898
+ grown.set(recorded);
1899
+ recorded = grown;
1900
+ this.__interp_recorded = grown;
1901
+ }
1902
+ recorded[recorded_count++] = idx;
1903
+
1904
+ const interpolands = interpolated.interpolands;
1905
+ for (let k = 0; k < interpolands.length; k++) {
1906
+ const ip = interpolands[k];
1907
+ const target = dataset.getComponent(entity, ip.component_class);
1908
+ if (target === undefined || target === null) continue;
1909
+ const buf = log.begin_record(key, ip.type_id);
1910
+ ip.serialization_adapter.serialize(buf, target);
1911
+ log.end_record();
1912
+ }
1913
+ }
1914
+ log.end_tick();
1915
+
1916
+ this.__interp_recorded_count = recorded_count;
1917
+ }
1918
+
1919
+ /**
1920
+ * Advance the simulation by one step: forces, broadphase, narrowphase,
1921
+ * islands, the substep solve, CCD, sleep, and contact-event dispatch.
1922
+ * Reads and writes the linked {@link RigidBody} and {@link Transform64}
1923
+ * components in place.
1924
+ *
1925
+ * Pass the same `dt` every step — warm-starting and sleep timing both
1926
+ * assume a fixed rate.
1927
+ *
1928
+ * @param {number} dt seconds
1929
+ */
1930
+ fixedUpdate(dt) {
1931
+ // Producer: restore authoritative pose (undo render interpolation)
1932
+ // before the sim reads any Transform this step.
1933
+ this.__interp_restore();
1934
+
1935
+ const gx = this.gravity.x;
1936
+ const gy = this.gravity.y;
1937
+ const gz = this.gravity.z;
1938
+
1939
+ const storage = this.storage;
1940
+ const bodies = this.__bodies;
1941
+ const transforms = this.__transforms;
1942
+ const joints = this.__joints;
1943
+ const manifolds = this.manifolds;
1944
+
1945
+ // Stage 0: wake propagation through the persistent manifolds
1946
+ // (last frame's broadphase-touched pairs), then through joints.
1947
+ // Runs before everything else so a just-woken body is part of this
1948
+ // frame's awake set end to end (forces, refit, broadphase,
1949
+ // narrowphase, islands, solve): waking later would give it one
1950
+ // unsupported step (gravity with no contacts penetration pop).
1951
+ // Reads only manifold + body state both part of the exportable
1952
+ // world state never the transient pair list, so a reconstructed
1953
+ // engine wakes on the original's schedule.
1954
+ this.__wake_pairs(dt);
1955
+ this.__wake_joints();
1956
+
1957
+ const count = storage.awake_count;
1958
+
1959
+ // Stage 1: consume the per-frame force / torque accumulators into
1960
+ // velocity at the full `dt`, exactly once (a user force is a per-frame
1961
+ // budget that must land in full regardless of substep count). Gravity
1962
+ // is applied per substep below, so the trajectory integrates at the
1963
+ // substep rate and — crucially — each substep's gravity is balanced by
1964
+ // that substep's contact warm-start, keeping resting stacks at zero
1965
+ // velocity.
1966
+ for (let i = 0; i < count; i++) {
1967
+ const idx = storage.awake_at(i);
1968
+
1969
+ const rb = bodies[idx];
1970
+ const tr = transforms[idx];
1971
+
1972
+ integrate_velocity_forces(rb, tr, dt);
1973
+ }
1974
+
1975
+ // Stage 2: refit each awake body's collider leaves at the current
1976
+ // pose, padded by the swept extent for the body's velocity. The fat
1977
+ // margin uses the post-force velocity; this frame's gravity increment
1978
+ // is a sub-millimetre slack difference, safely inside the margin.
1979
+ const lists = this.__body_collider_lists;
1980
+ for (let i = 0; i < count; i++) {
1981
+
1982
+ const idx = storage.awake_at(i);
1983
+ const rb = bodies[idx];
1984
+ const list = lists[idx];
1985
+
1986
+ if (list === undefined) {
1987
+ continue;
1988
+ }
1989
+
1990
+ const lv = rb.linearVelocity;
1991
+
1992
+ const list_length = list.length;
1993
+
1994
+ for (let k = 0; k < list_length; k++) {
1995
+
1996
+ const entry = list[k];
1997
+
1998
+ // Refit skip the fat margin's whole purpose: while the
1999
+ // body's FATTENED box (tight + velocity pad) stays inside the
2000
+ // leaf's stored AABB, the stored box still covers everything
2001
+ // the body can reach this step, so the set + refit-to-root is
2002
+ // pure waste. A resting field pays only this containment test
2003
+ // per leaf per frame (at v ≈ 0 the fat box is the tight box
2004
+ // plus the constant slack, which the stored box includes).
2005
+ //
2006
+ // The containment probe MUST be the fat box, not the tight
2007
+ // one: the leaf is how the broadphase and every swept query
2008
+ // discover this body. A body whose velocity grew while its
2009
+ // tight box still sat inside the stored leaf (freshly linked
2010
+ // with a launch velocity, or accelerated by an impulse) would
2011
+ // otherwise keep a leaf covering none of the path it travels
2012
+ // this step invisible to pair generation and to other
2013
+ // bodies' CCD sweeps for the whole step.
2014
+ compute_world_aabb(scratch_world_aabb, 0, entry.collider.shape, entry.transform);
2015
+ fatten_world_aabb(scratch_world_aabb, 0, lv[0], lv[1], lv[2], dt);
2016
+ const node = entry.bvhNode;
2017
+ this.dynamicBvh.node_get_aabb(node, scratch_stored_aabb);
2018
+ if (scratch_world_aabb[0] >= scratch_stored_aabb[0]
2019
+ && scratch_world_aabb[1] >= scratch_stored_aabb[1]
2020
+ && scratch_world_aabb[2] >= scratch_stored_aabb[2]
2021
+ && scratch_world_aabb[3] <= scratch_stored_aabb[3]
2022
+ && scratch_world_aabb[4] <= scratch_stored_aabb[4]
2023
+ && scratch_world_aabb[5] <= scratch_stored_aabb[5]) {
2024
+ continue;
2025
+ }
2026
+
2027
+ this.dynamicBvh.node_move_aabb(node, scratch_world_aabb);
2028
+ }
2029
+ }
2030
+
2031
+ // Stage 3: broadphase pair generation. The fat AABBs cover the full
2032
+ // outer-step motion, so the pair set stays valid across all substeps
2033
+ // — broadphase runs once.
2034
+ generate_pairs(
2035
+ storage,
2036
+ this.dynamicBvh,
2037
+ this.staticBvh,
2038
+ manifolds,
2039
+ lists,
2040
+ this.pairs,
2041
+ this.__pair_filter,
2042
+ this,
2043
+ );
2044
+
2045
+ // Stage 5: narrowphase once per outer step. The substep loop below
2046
+ // re-derives each contact's penetration analytically from the moved
2047
+ // poses rather than re-running geometry.
2048
+ narrowphase_step(this.pairs, manifolds, this.__body_collider_lists);
2049
+
2050
+ // Stage 6: partition awake bodies + touched contacts into islands.
2051
+ // Consumed by the solver (flattened contact list) and the sleep test.
2052
+ this.islands.build(storage, manifolds, bodies, this.__body_collider_lists, joints);
2053
+
2054
+ // Stage 7: TGS substep loop.
2055
+ //
2056
+ // prepare_contacts captures per-contact anchors / effective masses /
2057
+ // approach velocity (no warm-start that's per-substep). Each substep
2058
+ // integrates gravity by `h`, re-derives contact geometry from the
2059
+ // current poses, replays warm-start, solves velocity (non-penetration
2060
+ // + friction) and position (pseudo-velocity), and integrates the pose
2061
+ // by `h`. Per-substep gravity + per-substep warm-start balance exactly
2062
+ // at a resting contact (each cancels `h` of the other), so stacks hold
2063
+ // at zero velocity and sleep; the position correction adapts as bodies
2064
+ // separate between substeps the TGS stack-stability mechanism —
2065
+ // without re-running narrowphase. Restitution is applied once after
2066
+ // the loop.
2067
+ // Clamp the substep count: 0 (or a negative / fractional value) would
2068
+ // feed h = dt / N = Infinity into prepare_contacts and a broken loop
2069
+ // bound into the solver.
2070
+ const N = this.substeps > 1 ? this.substeps | 0 : 1;
2071
+ const h = dt / N;
2072
+ const count_after_wake = storage.awake_count;
2073
+
2074
+ // CCD: capture start-of-step positions for flagged bodies over the
2075
+ // post-wake awake set (poses are unchanged until the substep loop below
2076
+ // integrates them). The CCD pass after the solver sweeps from here to
2077
+ // each body's final pose. Reads the primary collider's transform so the
2078
+ // start matches the end the resolve pass reads. Zero-cost when no body
2079
+ // is flagged.
2080
+ const ccd_on = this.ccdEnabled;
2081
+
2082
+ if (ccd_on) {
2083
+
2084
+ const ccd_need = storage.high_water_mark * 3;
2085
+
2086
+ if (this.__ccd_start_pos.length < ccd_need) {
2087
+ this.__ccd_start_pos = new Float64Array(ccd_need);
2088
+ }
2089
+
2090
+ const ccd_start = this.__ccd_start_pos;
2091
+
2092
+ for (let i = 0; i < count_after_wake; i++) {
2093
+ const idx = storage.awake_at(i);
2094
+ const rb = bodies[idx];
2095
+
2096
+ if (rb.kind !== BodyKind.Dynamic) {
2097
+ continue;
2098
+ }
2099
+ if ((rb.flags & RigidBodyFlags.CCD) === 0) {
2100
+ continue;
2101
+ }
2102
+
2103
+ const list = this.__body_collider_lists[idx];
2104
+
2105
+ if (list === undefined || list.length === 0) {
2106
+ continue;
2107
+ }
2108
+
2109
+ const cp = list[0].transform;
2110
+ const cb = idx * 3;
2111
+
2112
+ ccd_start[cb] = cp.translation_x;
2113
+ ccd_start[cb + 1] = cp.translation_y;
2114
+ ccd_start[cb + 2] = cp.translation_z;
2115
+ }
2116
+ }
2117
+
2118
+ // Size the pseudo-velocity buffer ONCE (it may reallocate on growth),
2119
+ // then capture the reference. Inside the loop we only zero its live
2120
+ // region per substep — re-capturing is unnecessary since it won't
2121
+ // reallocate again this step.
2122
+ this.__reset_pseudo_velocity();
2123
+ const pseudoVel = this.__pseudo_velocity;
2124
+ const pseudo_len = storage.high_water_mark * 6;
2125
+
2126
+ // Gather the data-oriented solver state for every body the substep loop
2127
+ // will touch: the post-wake awake set (all dynamics, at their post-force
2128
+ // velocity) plus the static / kinematic anchors and jointed partners
2129
+ // referenced by this step's contacts and joints. From here the substep
2130
+ // loop reads / writes velocity + orientation through `ss_data` with no
2131
+ // component-object dereference; persistent velocity is scattered back
2132
+ // onto the RigidBodies after the solve.
2133
+ const solver_state = this.__solver_state;
2134
+ solver_state.begin(storage.high_water_mark);
2135
+
2136
+ for (let i = 0; i < count_after_wake; i++) {
2137
+ const idx = storage.awake_at(i);
2138
+ solver_state.gather(idx, bodies[idx], transforms[idx]);
2139
+ }
2140
+
2141
+ const island_contacts = this.islands.contact_data;
2142
+ const island_contact_total = this.islands.contact_offsets[this.islands.island_count];
2143
+
2144
+ for (let i = 0; i < island_contact_total; i++) {
2145
+ const slot = island_contacts[i];
2146
+
2147
+ const ia = body_id_index(manifolds.bodyA(slot));
2148
+ const ib = body_id_index(manifolds.bodyB(slot));
2149
+
2150
+ solver_state.gather(ia, bodies[ia], transforms[ia]);
2151
+ solver_state.gather(ib, bodies[ib], transforms[ib]);
2152
+ }
2153
+
2154
+ const joint_count = joints.length;
2155
+ for (let i = 0; i < joint_count; i++) {
2156
+ const joint = joints[i];
2157
+
2158
+ if (joint === undefined || joint === null) {
2159
+ continue;
2160
+ }
2161
+
2162
+ if (!storage.is_valid(joint._bodyIdA)) {
2163
+ continue;
2164
+ }
2165
+
2166
+ const ia = body_id_index(joint._bodyIdA);
2167
+
2168
+ solver_state.gather(ia, bodies[ia], transforms[ia]);
2169
+
2170
+ if (joint._bodyIdB !== JOINT_WORLD && storage.is_valid(joint._bodyIdB)) {
2171
+
2172
+ const ib = body_id_index(joint._bodyIdB);
2173
+
2174
+ solver_state.gather(ib, bodies[ib], transforms[ib]);
2175
+
2176
+ }
2177
+ }
2178
+
2179
+ const ss_data = solver_state.data;
2180
+
2181
+ prepare_contacts(manifolds, this, h);
2182
+
2183
+ for (let s = 0; s < N; s++) {
2184
+
2185
+ // Gravity (+ damping) for this substep.
2186
+ for (let i = 0; i < count_after_wake; i++) {
2187
+ const idx = storage.awake_at(i);
2188
+ integrate_velocity_gravity(ss_data, idx * SBS_STRIDE, bodies[idx], gx, gy, gz, h);
2189
+ }
2190
+
2191
+ // Re-derive contact geometry at the current poses: concave pairs
2192
+ // re-run narrowphase (fresh feature/normal as the body rocks),
2193
+ // convex pairs rotate frozen anchors analytically. Then replay
2194
+ // the per-substep warm-start and solve velocity.
2195
+ redetect_concave_contacts(manifolds, this);
2196
+ refresh_contacts(manifolds, this.__transforms);
2197
+ warm_start_contacts(manifolds, this);
2198
+ solve_velocity(manifolds, this, this.velocityIterations);
2199
+
2200
+ // Joints share the substep: warm-start + velocity-solve the 6-DOF
2201
+ // constraints on real velocity, coupled with the contacts above
2202
+ // (a body touched by both sees one substep of Gauss-Seidel across
2203
+ // contacts then joints). Position correction for locked DOFs is a
2204
+ // SPOOK bias inside this solve, so no separate joint position pass.
2205
+ // Solved in the entity-canonical order (NOT joint-id order, which
2206
+ // encodes link/unlink history) — see __joints_sorted.
2207
+ if (this.__joints_sorted.length > 0) {
2208
+ // Alternate the joint sweep direction on odd substeps so the chain
2209
+ // is solved root→tip and tip→root across the step (symmetric
2210
+ // Gauss-Seidel), cancelling the one-direction propagation bias.
2211
+ solve_joints(this.__joints_sorted, this, h, this.jointIterations, (s & 1) === 1);
2212
+ }
2213
+
2214
+ // Position correction writes pseudo-velocity (zeroed first so it
2215
+ // is a fresh per-substep correction), folded into the pose by the
2216
+ // position integrate and then discarded. SOFT contacts skip it:
2217
+ // their penetration recovery is the velocity-bias term, and the
2218
+ // relax pass below strips the momentum that bias injected
2219
+ // (Box2D-v3 substep order).
2220
+ const contacts_soft = this.contactHertz > 0;
2221
+ pseudoVel.fill(0, 0, pseudo_len);
2222
+ if (!contacts_soft) {
2223
+ solve_position(manifolds, this, this.positionIterations);
2224
+ }
2225
+
2226
+ for (let i = 0; i < count_after_wake; i++) {
2227
+ const idx = storage.awake_at(i);
2228
+ const rb = bodies[idx];
2229
+ const tr = transforms[idx];
2230
+ const base = idx * 6;
2231
+ integrate_position(ss_data, idx * SBS_STRIDE, rb, tr, h,
2232
+ pseudoVel[base], pseudoVel[base + 1], pseudoVel[base + 2],
2233
+ pseudoVel[base + 3], pseudoVel[base + 4], pseudoVel[base + 5]);
2234
+ }
2235
+
2236
+ // Relax pass (soft contacts only): one biasless sweep over the
2237
+ // contact rows AFTER position integration, removing the real
2238
+ // momentum the velocity-bias push injected this substep
2239
+ // (Box2D-v3 substep order: solve -> integrate -> relax).
2240
+ if (contacts_soft) {
2241
+ solve_velocity(manifolds, this, 1, false);
2242
+ }
2243
+ }
2244
+
2245
+ // Stage 8: one-shot restitution, after the substep loop, keyed off
2246
+ // the approach velocity captured at prepare time.
2247
+ apply_restitution(manifolds, this);
2248
+
2249
+ // Scatter the solved persistent linear / angular velocity back onto the
2250
+ // RigidBody components (pose was written through to the Transforms each
2251
+ // substep). After this the bodies are authoritative again for CCD,
2252
+ // interpolation recording, and the sleep test below.
2253
+ solver_state.scatter(bodies);
2254
+
2255
+ // The transforms moved and carry no signals of their own, so the move is announced here:
2256
+ // once per awake body per step, rather than once per substep write. Everything that used to
2257
+ // wake on position.onChanged - the render placement, the parent/child sync, fog-of-war -
2258
+ // wakes on this instead.
2259
+ {
2260
+ const ecd = (this.entityManager !== null && this.entityManager !== undefined)
2261
+ ? this.entityManager.dataset
2262
+ : null;
2263
+
2264
+ if (ecd !== null) {
2265
+ const awake_count = storage.awake_count;
2266
+
2267
+ for (let i = 0; i < awake_count; i++) {
2268
+ const idx = storage.awake_at(i);
2269
+ const entity = storage.entity_at(idx);
2270
+
2271
+ if (entity >= 0) {
2272
+ ecd.sendEvent(entity, EventType.ComponentChanged, {
2273
+ klass: Transform64,
2274
+ instance: transforms[idx]
2275
+ });
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+
2281
+ // Stage 8.5: continuous collision sweep CCD-flagged fast movers along
2282
+ // their net step translation and stop them at the first blocker, so they
2283
+ // can't tunnel through thin geometry between discrete steps. Runs on the
2284
+ // final post-solve poses, before the sleep test sees the clamped
2285
+ // velocities. No-op when no awake body is flagged.
2286
+ if (ccd_on) {
2287
+ ccd_resolve(this);
2288
+ }
2289
+
2290
+ // Producer: record the post-step authoritative pose of every awake
2291
+ // interpolated body under this step's tick. Before the sleep test, so a
2292
+ // body that settles this step still records its final pose for the last
2293
+ // interpolation interval.
2294
+ this.__interp_record();
2295
+
2296
+ // Stage 9: sleep test.
2297
+ this.__sleep_test(dt);
2298
+
2299
+ // Stage 10: diff manifolds against the previous frame and dispatch
2300
+ // Begin / Stay / End events. MUST run before advance_frame, which
2301
+ // rolls the touched flags.
2302
+ diff_manifolds(manifolds, storage, this.contactEvents);
2303
+ this.__dispatch_contact_events();
2304
+
2305
+ // Stage 11 (end-of-step): roll touched → prev_touched and the
2306
+ // had-contacts flag, evict slots whose pair has not been touched
2307
+ // within the grace window. Dormant pairs (both bodies asleep) are
2308
+ // frozen — contacts and warm-start impulses survive the nap.
2309
+ manifolds.advance_frame(storage);
2310
+
2311
+ // Stage 11.5: take back the contacts the CCD pass wrote for its own
2312
+ // impacts. They were a report for the diff above, not manifold state —
2313
+ // the advance has already rolled them into their pair's had-contacts
2314
+ // bit (so the next step resolves to Stay, not a second Begin), and
2315
+ // leaving them in the slabs would let __wake_pairs, which runs before
2316
+ // the narrowphase can clear them, wake a sleeper on a contact the
2317
+ // solver never acts on.
2318
+ if (ccd_on) {
2319
+ ccd_release_contacts(this);
2320
+ }
2321
+
2322
+ // Dev tripwire (assert.* is compiled out of prod): no NaN / Infinity
2323
+ // may survive a step in body state or contact manifolds. NaN is
2324
+ // self-concealing downstream — it fails every depth comparison and
2325
+ // sleep threshold — so it must fail loudly here, at the step boundary.
2326
+ assert.equal(find_non_finite_physics_state(this), "", "non-finite physics state after fixedUpdate");
2327
+ }
2328
+ }
2329
+
2330
+ /**
2331
+ * @readonly
2332
+ * @type {boolean}
2333
+ */
2334
+ PhysicsSystem.prototype.isPhysicsSystem = true;
2335
+
2336
+ // Re-export for convenience.
2337
+ export { BodyKind, RigidBodyFlags };