@woosh/meep-engine 2.155.0 → 2.156.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 (475) hide show
  1. package/README.md +1 -1
  2. package/build/bundle-worker-image-decoder.js +1 -1
  3. package/build/bundle-worker-terrain.js +1 -1
  4. package/editor/view/ecs/ComponentControlView.d.ts +0 -9
  5. package/editor/view/ecs/ComponentControlView.js +2 -98
  6. package/package.json +1 -1
  7. package/src/core/binary/32BitEncoder.js +1 -1
  8. package/src/core/binary/to_half_float_uint16.js +3 -3
  9. package/src/core/bvh2/bvh3/ebvh_build_hierarchy_radix.d.ts.map +1 -1
  10. package/src/core/bvh2/bvh3/ebvh_build_hierarchy_radix.js +275 -253
  11. package/src/core/cache/Cache.d.ts.map +1 -1
  12. package/src/core/cache/Cache.js +7 -0
  13. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  14. package/src/core/cache/FrequencySketch.js +8 -4
  15. package/src/core/clipboard/obtainClipBoard.d.ts +6 -0
  16. package/src/core/clipboard/obtainClipBoard.d.ts.map +1 -0
  17. package/src/core/clipboard/obtainClipBoard.js +29 -0
  18. package/src/core/clipboard/safeClipboardReadText.d.ts +6 -0
  19. package/src/core/clipboard/safeClipboardReadText.d.ts.map +1 -0
  20. package/src/core/clipboard/safeClipboardReadText.js +55 -0
  21. package/src/core/clipboard/safeClipboardWriteText.d.ts +8 -0
  22. package/src/core/clipboard/safeClipboardWriteText.d.ts.map +1 -0
  23. package/src/core/clipboard/safeClipboardWriteText.js +23 -0
  24. package/src/core/collection/array/array_quick_sort_by_lookup_map.js +1 -1
  25. package/src/core/collection/array/array_set_diff_sorting.d.ts.map +1 -1
  26. package/src/core/collection/array/array_set_diff_sorting.js +4 -1
  27. package/src/core/collection/array/array_shuffle.d.ts.map +1 -1
  28. package/src/core/collection/array/array_shuffle.js +30 -27
  29. package/src/core/collection/array/binarySearchLowIndex.d.ts.map +1 -1
  30. package/src/core/collection/array/binarySearchLowIndex.js +4 -3
  31. package/src/core/collection/array/typed/array_buffer_hash.js +1 -1
  32. package/src/core/collection/array/typed/is_typed_array_equals.d.ts.map +1 -1
  33. package/src/core/collection/array/typed/is_typed_array_equals.js +12 -2
  34. package/src/core/collection/heap/BinaryHeap.d.ts.map +1 -1
  35. package/src/core/collection/heap/BinaryHeap.js +12 -2
  36. package/src/core/collection/queue/Deque.d.ts.map +1 -1
  37. package/src/core/collection/queue/Deque.js +10 -8
  38. package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
  39. package/src/core/collection/table/RowFirstTable.js +4 -2
  40. package/src/core/collection/table/RowFirstTableSpec.js +2 -2
  41. package/src/core/color/operations/color_lerp.d.ts.map +1 -1
  42. package/src/core/color/operations/color_lerp.js +10 -3
  43. package/src/core/color/rgb2uint32.js +1 -1
  44. package/src/core/color/rgbe9995_to_rgb.js +1 -1
  45. package/src/core/function/objectsEqual.d.ts.map +1 -1
  46. package/src/core/function/objectsEqual.js +2 -1
  47. package/src/core/geom/2d/aabb/AABB2.d.ts.map +1 -1
  48. package/src/core/geom/2d/aabb/AABB2.js +12 -11
  49. package/src/core/geom/2d/convex-hull/convex_hull_jarvis_2d.d.ts.map +1 -1
  50. package/src/core/geom/2d/convex-hull/convex_hull_jarvis_2d.js +30 -4
  51. package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.d.ts.map +1 -1
  52. package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.js +6 -2
  53. package/src/core/geom/2d/hash-grid/SpatialHashGrid.d.ts.map +1 -1
  54. package/src/core/geom/2d/hash-grid/SpatialHashGrid.js +388 -386
  55. package/src/core/geom/2d/hash-grid/shg_query_elements_line.d.ts.map +1 -1
  56. package/src/core/geom/2d/hash-grid/shg_query_elements_line.js +8 -3
  57. package/src/core/geom/2d/quad-tree/QuadTreeDatum.d.ts.map +1 -1
  58. package/src/core/geom/2d/quad-tree/QuadTreeDatum.js +9 -1
  59. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.d.ts +3 -1
  60. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.d.ts.map +1 -1
  61. package/src/core/geom/2d/quad-tree/qt_query_data_nearest_to_point.js +3 -1
  62. package/src/core/geom/2d/quad-tree-binary/QuadTree.js +714 -714
  63. package/src/core/geom/2d/r-tree/StaticR2Tree.d.ts.map +1 -1
  64. package/src/core/geom/2d/r-tree/StaticR2Tree.js +5 -4
  65. package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.d.ts.map +1 -1
  66. package/src/core/geom/3d/aabb/aabb3_detailed_volume_intersection.js +33 -29
  67. package/src/core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.d.ts.map +1 -1
  68. package/src/core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.js +3 -1
  69. package/src/core/geom/3d/aabb/aabb3_signed_distance_to_aabb3.d.ts.map +1 -1
  70. package/src/core/geom/3d/aabb/aabb3_signed_distance_to_aabb3.js +10 -7
  71. package/src/core/geom/3d/aabb/aabb3_transformed_compute_plane_side.d.ts.map +1 -1
  72. package/src/core/geom/3d/aabb/aabb3_transformed_compute_plane_side.js +30 -9
  73. package/src/core/geom/3d/aabb/compute_aabb_from_points.js +3 -3
  74. package/src/core/geom/3d/box/box3_raycast.d.ts +37 -0
  75. package/src/core/geom/3d/box/box3_raycast.d.ts.map +1 -0
  76. package/src/core/geom/3d/box/box3_raycast.js +81 -0
  77. package/src/core/geom/3d/capsule/capsule_raycast.d.ts +35 -0
  78. package/src/core/geom/3d/capsule/capsule_raycast.d.ts.map +1 -0
  79. package/src/core/geom/3d/capsule/capsule_raycast.js +93 -0
  80. package/src/core/geom/3d/cone/compute_bounding_cone_of_2_cones.d.ts.map +1 -1
  81. package/src/core/geom/3d/cone/compute_bounding_cone_of_2_cones.js +4 -0
  82. package/src/core/geom/3d/frustum/frustum3_computeNearestPointToPoint.js +1 -1
  83. package/src/core/geom/3d/line/line3_compute_segment_point_distance_eikonal.d.ts.map +1 -1
  84. package/src/core/geom/3d/line/line3_compute_segment_point_distance_eikonal.js +3 -2
  85. package/src/core/geom/3d/mat4/decompose_matrix_4_array.d.ts.map +1 -1
  86. package/src/core/geom/3d/mat4/decompose_matrix_4_array.js +12 -2
  87. package/src/core/geom/3d/mat4/eulerAnglesFromMatrix.js +2 -2
  88. package/src/core/geom/3d/mat4/m4_multiply_alphatensor.d.ts +1 -1
  89. package/src/core/geom/3d/mat4/m4_multiply_alphatensor.d.ts.map +1 -1
  90. package/src/core/geom/3d/mat4/m4_multiply_alphatensor.js +19 -13
  91. package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.d.ts.map +1 -1
  92. package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.js +3 -2
  93. package/src/core/geom/3d/plane/plane3_compute_plane_intersection.js +3 -2
  94. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -1
  95. package/src/core/geom/3d/shape/MeshShape3D.js +7 -0
  96. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  97. package/src/core/geom/3d/shape/UnionShape3D.js +3 -2
  98. package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.d.ts.map +1 -1
  99. package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.js +153 -148
  100. package/src/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.d.ts.map +1 -1
  101. package/src/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.js +7 -0
  102. package/src/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.d.ts.map +1 -1
  103. package/src/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.js +13 -10
  104. package/src/core/geom/3d/sphere/sphere_projected_sphere_radius_sqr.d.ts +1 -1
  105. package/src/core/geom/3d/sphere/sphere_projected_sphere_radius_sqr.js +2 -2
  106. package/src/core/geom/3d/sphere/sphere_raycast.d.ts +33 -0
  107. package/src/core/geom/3d/sphere/sphere_raycast.d.ts.map +1 -0
  108. package/src/core/geom/3d/sphere/sphere_raycast.js +47 -0
  109. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.d.ts +24 -0
  110. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.d.ts.map +1 -0
  111. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_tet_get_neighbours.js +39 -0
  112. package/src/core/geom/3d/tetrahedra/triangle/trace_triangular_depth_map.d.ts.map +1 -1
  113. package/src/core/geom/3d/tetrahedra/triangle/trace_triangular_depth_map.js +4 -2
  114. package/src/core/geom/3d/topology/bounds/computeTriangleClusterNormalBoundingCone.js +3 -3
  115. package/src/core/geom/3d/topology/simplify/decimate_edge_collapse_snap.js +1 -1
  116. package/src/core/geom/3d/topology/tm_vertex_compute_normal.d.ts.map +1 -1
  117. package/src/core/geom/3d/topology/tm_vertex_compute_normal.js +4 -2
  118. package/src/core/geom/3d/util/make_justified_point_grid.d.ts.map +1 -1
  119. package/src/core/geom/3d/util/make_justified_point_grid.js +18 -10
  120. package/src/core/geom/ConicRay.d.ts.map +1 -1
  121. package/src/core/geom/ConicRay.js +11 -13
  122. package/src/core/geom/packing/max-rect/removeRedundantBoxes.d.ts.map +1 -1
  123. package/src/core/geom/packing/max-rect/removeRedundantBoxes.js +19 -4
  124. package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts.map +1 -0
  125. package/src/{engine/graphics/sh3/path_tracer/sampling → core/geom/vec3}/v3_orthonormal_matrix_from_normal.js +1 -1
  126. package/src/core/graph/coloring/colorizeGraph.js +2 -2
  127. package/src/core/graph/csr/CSRGraph.d.ts.map +1 -1
  128. package/src/core/graph/csr/CSRGraph.js +325 -319
  129. package/src/core/graph/layout/CircleLayout.d.ts.map +1 -1
  130. package/src/core/graph/layout/CircleLayout.js +8 -6
  131. package/src/core/graph/metis/native/refine/compute_kway_params.d.ts.map +1 -1
  132. package/src/core/graph/metis/native/refine/compute_kway_params.js +139 -138
  133. package/src/core/graph/mn_graph_coarsen.d.ts.map +1 -1
  134. package/src/core/graph/mn_graph_coarsen.js +4 -2
  135. package/src/core/graph/v2/NodeContainer.js +7 -7
  136. package/src/core/localization/LocalizationEngine.js +1 -1
  137. package/src/core/math/bell_membership_function.d.ts.map +1 -1
  138. package/src/core/math/bell_membership_function.js +3 -1
  139. package/src/core/math/complex/complex_add.d.ts +4 -4
  140. package/src/core/math/complex/complex_add.d.ts.map +1 -1
  141. package/src/core/math/complex/complex_add.js +3 -3
  142. package/src/core/math/complex/complex_div.d.ts +4 -4
  143. package/src/core/math/complex/complex_div.d.ts.map +1 -1
  144. package/src/core/math/complex/complex_div.js +3 -3
  145. package/src/core/math/complex/complex_mul.d.ts +4 -4
  146. package/src/core/math/complex/complex_mul.d.ts.map +1 -1
  147. package/src/core/math/complex/complex_mul.js +3 -3
  148. package/src/core/math/complex/complex_sub.d.ts +4 -4
  149. package/src/core/math/complex/complex_sub.d.ts.map +1 -1
  150. package/src/core/math/complex/complex_sub.js +3 -3
  151. package/src/core/math/idct_1d.d.ts +4 -4
  152. package/src/core/math/idct_1d.d.ts.map +1 -1
  153. package/src/core/math/idct_1d.js +3 -3
  154. package/src/core/math/noise/create_simplex_noise_2d.d.ts.map +1 -1
  155. package/src/core/math/noise/create_simplex_noise_2d.js +4 -2
  156. package/src/core/math/noise/sdnoise.d.ts.map +1 -1
  157. package/src/core/math/noise/sdnoise.js +12 -9
  158. package/src/core/math/physics/mie/compute_bhmie_optical_properties.d.ts.map +1 -1
  159. package/src/core/math/physics/mie/compute_bhmie_optical_properties.js +94 -50
  160. package/src/core/math/physics/mie/lorenz_mie_coefs.d.ts +3 -6
  161. package/src/core/math/physics/mie/lorenz_mie_coefs.d.ts.map +1 -1
  162. package/src/core/math/physics/mie/lorenz_mie_coefs.js +180 -157
  163. package/src/core/math/physics/mie/mie_ab_to_optical_properties.d.ts +3 -4
  164. package/src/core/math/physics/mie/mie_ab_to_optical_properties.d.ts.map +1 -1
  165. package/src/core/math/physics/mie/mie_ab_to_optical_properties.js +47 -21
  166. package/src/core/math/random/randomIntegerBetween.d.ts.map +1 -1
  167. package/src/core/math/random/randomIntegerBetween.js +4 -1
  168. package/src/core/math/solveCubic.d.ts.map +1 -1
  169. package/src/core/math/solveCubic.js +95 -82
  170. package/src/core/math/spline/computeCatmullRomSplineUniformDistance.d.ts.map +1 -1
  171. package/src/core/math/spline/computeCatmullRomSplineUniformDistance.js +13 -0
  172. package/src/core/math/statistics/softmax.js +1 -1
  173. package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts +1 -0
  174. package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts.map +1 -1
  175. package/src/core/model/node-graph/visual/NodeGraphVisualData.js +2 -1
  176. package/src/core/model/node-graph/visual/NodeVisualData.js +1 -1
  177. package/src/core/model/object/ImmutableObjectPool.d.ts +7 -0
  178. package/src/core/model/object/ImmutableObjectPool.d.ts.map +1 -1
  179. package/src/core/model/object/ImmutableObjectPool.js +20 -10
  180. package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.d.ts.map +1 -1
  181. package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.js +39 -2
  182. package/src/core/model/reactive/model/terminal/ReactiveReference.d.ts.map +1 -1
  183. package/src/core/model/reactive/model/terminal/ReactiveReference.js +2 -0
  184. package/src/core/parser/simple/readHexToken.d.ts.map +1 -1
  185. package/src/core/parser/simple/readHexToken.js +6 -0
  186. package/src/core/primitives/numbers/number_pretty_print.d.ts.map +1 -1
  187. package/src/core/primitives/numbers/number_pretty_print.js +4 -1
  188. package/src/core/primitives/strings/string_jaro_winkler.js +1 -1
  189. package/src/core/process/CompositeProcess.js +1 -1
  190. package/src/core/process/action/AsynchronousDelayAction.d.ts.map +1 -1
  191. package/src/core/process/action/AsynchronousDelayAction.js +3 -0
  192. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  193. package/src/core/process/executor/ConcurrentExecutor.js +3 -2
  194. package/src/core/process/task/util/randomCountTask.d.ts.map +1 -1
  195. package/src/core/process/task/util/randomCountTask.js +3 -1
  196. package/src/core/process/undo/ActionProcessor.d.ts.map +1 -1
  197. package/src/core/process/undo/ActionProcessor.js +5 -3
  198. package/src/core/process/worker/WorkerBuilder.js +3 -3
  199. package/src/engine/animation/curve/AnimationCurve.d.ts.map +1 -1
  200. package/src/engine/animation/curve/AnimationCurve.js +4 -2
  201. package/src/engine/control/first-person/DESIGN.md +1 -1
  202. package/src/engine/control/first-person/FirstPersonMotionPhase.d.ts +55 -0
  203. package/src/engine/control/first-person/FirstPersonMotionPhase.d.ts.map +1 -0
  204. package/src/engine/control/first-person/FirstPersonMotionPhase.js +134 -0
  205. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +23 -2
  206. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  207. package/src/engine/control/first-person/FirstPersonPlayerController.js +1 -1
  208. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +168 -0
  209. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  210. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +115 -0
  211. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +71 -0
  212. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  213. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +255 -55
  214. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +82 -43
  215. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
  216. package/src/engine/control/first-person/abilities/LedgeGrab.js +405 -213
  217. package/src/engine/control/first-person/abilities/Mantle.d.ts +6 -0
  218. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
  219. package/src/engine/control/first-person/abilities/Mantle.js +104 -45
  220. package/src/engine/control/first-person/abilities/ScrambleUp.d.ts +61 -0
  221. package/src/engine/control/first-person/abilities/ScrambleUp.d.ts.map +1 -0
  222. package/src/engine/control/first-person/abilities/ScrambleUp.js +182 -0
  223. package/src/engine/control/first-person/math/jumpDynamics.d.ts +84 -0
  224. package/src/engine/control/first-person/math/jumpDynamics.d.ts.map +1 -0
  225. package/src/engine/control/first-person/math/jumpDynamics.js +108 -0
  226. package/src/engine/control/first-person/prototype_first_person_controller.js +45 -1
  227. package/src/engine/graphics/camera/testClippingPlaneComputation.js +1 -1
  228. package/src/engine/graphics/ecs/path/tube/prototypeAnimatedPathMask.js +1 -1
  229. package/src/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +1 -1
  230. package/src/engine/graphics/render/buffer/buffers/prototypeNormalFrameBuffer.js +1 -1
  231. package/src/engine/graphics/render/forward_plus/plugin/ptototypeFPPlugin.js +1 -1
  232. package/src/engine/graphics/render/visibility/hiz/prototypeHiZ.js +1 -1
  233. package/src/engine/graphics/sh3/path_tracer/texture/sample_material.js +1 -1
  234. package/src/engine/graphics/shadows/testShadowMapRendering.js +1 -1
  235. package/src/engine/physics/CONSTRAINT_SOLVER_BENCH_LOG.md +208 -0
  236. package/src/engine/physics/CONSTRAINT_SOLVER_IMPROVEMENTS_PLAN.md +364 -0
  237. package/src/engine/physics/PLAN.md +6 -5
  238. package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
  239. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  240. package/src/engine/physics/constraint/solve_constraints.js +147 -33
  241. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  242. package/src/engine/physics/ecs/PhysicsSystem.js +1750 -1747
  243. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  244. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -1
  245. package/src/engine/physics/gjk/gjk_epa_penetration.js +5 -9
  246. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
  247. package/src/engine/physics/narrowphase/convex_convex_manifold.js +22 -25
  248. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -13
  249. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -1
  250. package/src/engine/physics/narrowphase/convex_decomposition.js +61 -65
  251. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -1
  252. package/src/engine/physics/narrowphase/mesh_convex_hull.js +13 -8
  253. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  254. package/src/engine/physics/narrowphase/refine_ray_concave.js +5 -3
  255. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
  256. package/src/engine/physics/narrowphase/refine_ray_hit.js +81 -78
  257. package/src/engine/sound/SoundEngine.d.ts.map +1 -1
  258. package/src/engine/sound/SoundEngine.js +28 -0
  259. package/src/engine/sound/dB2Volume.d.ts +1 -1
  260. package/src/engine/sound/dB2Volume.d.ts.map +1 -1
  261. package/src/engine/sound/dB2Volume.js +1 -1
  262. package/src/engine/sound/ecs/SoundController.d.ts +4 -0
  263. package/src/engine/sound/ecs/SoundController.d.ts.map +1 -1
  264. package/src/engine/sound/ecs/SoundController.js +4 -0
  265. package/src/engine/sound/ecs/SoundControllerSystem.d.ts +5 -0
  266. package/src/engine/sound/ecs/SoundControllerSystem.d.ts.map +1 -1
  267. package/src/engine/sound/ecs/SoundControllerSystem.js +5 -0
  268. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +69 -0
  269. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -0
  270. package/src/engine/sound/ecs/audio/AudioEmitter.js +83 -0
  271. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +97 -0
  272. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -0
  273. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +238 -0
  274. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +90 -0
  275. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -0
  276. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +324 -0
  277. package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts +59 -0
  278. package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts.map +1 -0
  279. package/src/engine/sound/ecs/audio/SpatialAudioIndex.js +140 -0
  280. package/src/engine/sound/ecs/emitter/SoundEmitter.d.ts +16 -65
  281. package/src/engine/sound/ecs/emitter/SoundEmitter.d.ts.map +1 -1
  282. package/src/engine/sound/ecs/emitter/SoundEmitter.js +19 -224
  283. package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.d.ts +26 -29
  284. package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.d.ts.map +1 -1
  285. package/src/engine/sound/ecs/emitter/SoundEmitterComponentContext.js +168 -135
  286. package/src/engine/sound/ecs/emitter/SoundEmitterSystem.d.ts +36 -59
  287. package/src/engine/sound/ecs/emitter/SoundEmitterSystem.d.ts.map +1 -1
  288. package/src/engine/sound/ecs/emitter/SoundEmitterSystem.js +154 -390
  289. package/src/engine/sound/ecs/emitter/SoundTrack.d.ts +20 -23
  290. package/src/engine/sound/ecs/emitter/SoundTrack.d.ts.map +1 -1
  291. package/src/engine/sound/ecs/emitter/SoundTrack.js +34 -152
  292. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +993 -0
  293. package/src/engine/sound/sopra/README.md +643 -7
  294. package/src/engine/sound/sopra/SopraEngine.d.ts +229 -0
  295. package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -0
  296. package/src/engine/sound/sopra/SopraEngine.js +423 -0
  297. package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.d.ts +26 -0
  298. package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.d.ts.map +1 -0
  299. package/src/engine/sound/sopra/asset/AssetManagerBufferProvider.js +71 -0
  300. package/src/engine/sound/sopra/asset/BufferProvider.d.ts +24 -0
  301. package/src/engine/sound/sopra/asset/BufferProvider.d.ts.map +1 -0
  302. package/src/engine/sound/sopra/asset/BufferProvider.js +29 -0
  303. package/src/engine/sound/sopra/asset/StubBufferProvider.d.ts +31 -0
  304. package/src/engine/sound/sopra/asset/StubBufferProvider.d.ts.map +1 -0
  305. package/src/engine/sound/sopra/asset/StubBufferProvider.js +58 -0
  306. package/src/engine/sound/sopra/definition/BusDefinition.d.ts +83 -0
  307. package/src/engine/sound/sopra/definition/BusDefinition.d.ts.map +1 -0
  308. package/src/engine/sound/sopra/definition/BusDefinition.js +142 -0
  309. package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.d.ts +17 -0
  310. package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.d.ts.map +1 -0
  311. package/src/engine/sound/sopra/definition/BusDefinitionSerializationAdapter.js +54 -0
  312. package/src/engine/sound/sopra/definition/DuckingRule.d.ts +71 -0
  313. package/src/engine/sound/sopra/definition/DuckingRule.d.ts.map +1 -0
  314. package/src/engine/sound/sopra/definition/DuckingRule.js +106 -0
  315. package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.d.ts +18 -0
  316. package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.d.ts.map +1 -0
  317. package/src/engine/sound/sopra/definition/DuckingRuleSerializationAdapter.js +31 -0
  318. package/src/engine/sound/sopra/definition/EventDescription.d.ts +132 -0
  319. package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -0
  320. package/src/engine/sound/sopra/definition/EventDescription.js +259 -0
  321. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +17 -0
  322. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -0
  323. package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +71 -0
  324. package/src/engine/sound/sopra/definition/MixerSnapshot.d.ts +51 -0
  325. package/src/engine/sound/sopra/definition/MixerSnapshot.d.ts.map +1 -0
  326. package/src/engine/sound/sopra/definition/MixerSnapshot.js +83 -0
  327. package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.d.ts +18 -0
  328. package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.d.ts.map +1 -0
  329. package/src/engine/sound/sopra/definition/MixerSnapshotSerializationAdapter.js +39 -0
  330. package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts +72 -0
  331. package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts.map +1 -0
  332. package/src/engine/sound/sopra/definition/ParameterDefinition.js +117 -0
  333. package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.d.ts +18 -0
  334. package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.d.ts.map +1 -0
  335. package/src/engine/sound/sopra/definition/ParameterDefinitionSerializationAdapter.js +31 -0
  336. package/src/engine/sound/sopra/definition/SopraPanningModel.d.ts +14 -0
  337. package/src/engine/sound/sopra/definition/SopraPanningModel.d.ts.map +1 -0
  338. package/src/engine/sound/sopra/definition/SopraPanningModel.js +20 -0
  339. package/src/engine/sound/sopra/definition/VoiceStealMode.d.ts +10 -0
  340. package/src/engine/sound/sopra/definition/VoiceStealMode.d.ts.map +1 -0
  341. package/src/engine/sound/sopra/definition/VoiceStealMode.js +18 -0
  342. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts +93 -0
  343. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts.map +1 -0
  344. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +109 -0
  345. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts +80 -0
  346. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -0
  347. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.js +181 -0
  348. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.d.ts +17 -0
  349. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.d.ts.map +1 -0
  350. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClipSerializationAdapter.js +74 -0
  351. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.d.ts +34 -0
  352. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.d.ts.map +1 -0
  353. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.js +100 -0
  354. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +101 -0
  355. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -0
  356. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +230 -0
  357. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.d.ts +17 -0
  358. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.d.ts.map +1 -0
  359. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClipSerializationAdapter.js +54 -0
  360. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts +103 -0
  361. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -0
  362. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +191 -0
  363. package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.d.ts +18 -0
  364. package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.d.ts.map +1 -0
  365. package/src/engine/sound/sopra/definition/clip/SampleAudioClipSerializationAdapter.js +39 -0
  366. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts +40 -0
  367. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -0
  368. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.js +91 -0
  369. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.d.ts +17 -0
  370. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.d.ts.map +1 -0
  371. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClipSerializationAdapter.js +42 -0
  372. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.d.ts +44 -0
  373. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.d.ts.map +1 -0
  374. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.js +77 -0
  375. package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.d.ts +18 -0
  376. package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.d.ts.map +1 -0
  377. package/src/engine/sound/sopra/definition/clip/SilenceAudioClipSerializationAdapter.js +27 -0
  378. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +65 -0
  379. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -0
  380. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.js +131 -0
  381. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.d.ts +17 -0
  382. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.d.ts.map +1 -0
  383. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClipSerializationAdapter.js +41 -0
  384. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +24 -0
  385. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -0
  386. package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +24 -0
  387. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +70 -0
  388. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -0
  389. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +120 -0
  390. package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.d.ts +18 -0
  391. package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.d.ts.map +1 -0
  392. package/src/engine/sound/sopra/definition/effect/CompressorEffectSerializationAdapter.js +31 -0
  393. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +74 -0
  394. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -0
  395. package/src/engine/sound/sopra/definition/effect/EqEffect.js +128 -0
  396. package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.d.ts +18 -0
  397. package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.d.ts.map +1 -0
  398. package/src/engine/sound/sopra/definition/effect/EqEffectSerializationAdapter.js +29 -0
  399. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +49 -0
  400. package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -0
  401. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +101 -0
  402. package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.d.ts +18 -0
  403. package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.d.ts.map +1 -0
  404. package/src/engine/sound/sopra/definition/effect/ReverbEffectSerializationAdapter.js +25 -0
  405. package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.d.ts +31 -0
  406. package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.d.ts.map +1 -0
  407. package/src/engine/sound/sopra/legacy/soundEmitterToEventDescription.js +106 -0
  408. package/src/engine/sound/sopra/runtime/BusGraph.d.ts +79 -0
  409. package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -0
  410. package/src/engine/sound/sopra/runtime/BusGraph.js +227 -0
  411. package/src/engine/sound/sopra/runtime/EventInstance.d.ts +144 -0
  412. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -0
  413. package/src/engine/sound/sopra/runtime/EventInstance.js +579 -0
  414. package/src/engine/sound/sopra/runtime/ParameterStore.d.ts +42 -0
  415. package/src/engine/sound/sopra/runtime/ParameterStore.d.ts.map +1 -0
  416. package/src/engine/sound/sopra/runtime/ParameterStore.js +98 -0
  417. package/src/engine/sound/sopra/runtime/SopraPlaybackContext.d.ts +42 -0
  418. package/src/engine/sound/sopra/runtime/SopraPlaybackContext.d.ts.map +1 -0
  419. package/src/engine/sound/sopra/runtime/SopraPlaybackContext.js +68 -0
  420. package/src/engine/sound/sopra/runtime/Voice.d.ts +67 -0
  421. package/src/engine/sound/sopra/runtime/Voice.d.ts.map +1 -0
  422. package/src/engine/sound/sopra/runtime/Voice.js +145 -0
  423. package/src/engine/sound/sopra/runtime/VoiceManager.d.ts +38 -0
  424. package/src/engine/sound/sopra/runtime/VoiceManager.d.ts.map +1 -0
  425. package/src/engine/sound/sopra/runtime/VoiceManager.js +136 -0
  426. package/src/engine/sound/sopra/runtime/VoicePool.d.ts +12 -0
  427. package/src/engine/sound/sopra/runtime/VoicePool.d.ts.map +1 -0
  428. package/src/engine/sound/sopra/runtime/VoicePool.js +17 -0
  429. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +11 -0
  430. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -0
  431. package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +42 -0
  432. package/src/engine/sound/sopra/serialization/sopraJSON.d.ts +33 -0
  433. package/src/engine/sound/sopra/serialization/sopraJSON.d.ts.map +1 -0
  434. package/src/engine/sound/sopra/serialization/sopraJSON.js +99 -0
  435. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +27 -0
  436. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -0
  437. package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +49 -0
  438. package/src/engine/sound/sopra/util/MockAudioContext.d.ts +74 -0
  439. package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -0
  440. package/src/engine/sound/sopra/util/MockAudioContext.js +215 -0
  441. package/src/engine/sound/sopra/util/buildAttenuationCurve.d.ts +15 -0
  442. package/src/engine/sound/sopra/util/buildAttenuationCurve.d.ts.map +1 -0
  443. package/src/engine/sound/sopra/util/buildAttenuationCurve.js +40 -0
  444. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +34 -0
  445. package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -0
  446. package/src/engine/sound/sopra/util/fadeOutAndStop.js +60 -0
  447. package/src/engine/sound/volume2dB.d.ts +1 -1
  448. package/src/engine/sound/volume2dB.d.ts.map +1 -1
  449. package/src/engine/sound/volume2dB.js +1 -1
  450. package/src/engine/graphics/sh3/path_tracer/sampling/v3_orthonormal_matrix_from_normal.d.ts.map +0 -1
  451. package/src/engine/physics/narrowphase/ray_shapes.d.ts +0 -66
  452. package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +0 -1
  453. package/src/engine/physics/narrowphase/ray_shapes.js +0 -187
  454. package/src/engine/sound/ecs/emitter/SoundEmitterChannel.d.ts +0 -23
  455. package/src/engine/sound/ecs/emitter/SoundEmitterChannel.d.ts.map +0 -1
  456. package/src/engine/sound/ecs/emitter/SoundEmitterChannel.js +0 -32
  457. package/src/engine/sound/ecs/emitter/SoundTrackNodes.d.ts +0 -18
  458. package/src/engine/sound/ecs/emitter/SoundTrackNodes.d.ts.map +0 -1
  459. package/src/engine/sound/ecs/emitter/SoundTrackNodes.js +0 -18
  460. package/src/engine/sound/sopra/AbstractAudioClip.d.ts +0 -26
  461. package/src/engine/sound/sopra/AbstractAudioClip.d.ts.map +0 -1
  462. package/src/engine/sound/sopra/AbstractAudioClip.js +0 -29
  463. package/src/engine/sound/sopra/ContainerAudioClip.d.ts +0 -12
  464. package/src/engine/sound/sopra/ContainerAudioClip.d.ts.map +0 -1
  465. package/src/engine/sound/sopra/ContainerAudioClip.js +0 -13
  466. package/src/engine/sound/sopra/RandomContainerAudioClip.d.ts +0 -12
  467. package/src/engine/sound/sopra/RandomContainerAudioClip.d.ts.map +0 -1
  468. package/src/engine/sound/sopra/RandomContainerAudioClip.js +0 -15
  469. package/src/engine/sound/sopra/SequenceContainerAudioClip.d.ts +0 -7
  470. package/src/engine/sound/sopra/SequenceContainerAudioClip.d.ts.map +0 -1
  471. package/src/engine/sound/sopra/SequenceContainerAudioClip.js +0 -8
  472. package/src/engine/sound/sopra/SilenceAudioClip.d.ts +0 -13
  473. package/src/engine/sound/sopra/SilenceAudioClip.d.ts.map +0 -1
  474. package/src/engine/sound/sopra/SilenceAudioClip.js +0 -15
  475. /package/src/{engine/graphics/sh3/path_tracer/sampling → core/geom/vec3}/v3_orthonormal_matrix_from_normal.d.ts +0 -0
@@ -1,1747 +1,1750 @@
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 { returnTrue } from "../../../core/function/returnTrue.js";
5
- import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
6
- import Vector3 from "../../../core/geom/Vector3.js";
7
- import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
8
- import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
9
- import { System } from "../../ecs/System.js";
10
- import { Transform } from "../../ecs/transform/Transform.js";
11
- import { Interpolated } from "../../interpolation/Interpolated.js";
12
- import { body_id_index, BodyStorage } from "../body/BodyStorage.js";
13
- import { SBS_STRIDE, SolverBodyState } from "../body/SolverBodyState.js";
14
- import { compute_fat_world_aabb } from "../broadphase/compute_fat_world_aabb.js";
15
- import { generate_pairs } from "../broadphase/generate_pairs.js";
16
- import { PairList } from "../broadphase/PairList.js";
17
- import { ccd_resolve } from "../ccd/linear_sweep.js";
18
- import { solve_joints } from "../constraint/solve_constraints.js";
19
- import { ManifoldStore } from "../contact/ManifoldStore.js";
20
- import { ContactEventBuffer, ContactEventKind } from "../events/ContactEventBuffer.js";
21
- import { diff_manifolds } from "../events/diff_manifolds.js";
22
- import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
23
- import { integrate_position } from "../integration/integrate_position.js";
24
- import { integrate_velocity_forces, integrate_velocity_gravity } from "../integration/integrate_velocity.js";
25
- import { IslandBuilder } from "../island/IslandBuilder.js";
26
- import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
27
- import { overlap_shape as overlap_shape_query } from "../queries/overlap_shape.js";
28
- import { raycast as raycast_query } from "../queries/raycast.js";
29
- import { shape_cast as shape_cast_query } from "../queries/shape_cast.js";
30
- import {
31
- apply_restitution,
32
- prepare_contacts,
33
- redetect_concave_contacts,
34
- refresh_contacts,
35
- solve_position,
36
- solve_velocity,
37
- warm_start_contacts,
38
- } from "../solver/solve_contacts.js";
39
- import { BodyKind } from "./BodyKind.js";
40
- import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
41
- import { JOINT_UNALLOCATED, JOINT_WORLD } from "./Joint.js";
42
- import { PhysicsEvents } from "./PhysicsEvents.js";
43
- import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
44
- import { RigidBodyFlags } from "./RigidBodyFlags.js";
45
- import { SleepState } from "./SleepState.js";
46
-
47
- /**
48
- * Scratch for {@link applyImpulseAt}'s angular delta calculation.
49
- * @type {Float64Array}
50
- */
51
- const scratch_angular_delta = new Float64Array(3);
52
-
53
- /**
54
- * Reusable scratch buffer for world-AABB construction so the link path is
55
- * allocation-free in steady state.
56
- * @type {Float64Array}
57
- */
58
- const scratch_world_aabb = new Float64Array(6);
59
-
60
- /**
61
- * Reusable scratch buffer for the local AABB returned by
62
- * {@link AbstractShape3D#compute_bounding_box}.
63
- * @type {Float64Array}
64
- */
65
- const scratch_local_aabb = new Float64Array(6);
66
-
67
- /**
68
- * Rigid-body physics system.
69
- *
70
- * v1 scope: pool + active list + two BVHs (static / dynamic), Transform sync
71
- * contract, and the user-facing API surface (gravity, force / impulse, layer
72
- * filtering, wake / sleep, contact filter). The per-step simulation pipeline
73
- * (broadphase pair generation, narrowphase, solver, islands, sleep test,
74
- * contact-event emission) is built on top of this skeleton in subsequent
75
- * iterations.
76
- *
77
- * Dependency tuple is `(RigidBody, Collider, Transform)` — every body has
78
- * exactly one collider on the same entity. Compound bodies via child collider
79
- * entities are an extension point handled by a follow-up sub-observer.
80
- *
81
- * @author Alex Goldring
82
- * @copyright Company Named Limited (c) 2026
83
- */
84
- export class PhysicsSystem extends System {
85
-
86
- constructor() {
87
- super();
88
-
89
- this.dependencies = [RigidBody, Transform];
90
-
91
- this.components_used = [
92
- ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Read | ResourceAccessKind.Write),
93
- ResourceAccessSpecification.from(Collider, ResourceAccessKind.Read),
94
- ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read | ResourceAccessKind.Write),
95
- ];
96
-
97
- /**
98
- * @type {BodyStorage}
99
- * @readonly
100
- */
101
- this.storage = new BodyStorage();
102
-
103
- /**
104
- * @type {BVH}
105
- */
106
- this.staticBvh = new BVH();
107
-
108
- /**
109
- * @type {BVH}
110
- */
111
- this.dynamicBvh = new BVH();
112
-
113
- /**
114
- * Persistent contact-manifold cache. One slot per active pair.
115
- * @type {ManifoldStore}
116
- * @readonly
117
- */
118
- this.manifolds = new ManifoldStore();
119
-
120
- /**
121
- * Per-frame list of broadphase-overlapping pairs (canonical
122
- * `(min, max)`). Cleared at the top of each step.
123
- * @type {PairList}
124
- */
125
- this.pairs = new PairList();
126
-
127
- /**
128
- * Per-frame contact-event buffer. Populated by the manifold diff
129
- * pass at end-of-step and consumed by the dispatch pass.
130
- * @type {ContactEventBuffer}
131
- */
132
- this.contactEvents = new ContactEventBuffer();
133
-
134
- /**
135
- * Per-frame island partitioning of the awake-body + contact graph.
136
- * Rebuilt after narrowphase, consumed by the solver (and, in the
137
- * follow-up slice, by the per-island atomic sleep test). Bodies
138
- * static and kinematic act as constraint anchors and do not enlarge
139
- * islands.
140
- * @type {IslandBuilder}
141
- */
142
- this.islands = new IslandBuilder();
143
-
144
- /**
145
- * Data-oriented mirror of the per-body solver hot state (velocity,
146
- * inverse mass / inertia, orientation), packed into one ArrayBuffer
147
- * and indexed by body slot. Gathered from the `RigidBody` /
148
- * `Transform` components once per step (after islands are built),
149
- * mutated in place by the TGS substep loop, and the persistent
150
- * velocity scattered back at the end. Keeps the solver's hottest inner
151
- * loop free of component-object dereferences.
152
- * @type {SolverBodyState}
153
- */
154
- this.__solver_state = new SolverBodyState();
155
-
156
- /**
157
- * Velocity-squared threshold below which a body is eligible to start
158
- * accumulating sleep time. Combined linear + angular kinetic-ish
159
- * metric: `vx²+vy²+vz² + ωx²+ωy²+ωz²`. Default 0.01 corresponds to
160
- * ~0.1 m/s linear or ~0.1 rad/s angular.
161
- * @type {number}
162
- */
163
- this.sleepVelocitySqrThreshold = 0.01;
164
-
165
- /**
166
- * Seconds of below-threshold motion before a body is moved to the
167
- * sleeping set. Box2D default is 0.5 s.
168
- * @type {number}
169
- */
170
- this.sleepTimeThreshold = 0.5;
171
-
172
- /**
173
- * Number of TGS substeps per `fixedUpdate`. Each substep re-runs the
174
- * velocity + position solve at `dt / substeps` against contacts whose
175
- * penetration is re-derived analytically from the bodies' moved poses
176
- * — narrowphase still runs once per outer step. Higher counts buy
177
- * stack stability, high-mass-ratio robustness, and smoother
178
- * trajectories at a near-linear solver cost (sleeping islands are
179
- * unaffected — they never enter the loop).
180
- *
181
- * `1` reproduces the non-substepped (single-step) solve.
182
- * @type {number}
183
- */
184
- this.substeps = 4;
185
-
186
- /**
187
- * Velocity iterations per substep. Lower than a single-step solver
188
- * would need, because the substep loop revisits the contact set
189
- * `substeps` times.
190
- * @type {number}
191
- */
192
- this.velocityIterations = 4;
193
-
194
- /**
195
- * Position (split-impulse) iterations per substep.
196
- * @type {number}
197
- */
198
- this.positionIterations = 1;
199
-
200
- /**
201
- * Reusable contact-event payload. Listeners must copy any fields they
202
- * intend to retain past their own scope. Reset before each dispatch.
203
- * @private
204
- */
205
- this.__contact_payload = {
206
- entityA: -1,
207
- entityB: -1,
208
- kind: 0,
209
- depth: 0,
210
- normal: new Vector3(),
211
- point: new Vector3(),
212
- };
213
-
214
- /**
215
- * World gravity, m/s². Applied each step scaled by per-body gravityScale.
216
- * @readonly
217
- * @type {Vector3}
218
- */
219
- this.gravity = new Vector3(0, -9.81, 0);
220
-
221
- /**
222
- * Optional global contact filter. Called for each surviving broadphase
223
- * pair; returning `false` discards the pair.
224
- * @type {((entityA: number, entityB: number, colliderA: Collider, colliderB: Collider) => boolean) | null}
225
- * @private
226
- */
227
- this.__contact_filter = null;
228
-
229
- /**
230
- * Per-body component side-tables indexed by body index (NOT packed id).
231
- * Sparse arrays — populated at link, cleared at unlink. The hot loop
232
- * iterates `storage.awake_at(i)` and dereferences these.
233
- * @type {RigidBody[]}
234
- */
235
- this.__bodies = [];
236
-
237
- /**
238
- * @type {Transform[]}
239
- * @readonly
240
- */
241
- this.__transforms = [];
242
-
243
- /**
244
- * Per-body list of attached colliders. Each entry stores the
245
- * Collider component, its world Transform, the entity that owns
246
- * it (the body entity for same-entity attachments, a child entity
247
- * for compound bodies), and the BVH leaf id assigned at attach
248
- * time. A body may have zero or more attached colliders.
249
- *
250
- * @type {Array<Array<{collider: Collider, transform: Transform, entity: number, bvhNode: number}>>}
251
- */
252
- this.__body_collider_lists = [];
253
-
254
- /**
255
- * Live {@link Joint} (6-DOF constraint) instances, in a sparse array
256
- * indexed by joint id. Solved alongside contacts inside the TGS
257
- * substep loop. Holes (unlinked joints) are `undefined`.
258
- * @type {Joint[]}
259
- */
260
- this.__joints = [];
261
-
262
- /**
263
- * Lowest free index in {@link __joints} for slot reuse on link.
264
- * @private
265
- * @type {number}
266
- */
267
- this.__joint_free = [];
268
-
269
- /**
270
- * Velocity iterations per substep for the joint solver. Joints (and
271
- * especially joint chains) want a few more iterations than contacts to
272
- * propagate impulses along the chain; cheap because joints are far
273
- * fewer than contacts.
274
- * @type {number}
275
- */
276
- this.jointIterations = 8;
277
-
278
- /**
279
- * Per-body pseudo-velocity for the Catto split-impulse position
280
- * pass (TGS Phase 1). Flat layout, 6 doubles per body slot index:
281
- * `[lin.x, lin.y, lin.z, ang.x, ang.y, ang.z]`. Sized to
282
- * `storage.high_water_mark * 6` at the top of each fixedUpdate
283
- * and zeroed in place so unwritten slots contribute nothing to
284
- * `integrate_position`.
285
- *
286
- * The solver writes during its position pass; `integrate_position`
287
- * reads and folds into the pose update on the same tick, then
288
- * the next tick's zero-pass wipes the state. It NEVER lands in
289
- * `linearVelocity` / `angularVelocity` — that's the point of
290
- * split impulse: depth correction does not contaminate persistent
291
- * velocity (so a `restitution = 0` impact stops cleanly).
292
- *
293
- * @type {Float64Array}
294
- */
295
- this.__pseudo_velocity = new Float64Array(0);
296
-
297
- /**
298
- * Master switch for the continuous-collision pass. When false the
299
- * {@link RigidBodyFlags.CCD} flag is ignored and no swept queries run.
300
- * @type {boolean}
301
- */
302
- this.ccdEnabled = true;
303
-
304
- /**
305
- * Start-of-step world positions for CCD-flagged bodies — 3 doubles per
306
- * body slot index (`[x, y, z]`). Captured in Stage 1 before the substep
307
- * loop integrates poses; the CCD pass ({@link ccd_resolve}) sweeps from
308
- * here to the final pose. Grows to `storage.high_water_mark * 3`; only
309
- * CCD-flagged slots are written each step.
310
- * @type {Float64Array}
311
- */
312
- this.__ccd_start_pos = new Float64Array(0);
313
-
314
-
315
- /**
316
- * Optional shared interpolation log to produce per-step pose snapshots
317
- * into (the {@link InterpolationSystem}'s log). When set — and only then —
318
- * each fixedUpdate restores every awake {@link Interpolated} body's live
319
- * components from the previous tick (undoing render-time interpolation so
320
- * the sim reads authoritative state), then records the post-step state
321
- * under the current `entityManager.fixedStepTick`. Null on a headless /
322
- * non-rendering world, where the producer work is skipped entirely.
323
- * @type {InterpolationLog|null}
324
- */
325
- this.interpolationLog = null;
326
-
327
- /**
328
- * Reusable decode buffer for restoring interpolated snapshots.
329
- * @private
330
- * @type {BinaryBuffer}
331
- */
332
- this.__interp_scratch = new BinaryBuffer();
333
- this.__interp_scratch.fromArrayBuffer(new ArrayBuffer(256));
334
- }
335
-
336
- /**
337
- * Symmetric layer/mask check + optional user callback. Called per
338
- * candidate pair during broadphase. Returns `true` to accept the pair.
339
- *
340
- * Layer/mask rule: pair (A, B) collides iff
341
- * `(A.layer & B.mask) !== 0 && (B.layer & A.mask) !== 0`.
342
- * The user's contact-filter callback is consulted only for pairs that
343
- * pass the bitmask test (cheap gate first).
344
- *
345
- * @private
346
- * @param {number} idA packed body id
347
- * @param {number} idB packed body id
348
- * @returns {boolean}
349
- */
350
- __pair_filter(idA, idB) {
351
- const idxA = body_id_index(idA);
352
- const idxB = body_id_index(idB);
353
-
354
- const rbA = this.__bodies[idxA];
355
- const rbB = this.__bodies[idxB];
356
-
357
- if (rbA === undefined || rbB === undefined) {
358
- return false;
359
- }
360
-
361
- // Layer/mask gate (symmetric).
362
- if (((rbA.layer & rbB.mask) | 0) === 0) {
363
- return false;
364
- }
365
- if (((rbB.layer & rbA.mask) | 0) === 0) {
366
- return false;
367
- }
368
-
369
- // User callback gate, if installed.
370
- const fn = this.__contact_filter;
371
- if (fn !== null) {
372
- const entA = this.storage.entity_at(idxA);
373
- const entB = this.storage.entity_at(idxB);
374
- const colA = this.__primary_collider(idxA);
375
- const colB = this.__primary_collider(idxB);
376
- if (!fn(entA, entB, colA, colB)) return false;
377
- }
378
- return true;
379
- }
380
-
381
- /**
382
- * First attached collider of a body, or `null` if none. Used for
383
- * body-level checks that aren't yet per-collider: the sensor flag, the
384
- * concave-shape dispatch flag, and the contact-filter callback's
385
- * `colliderA` / `colliderB` arguments. Contact **materials** (friction /
386
- * restitution) are NOT read here — the narrowphase tracks the specific
387
- * source collider on each side of every contact and stamps the combined
388
- * coefficients into the manifold per contact, so mixed-material compound
389
- * bodies are accurate. (Per-contact source colliders for the filter
390
- * callback / sensor flag remain a follow-up.)
391
- *
392
- * @private
393
- * @param {number} body_idx
394
- * @returns {Collider|null}
395
- */
396
- __primary_collider(body_idx) {
397
- const list = this.__body_collider_lists[body_idx];
398
- return (list !== undefined && list.length > 0) ? list[0].collider : null;
399
- }
400
-
401
- /**
402
- * Resize {@link __pseudo_velocity} to cover every live body slot and
403
- * zero its contents. Called at the top of each fixedUpdate so the
404
- * split-impulse position pass starts from a clean state — bodies the
405
- * solver doesn't touch contribute zero pseudo-velocity to
406
- * {@link integrate_position}.
407
- *
408
- * Doubles on growth like the other physics scratches; never shrinks.
409
- *
410
- * @private
411
- */
412
- __reset_pseudo_velocity() {
413
- const required = this.storage.high_water_mark * 6;
414
- if (this.__pseudo_velocity.length < required) {
415
- this.__pseudo_velocity = new Float64Array(required * 2);
416
- } else if (required > 0) {
417
- this.__pseudo_velocity.fill(0, 0, required);
418
- }
419
- }
420
-
421
- /**
422
- * Replace the world gravity vector. Effective on the next step.
423
- * @param {Vector3|{x:number,y:number,z:number}} v
424
- */
425
- setGravity(v) {
426
- this.gravity.set(v.x, v.y, v.z);
427
- }
428
-
429
- /**
430
- * Install (or remove with `null`) the contact filter callback.
431
- * @param {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null} fn
432
- */
433
- setContactFilter(fn) {
434
- this.__contact_filter = fn;
435
- }
436
-
437
- /**
438
- * @returns {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null}
439
- */
440
- getContactFilter() {
441
- return this.__contact_filter;
442
- }
443
-
444
- /**
445
- * @private
446
- * @param {RigidBody} rb
447
- * @param {Collider} collider
448
- * @param {Transform} transform
449
- * @returns {number} BVH node id
450
- */
451
- __insert_into_broadphase(rb, collider, transform) {
452
- const shape = collider.shape;
453
-
454
- assert.notNull(shape, 'Collider.shape must be set before attaching');
455
-
456
- shape.compute_bounding_box(scratch_local_aabb);
457
-
458
- const p = transform.position;
459
- const q = transform.rotation;
460
-
461
- aabb3_transform_oriented(
462
- scratch_world_aabb, 0,
463
- scratch_local_aabb[0], scratch_local_aabb[1], scratch_local_aabb[2],
464
- scratch_local_aabb[3], scratch_local_aabb[4], scratch_local_aabb[5],
465
- p.x, p.y, p.z,
466
- q.x, q.y, q.z, q.w
467
- );
468
-
469
- const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
470
- const node = bvh.allocate_node();
471
-
472
- bvh.node_set_aabb_primitive(
473
- node,
474
- scratch_world_aabb[0], scratch_world_aabb[1], scratch_world_aabb[2],
475
- scratch_world_aabb[3], scratch_world_aabb[4], scratch_world_aabb[5]
476
- );
477
- bvh.node_set_user_data(node, rb._bodyId);
478
- bvh.insert_leaf(node);
479
-
480
- return node;
481
- }
482
-
483
- /**
484
- * @private
485
- * @param {RigidBody} rb
486
- * @param {number} node
487
- */
488
- __remove_from_broadphase(rb, node) {
489
- const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
490
- bvh.remove_leaf(node);
491
- bvh.release_node(node);
492
- }
493
-
494
- /**
495
- * Lifecycle entry: invoked when an entity gains the (RigidBody, Transform)
496
- * tuple. Allocates the body's slot in storage and seeds an empty collider
497
- * list. Colliders are attached separately via {@link attach_collider} (the
498
- * paired {@link ColliderObserverSystem} drives this from the dataset; in
499
- * tests, call manually).
500
- *
501
- * @param {RigidBody} rigidBody
502
- * @param {Transform} transform
503
- * @param {number} entity
504
- */
505
- link(rigidBody, transform, entity) {
506
- const packed = this.storage.allocate(entity);
507
- rigidBody._bodyId = packed;
508
- rigidBody.sleepState = SleepState.Awake;
509
-
510
- const index = body_id_index(packed);
511
- this.storage.set_kind(index, rigidBody.kind);
512
- this.storage.set_flags(index, rigidBody.flags);
513
-
514
- this.__bodies[index] = rigidBody;
515
- this.__transforms[index] = transform;
516
- this.__body_collider_lists[index] = [];
517
-
518
- // Static bodies do not need to live in the active list — they never move.
519
- if (rigidBody.kind === BodyKind.Static) {
520
- this.storage.mark_sleeping(index);
521
- }
522
- }
523
-
524
- /**
525
- * Detach every collider attached to this body, free its slot, and clear
526
- * the side-tables.
527
- *
528
- * @param {RigidBody} rigidBody
529
- * @param {Transform} transform
530
- * @param {number} entity
531
- */
532
- unlink(rigidBody, transform, entity) {
533
- const packed = rigidBody._bodyId;
534
-
535
- assert.equal(this.storage.is_valid(packed), true, 'unlink: stale or absent body id');
536
-
537
- // If the body is sleeping inside a multi-member sleep group, dissolve
538
- // the group by waking every member first. Otherwise the surviving
539
- // members would hold dangling indices in their sleep-group chain
540
- // pointing at a slot that has just been freed.
541
- if (rigidBody.sleep_group_next !== -1) {
542
- this.__wake_body(rigidBody);
543
- }
544
-
545
- const index = body_id_index(packed);
546
- const list = this.__body_collider_lists[index];
547
- if (list !== undefined) {
548
- for (let i = 0; i < list.length; i++) {
549
- const entry = list[i];
550
- if (entry.bvhNode !== COLLIDER_UNBOUND) {
551
- this.__remove_from_broadphase(rigidBody, entry.bvhNode);
552
- }
553
- entry.collider._bvhNode = COLLIDER_UNBOUND;
554
- entry.collider._bodyId = -1;
555
- }
556
- }
557
-
558
- this.__bodies[index] = undefined;
559
- this.__transforms[index] = undefined;
560
- this.__body_collider_lists[index] = undefined;
561
-
562
- this.storage.free(packed);
563
- rigidBody._bodyId = RIGID_BODY_UNALLOCATED;
564
- }
565
-
566
- /**
567
- * Attach a collider to an existing body. The collider can live on the
568
- * same entity as the body (single-collider body) or on a child entity
569
- * (compound body). The world transform passed here is the collider's
570
- * own — for a same-entity collider it is the body's Transform; for a
571
- * child collider it is the child entity's Transform.
572
- *
573
- * Idempotent — re-attaching the same collider is a no-op.
574
- *
575
- * @param {number} body_entity entity that owns the body
576
- * @param {Collider} collider
577
- * @param {Transform} transform world transform of the collider
578
- * @param {number} [collider_entity] entity owning the collider (defaults to body_entity)
579
- */
580
- attach_collider(body_entity, collider, transform, collider_entity = body_entity) {
581
- // Find the body by walking the storage entity table. The body must
582
- // have been allocated via `link` before any colliders are attached.
583
- const body_index = this.__find_body_index_by_entity(body_entity);
584
- assert.notEqual(body_index, -1, `attach_collider: no body found for entity ${body_entity}`);
585
-
586
- const rb = this.__bodies[body_index];
587
- // Idempotent: skip if collider already attached.
588
- if (collider._bvhNode !== COLLIDER_UNBOUND) return;
589
-
590
- const node = this.__insert_into_broadphase(rb, collider, transform);
591
-
592
- collider._bvhNode = node;
593
- collider._bodyId = rb._bodyId;
594
- this.__body_collider_lists[body_index].push({
595
- collider, transform, entity: collider_entity, bvhNode: node,
596
- });
597
- }
598
-
599
- /**
600
- * Reverse of {@link attach_collider}. Idempotent.
601
- *
602
- * @param {number} body_entity
603
- * @param {Collider} collider
604
- */
605
- detach_collider(body_entity, collider) {
606
- if (collider._bvhNode === COLLIDER_UNBOUND) return;
607
-
608
- const body_index = this.__find_body_index_by_entity(body_entity);
609
- if (body_index === -1) return;
610
-
611
- const rb = this.__bodies[body_index];
612
- this.__remove_from_broadphase(rb, collider._bvhNode);
613
-
614
- const list = this.__body_collider_lists[body_index];
615
- for (let i = 0; i < list.length; i++) {
616
- if (list[i].collider === collider) {
617
- list.splice(i, 1);
618
- break;
619
- }
620
- }
621
-
622
- collider._bvhNode = COLLIDER_UNBOUND;
623
- collider._bodyId = -1;
624
- }
625
-
626
- /**
627
- * Resolve an entity to its body index, or -1 if no live body owns it.
628
- * O(1) via {@link BodyStorage#index_of_entity}'s entity → index map — used
629
- * on the collider attach / detach and joint link paths.
630
- *
631
- * @private
632
- * @param {number} entity
633
- * @returns {number} body index or -1
634
- */
635
- __find_body_index_by_entity(entity) {
636
- return this.storage.index_of_entity(entity);
637
- }
638
-
639
- /**
640
- * Register a {@link Joint} (6-DOF constraint). Resolves the joint's
641
- * entities to packed body ids and adds it to the active set, where it is
642
- * solved alongside contacts in the TGS substep loop. Body A must be a
643
- * linked body; body B is either a linked body or {@link JOINT_WORLD}
644
- * (anchoring A to a fixed world point — `localAnchorB` is then a world
645
- * coordinate).
646
- *
647
- * @param {Joint} joint
648
- */
649
- link_joint(joint) {
650
- const idxA = this.__find_body_index_by_entity(joint.entityA);
651
- assert.notEqual(idxA, -1, `link_joint: no body for entityA ${joint.entityA}`);
652
- joint._bodyIdA = this.__bodies[idxA]._bodyId;
653
-
654
- if (joint.entityB === JOINT_WORLD) {
655
- joint._bodyIdB = JOINT_WORLD;
656
- } else {
657
- const idxB = this.__find_body_index_by_entity(joint.entityB);
658
- assert.notEqual(idxB, -1, `link_joint: no body for entityB ${joint.entityB}`);
659
- joint._bodyIdB = this.__bodies[idxB]._bodyId;
660
- }
661
-
662
- // Reuse a freed slot if available so joint ids stay dense-ish.
663
- let id;
664
- if (this.__joint_free.length > 0) {
665
- id = this.__joint_free.pop();
666
- } else {
667
- id = this.__joints.length;
668
- }
669
- joint._jointId = id;
670
- this.__joints[id] = joint;
671
- }
672
-
673
- /**
674
- * Remove a previously {@link link_joint}'d joint from the active set.
675
- * Idempotent.
676
- * @param {Joint} joint
677
- */
678
- unlink_joint(joint) {
679
- const id = joint._jointId;
680
- if (id === JOINT_UNALLOCATED || this.__joints[id] !== joint) return;
681
- this.__joints[id] = undefined;
682
- this.__joint_free.push(id);
683
- joint._jointId = JOINT_UNALLOCATED;
684
- }
685
-
686
- /**
687
- * Resolve a packed body id to its entity, or `-1` if the id is stale.
688
- * @param {number} packed_body_id
689
- * @returns {number}
690
- */
691
- entityOf(packed_body_id) {
692
- if (!this.storage.is_valid(packed_body_id)) return -1;
693
- return this.storage.entity_at(body_id_index(packed_body_id));
694
- }
695
-
696
- /**
697
- * Number of live bodies (regardless of awake/sleeping state).
698
- * @returns {number}
699
- */
700
- get bodyCount() {
701
- return this.storage.size;
702
- }
703
-
704
- /**
705
- * Apply an instantaneous change of momentum at the body's centre of mass.
706
- * Linear-only — see {@link applyImpulseAt} for an off-centre impulse that
707
- * also produces angular response.
708
- *
709
- * Wakes the body if it is asleep.
710
- *
711
- * @param {RigidBody} rigidBody
712
- * @param {Vector3|{x:number,y:number,z:number}} impulse
713
- */
714
- applyImpulse(rigidBody, impulse) {
715
- if (rigidBody.kind !== BodyKind.Dynamic) {
716
- return;
717
- }
718
- const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
719
- rigidBody.linearVelocity.addScaled(impulse, inv_m);
720
-
721
- this.__wake_body(rigidBody);
722
- }
723
-
724
- /**
725
- * Apply an instantaneous change of momentum at a specific world-space point.
726
- * Off-centre impulses produce both linear (Δv = P/m) and angular
727
- * (Δω = I_w⁻¹·(r × P)) response.
728
- *
729
- * Wakes the body if it is asleep.
730
- *
731
- * @param {RigidBody} rigidBody
732
- * @param {Transform} transform body's current world Transform (used for r and I_w)
733
- * @param {Vector3|{x:number,y:number,z:number}} impulse
734
- * @param {Vector3|{x:number,y:number,z:number}} worldPoint
735
- */
736
- applyImpulseAt(rigidBody, transform, impulse, worldPoint) {
737
- if (rigidBody.kind !== BodyKind.Dynamic) {
738
- return;
739
- }
740
- const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
741
-
742
- rigidBody.linearVelocity.set(
743
- rigidBody.linearVelocity.x + impulse.x * inv_m,
744
- rigidBody.linearVelocity.y + impulse.y * inv_m,
745
- rigidBody.linearVelocity.z + impulse.z * inv_m
746
- );
747
-
748
- const rx = worldPoint.x - transform.position.x;
749
- const ry = worldPoint.y - transform.position.y;
750
- const rz = worldPoint.z - transform.position.z;
751
-
752
- // Δω = I_w⁻¹ · (r × P)
753
- const tx = ry * impulse.z - rz * impulse.y;
754
- const ty = rz * impulse.x - rx * impulse.z;
755
- const tz = rx * impulse.y - ry * impulse.x;
756
-
757
- world_inverse_inertia_apply(scratch_angular_delta, 0, rigidBody.inverseInertiaLocal, transform.rotation, tx, ty, tz);
758
-
759
- rigidBody.angularVelocity.set(
760
- rigidBody.angularVelocity.x + scratch_angular_delta[0],
761
- rigidBody.angularVelocity.y + scratch_angular_delta[1],
762
- rigidBody.angularVelocity.z + scratch_angular_delta[2]
763
- );
764
-
765
- this.__wake_body(rigidBody);
766
- }
767
-
768
- /**
769
- * Accumulate a continuous torque (world-space) for integration on the
770
- * next fixedUpdate. Pairs with {@link applyForce} for the rotational case.
771
- *
772
- * Wakes the body if asleep.
773
- *
774
- * @param {RigidBody} rigidBody
775
- * @param {Vector3|{x:number,y:number,z:number}} torque
776
- */
777
- applyTorque(rigidBody, torque) {
778
- if (rigidBody.kind !== BodyKind.Dynamic) {
779
- return;
780
- }
781
- rigidBody.accumulatedTorque.set(
782
- rigidBody.accumulatedTorque.x + torque.x,
783
- rigidBody.accumulatedTorque.y + torque.y,
784
- rigidBody.accumulatedTorque.z + torque.z
785
- );
786
- this.__wake_body(rigidBody);
787
- }
788
-
789
- /**
790
- * Apply a continuous force at a specific world-space point. The force
791
- * generates both a linear acceleration (F/m) and a torque (r × F) about
792
- * the body's centre of mass.
793
- *
794
- * Wakes the body if asleep.
795
- *
796
- * @param {RigidBody} rigidBody
797
- * @param {Transform} transform body's current world Transform
798
- * @param {Vector3|{x:number,y:number,z:number}} force
799
- * @param {Vector3|{x:number,y:number,z:number}} worldPoint
800
- */
801
- applyForceAt(rigidBody, transform, force, worldPoint) {
802
- if (rigidBody.kind !== BodyKind.Dynamic) {
803
- return;
804
- }
805
- rigidBody.accumulatedForce.set(
806
- rigidBody.accumulatedForce.x + force.x,
807
- rigidBody.accumulatedForce.y + force.y,
808
- rigidBody.accumulatedForce.z + force.z
809
- );
810
-
811
- const rx = worldPoint.x - transform.position.x;
812
- const ry = worldPoint.y - transform.position.y;
813
- const rz = worldPoint.z - transform.position.z;
814
-
815
- rigidBody.accumulatedTorque.set(
816
- rigidBody.accumulatedTorque.x + (ry * force.z - rz * force.y),
817
- rigidBody.accumulatedTorque.y + (rz * force.x - rx * force.z),
818
- rigidBody.accumulatedTorque.z + (rx * force.y - ry * force.x)
819
- );
820
-
821
- this.__wake_body(rigidBody);
822
- }
823
-
824
- /**
825
- * Accumulate a continuous force to be integrated next fixedUpdate step.
826
- * Wakes the body if asleep.
827
- *
828
- * @param {RigidBody} rigidBody
829
- * @param {Vector3|{x:number,y:number,z:number}} force
830
- */
831
- applyForce(rigidBody, force) {
832
- if (rigidBody.kind !== BodyKind.Dynamic) {
833
- return;
834
- }
835
- rigidBody.accumulatedForce.add(force);
836
- this.__wake_body(rigidBody);
837
- }
838
-
839
- /**
840
- * Replace the linear velocity. Wakes the body if asleep.
841
- *
842
- * @param {RigidBody} rigidBody
843
- * @param {Vector3|{x:number,y:number,z:number}} v
844
- */
845
- setLinearVelocity(rigidBody, v) {
846
- rigidBody.linearVelocity.copy(v);
847
- if (rigidBody.kind === BodyKind.Dynamic) {
848
- this.__wake_body(rigidBody);
849
- }
850
- }
851
-
852
- /**
853
- * Teleport a body to a new pose, bypassing integration: writes the body's
854
- * Transform directly and wakes it. For an interpolated body this also flags a
855
- * render `snap` on its {@link Interpolated} component, so the producer keeps
856
- * this pose (rather than restoring the previous tick over it) and the
857
- * renderer shows the new pose without sliding across the jump.
858
- *
859
- * This is the authoritative way to reposition an interpolated body — a raw
860
- * `Transform` write would be undone by the per-step restore. Velocity is left
861
- * as-is; zero it via {@link setLinearVelocity} if the teleport should also
862
- * stop the body.
863
- *
864
- * @param {RigidBody} rigidBody
865
- * @param {Vector3|{x:number,y:number,z:number}} position world position
866
- * @param {Quaternion|{x:number,y:number,z:number,w:number}} rotation world unit-quaternion rotation
867
- */
868
- setPose(rigidBody, position, rotation) {
869
- const idx = body_id_index(rigidBody._bodyId);
870
- const transform = this.__transforms[idx];
871
- if (transform === undefined) {
872
- return;
873
- }
874
-
875
- transform.position.set(position.x, position.y, position.z);
876
- transform.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w);
877
-
878
- this.__wake_body(rigidBody);
879
-
880
- // Flag a snap on the body's Interpolated component, if any, so the
881
- // interpolation producer (restore) and consumer (blend) treat this pose
882
- // as authoritative this frame instead of interpolating across the jump.
883
- const em = this.entityManager;
884
- if (em !== null && em !== undefined) {
885
- const dataset = em.dataset;
886
- if (dataset !== null && dataset !== undefined) {
887
- const entity = this.storage.entity_at(idx);
888
- const interpolated = dataset.getComponent(entity, Interpolated);
889
- if (interpolated !== undefined && interpolated !== null) {
890
- interpolated.snap = true;
891
- }
892
- }
893
- }
894
- }
895
-
896
- /**
897
- * Force the body awake. Static bodies are ignored.
898
- * @param {RigidBody} rigidBody
899
- */
900
- wake(rigidBody) {
901
- this.__wake_body(rigidBody);
902
- }
903
-
904
- /**
905
- * Force the body asleep. Dynamic bodies will not re-enter the active list
906
- * until a wake event occurs.
907
- * @param {RigidBody} rigidBody
908
- */
909
- sleep(rigidBody) {
910
- if (rigidBody.kind !== BodyKind.Dynamic) {
911
- return;
912
- }
913
- if (rigidBody.sleepState === SleepState.Sleeping) {
914
- return;
915
- }
916
- rigidBody.sleepState = SleepState.Sleeping;
917
- const index = body_id_index(rigidBody._bodyId);
918
- this.storage.mark_sleeping(index);
919
- }
920
-
921
- /**
922
- * Wake a body and atomically wake every other body it was last sleeping
923
- * with (its "sleep group"). Sleep groups are circular doubly-linked lists
924
- * threaded through every member of an island when it sleeps atomically;
925
- * waking any one member walks the chain and wakes the rest in the same
926
- * call.
927
- *
928
- * Without this, a 100-block stack hit at the base would wake one block
929
- * per frame as the broadphase propagated awareness up the stack — a
930
- * visible ~1.6 s wave at 60 fps. Atomic wake eliminates the wave.
931
- *
932
- * No-op for non-dynamic bodies. Idempotent for already-awake bodies.
933
- * @private
934
- * @param {RigidBody} rb
935
- */
936
- __wake_body(rb) {
937
- if (rb.kind !== BodyKind.Dynamic) return;
938
- if (rb.sleepState === SleepState.Awake) return;
939
- const index = body_id_index(rb._bodyId);
940
-
941
- // Remember the next-in-chain before clearing the body's own pointers;
942
- // the rest of the group is reached by walking forward from there.
943
- const start_next = rb.sleep_group_next;
944
-
945
- rb.sleepState = SleepState.Awake;
946
- rb.sleep_timer = 0;
947
- rb.sleep_group_next = -1;
948
- rb.sleep_group_prev = -1;
949
- this.storage.mark_awake(index);
950
-
951
- if (start_next === -1 || start_next === index) return;
952
-
953
- // Walk the (now-broken) chain forward until we loop back. The chain
954
- // is circular so we know when to stop; defensive `-1` guards against
955
- // corruption from a body being unlinked mid-sleep-group.
956
- let cur = start_next;
957
- while (cur !== -1 && cur !== index) {
958
- const cur_rb = this.__bodies[cur];
959
- if (cur_rb === undefined) break;
960
- const nxt = cur_rb.sleep_group_next;
961
- cur_rb.sleepState = SleepState.Awake;
962
- cur_rb.sleep_timer = 0;
963
- cur_rb.sleep_group_next = -1;
964
- cur_rb.sleep_group_prev = -1;
965
- this.storage.mark_awake(cur);
966
- cur = nxt;
967
- }
968
- }
969
-
970
- /**
971
- * Atomically put every body in a contiguous range of island members to
972
- * sleep. Members are threaded into a circular doubly-linked list so any
973
- * future `wake` on any member walks the chain and revives them all.
974
- *
975
- * Velocities are zeroed because the body is by definition at rest at
976
- * this point — the alternative (storing residual velocities for "softer"
977
- * wake) is what Bullet does, but for a deterministic game-physics target
978
- * fully resetting is simpler and avoids drift while sleeping.
979
- *
980
- * @private
981
- * @param {Uint32Array} member_array view (or full array) of body indices
982
- * @param {number} start
983
- * @param {number} end
984
- */
985
- __atomic_sleep_island_range(member_array, start, end) {
986
- const count = end - start;
987
- if (count === 0) return;
988
- const bodies = this.__bodies;
989
- const storage = this.storage;
990
-
991
- if (count === 1) {
992
- const idx = member_array[start];
993
- const rb = bodies[idx];
994
- if (rb === undefined) return;
995
- rb.sleep_group_next = -1;
996
- rb.sleep_group_prev = -1;
997
- rb.sleepState = SleepState.Sleeping;
998
- rb.linearVelocity[0] = 0;
999
- rb.linearVelocity[1] = 0;
1000
- rb.linearVelocity[2] = 0;
1001
- rb.angularVelocity[0] = 0;
1002
- rb.angularVelocity[1] = 0;
1003
- rb.angularVelocity[2] = 0;
1004
- storage.mark_sleeping(idx);
1005
- return;
1006
- }
1007
-
1008
- for (let i = 0; i < count; i++) {
1009
- const idx = member_array[start + i];
1010
- const rb = bodies[idx];
1011
- if (rb === undefined) continue;
1012
- const next_idx = member_array[start + ((i + 1) % count)];
1013
- const prev_idx = member_array[start + ((i - 1 + count) % count)];
1014
- rb.sleep_group_next = next_idx;
1015
- rb.sleep_group_prev = prev_idx;
1016
- rb.sleepState = SleepState.Sleeping;
1017
- rb.linearVelocity[0] = 0;
1018
- rb.linearVelocity[1] = 0;
1019
- rb.linearVelocity[2] = 0;
1020
- rb.angularVelocity[0] = 0;
1021
- rb.angularVelocity[1] = 0;
1022
- rb.angularVelocity[2] = 0;
1023
- storage.mark_sleeping(idx);
1024
- }
1025
- }
1026
-
1027
- /**
1028
- * Broadphase raycast against both BVHs. Fills `result` with the nearest
1029
- * hit and returns `true` on hit, `false` on miss.
1030
- *
1031
- * Narrowphase refinement against the actual shape geometry is a
1032
- * follow-up — for now `result.t` is the distance to the leaf's
1033
- * inflated AABB and `result.normal` is the AABB face normal. Both are
1034
- * exact for AABB-shaped colliders.
1035
- *
1036
- * @param {Ray3} ray origin + unit direction + `tMax`
1037
- * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
1038
- * @param {(entity:number, collider:Collider)=>boolean} [filter] defaults
1039
- * to {@link returnTrue} (accept every candidate)
1040
- * @returns {boolean}
1041
- */
1042
- raycast(ray, result, filter = returnTrue) {
1043
- return raycast_query(this, ray, result, filter);
1044
- }
1045
-
1046
- /**
1047
- * Sweep a convex shape along a ray and find the first body it
1048
- * would hit. The shape starts at `ray.origin` oriented by
1049
- * `rotation` and translates along `ray.direction` for up to
1050
- * `ray.tMax` units. Returns the nearest impact within that
1051
- * interval.
1052
- *
1053
- * The "swept AABB" broadphase finds candidate bodies whose BVH
1054
- * leaves overlap the shape's swept volume; the narrowphase then
1055
- * bisects [0, t] on GJK overlap to find the time-of-impact for
1056
- * each candidate. Best-t early termination skips candidates that
1057
- * can't tighten the answer.
1058
- *
1059
- * @param {Ray3} ray origin + unit direction + `tMax`
1060
- * @param {AbstractShape3D} shape
1061
- * @param {{x:number,y:number,z:number,w:number}} rotation
1062
- * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
1063
- * @param {(entity:number, collider:Collider)=>boolean} [filter]
1064
- * @returns {boolean}
1065
- */
1066
- shapeCast(ray, shape, rotation, result, filter = returnTrue) {
1067
- return shape_cast_query(this, ray, shape, rotation, result, filter);
1068
- }
1069
-
1070
- /**
1071
- * Speculative overlap query: find all bodies whose collider would
1072
- * overlap the given convex shape placed at the given world pose,
1073
- * without mutating the simulation. Intended for kinematic /
1074
- * character controllers that need to test "would I collide if I
1075
- * moved here?" before committing the move.
1076
- *
1077
- * Pipeline:
1078
- * 1. The shape's world AABB is computed from `position` + `rotation`.
1079
- * 2. Both broadphase trees (static + dynamic) are queried for
1080
- * bodies whose leaf AABB overlaps that envelope.
1081
- * 3. Each candidate is GJK-tested in world frame. Convex
1082
- * candidates run one GJK call; concave candidates (heightmap,
1083
- * mesh) run per-triangle GJK via the decomposition path.
1084
- * 4. The optional `filter` is consulted before the GJK test —
1085
- * useful for skipping the caller's own body, allies, sensors,
1086
- * etc.
1087
- *
1088
- * The output buffer is filled with overlapping bodies' `body_id`
1089
- * values (uint32 with packed generation), starting at
1090
- * `output_offset`. The caller is responsible for sizing the buffer;
1091
- * IDs past its end are dropped silently and the count caps at the
1092
- * available space.
1093
- *
1094
- * The query shape must be convex. Concave query shapes throw —
1095
- * they're typically static terrain and not used as kinematic
1096
- * probes; the M×N triangle-pair cost wouldn't be worth the rare
1097
- * use case.
1098
- *
1099
- * @param {AbstractShape3D} shape convex query shape, in its local frame
1100
- * @param {{x:number,y:number,z:number}} position world position of
1101
- * the query shape
1102
- * @param {{x:number,y:number,z:number,w:number}} rotation world
1103
- * rotation (unit quaternion)
1104
- * @param {Uint32Array|number[]} output buffer to receive body_ids
1105
- * @param {number} output_offset starting index in output
1106
- * @param {(entity:number, collider:Collider)=>boolean} [filter]
1107
- * defaults to {@link returnTrue}
1108
- * @returns {number} number of overlapping bodies written
1109
- */
1110
- overlap(shape, position, rotation, output, output_offset, filter = returnTrue) {
1111
- return overlap_shape_query(this, shape, position, rotation, output, output_offset, filter);
1112
- }
1113
-
1114
- /**
1115
- * Wake any sleeping body that appears in this step's broadphase pair list.
1116
- * A pair means the BVH AABBs overlap — even if the sleeper hasn't moved,
1117
- * an awake neighbour has come into contact range and the sleeper must
1118
- * participate in narrowphase / solve.
1119
- * @private
1120
- */
1121
- __wake_pairs() {
1122
- const list = this.pairs;
1123
- const n = list.count;
1124
- const bodies = this.__bodies;
1125
- for (let i = 0; i < n; i++) {
1126
- const idA = list.get_a(i);
1127
- const idB = list.get_b(i);
1128
- const idxA = body_id_index(idA);
1129
- const idxB = body_id_index(idB);
1130
- const a = bodies[idxA];
1131
- const b = bodies[idxB];
1132
- if (a !== undefined && a.sleepState === SleepState.Sleeping) {
1133
- this.__wake_body(a);
1134
- }
1135
- if (b !== undefined && b.sleepState === SleepState.Sleeping) {
1136
- this.__wake_body(b);
1137
- }
1138
- }
1139
- }
1140
-
1141
- /**
1142
- * Wake propagation across joints: if a joint connects an awake body to a
1143
- * sleeping one, wake the sleeper so the constraint stays coupled (a joint
1144
- * with one body asleep and one awake would otherwise be skipped by the
1145
- * solver, letting the awake side drift). A common trigger is a
1146
- * kinematic/motor-driven body pulling on a sleeping chain.
1147
- *
1148
- * Bodies that slept together as one island share a sleep group, so the
1149
- * usual atomic wake already revives the whole chain when any member is
1150
- * hit; this catches the cases that atomic wake doesn't (joint spanning
1151
- * separate groups, kinematic driver).
1152
- * @private
1153
- */
1154
- __wake_joints() {
1155
- const joints = this.__joints;
1156
- const n = joints.length;
1157
- if (n === 0) return;
1158
- const bodies = this.__bodies;
1159
- const storage = this.storage;
1160
- for (let i = 0; i < n; i++) {
1161
- const joint = joints[i];
1162
- if (joint === undefined || joint === null) continue;
1163
- if (joint._bodyIdB === JOINT_WORLD) continue;
1164
- if (!storage.is_valid(joint._bodyIdA) || !storage.is_valid(joint._bodyIdB)) continue;
1165
- const a = bodies[body_id_index(joint._bodyIdA)];
1166
- const b = bodies[body_id_index(joint._bodyIdB)];
1167
- if (a === undefined || b === undefined) continue;
1168
- const aSleep = a.sleepState === SleepState.Sleeping;
1169
- const bSleep = b.sleepState === SleepState.Sleeping;
1170
- if (aSleep === bSleep) continue; // both awake or both asleep — leave as is
1171
- if (aSleep) this.__wake_body(a); else this.__wake_body(b);
1172
- }
1173
- }
1174
-
1175
- /**
1176
- * Per-island atomic sleep test. Walks each island once and applies the
1177
- * decision uniformly across all members:
1178
- *
1179
- * - If any member carries {@link RigidBodyFlags.DisableSleep}, the
1180
- * entire island is exempt; every member's sleep_timer is reset.
1181
- * - If `max(|v|² + |ω|²)` across all members is below
1182
- * {@link sleepVelocitySqrThreshold}, every member's sleep_timer is
1183
- * incremented by `dt`. When the smallest member's timer crosses
1184
- * {@link sleepTimeThreshold}, the whole island sleeps atomically in
1185
- * the same step (members get threaded into a sleep-group chain so
1186
- * {@link __wake_body} can wake them all in one call).
1187
- * - Otherwise the island has at least one active member, so every
1188
- * member's timer is reset.
1189
- *
1190
- * This is the design-plan atomic-island sleep — replaces the per-body
1191
- * approximation that lived in this slot during the previous slice.
1192
- * Weakly-connected piles no longer chatter awake when a single member
1193
- * blips above threshold; piles fall asleep and wake up as one.
1194
- *
1195
- * @private
1196
- * @param {number} dt
1197
- */
1198
- __sleep_test(dt) {
1199
- const threshold_sqr = this.sleepVelocitySqrThreshold;
1200
- const time_threshold = this.sleepTimeThreshold;
1201
- const bodies = this.__bodies;
1202
- const islands = this.islands;
1203
- const island_count = islands.island_count;
1204
- const body_offsets = islands.body_offsets;
1205
- const body_data = islands.body_data;
1206
-
1207
- for (let isl = 0; isl < island_count; isl++) {
1208
- const start = body_offsets[isl];
1209
- const end = body_offsets[isl + 1];
1210
- if (end === start) continue;
1211
-
1212
- // Pass 1: find max v² + check DisableSleep across the island.
1213
- let max_v_sqr = 0;
1214
- let any_disable_sleep = false;
1215
- for (let i = start; i < end; i++) {
1216
- const idx = body_data[i];
1217
- const rb = bodies[idx];
1218
- if (rb === undefined) continue;
1219
- if ((rb.flags & RigidBodyFlags.DisableSleep) !== 0) {
1220
- any_disable_sleep = true;
1221
- break;
1222
- }
1223
- const lv = rb.linearVelocity;
1224
- const av = rb.angularVelocity;
1225
- const v_sqr = lv[0] * lv[0] + lv[1] * lv[1] + lv[2] * lv[2]
1226
- + av[0] * av[0] + av[1] * av[1] + av[2] * av[2];
1227
- if (v_sqr > max_v_sqr) max_v_sqr = v_sqr;
1228
- }
1229
-
1230
- if (any_disable_sleep) {
1231
- // Whole island is exempt — reset every member's timer.
1232
- for (let i = start; i < end; i++) {
1233
- const rb = bodies[body_data[i]];
1234
- if (rb !== undefined) rb.sleep_timer = 0;
1235
- }
1236
- continue;
1237
- }
1238
-
1239
- if (max_v_sqr < threshold_sqr) {
1240
- // Island is at rest — increment every member's timer; if the
1241
- // slowest-stabilising member has crossed the time threshold,
1242
- // every member has (they were incremented together this step),
1243
- // so atomic-sleep the island.
1244
- let min_timer = Infinity;
1245
- for (let i = start; i < end; i++) {
1246
- const rb = bodies[body_data[i]];
1247
- if (rb === undefined) continue;
1248
- rb.sleep_timer += dt;
1249
- if (rb.sleep_timer < min_timer) min_timer = rb.sleep_timer;
1250
- }
1251
- if (min_timer >= time_threshold) {
1252
- this.__atomic_sleep_island_range(body_data, start, end);
1253
- }
1254
- } else {
1255
- // At least one member is active — reset every timer.
1256
- for (let i = start; i < end; i++) {
1257
- const rb = bodies[body_data[i]];
1258
- if (rb !== undefined) rb.sleep_timer = 0;
1259
- }
1260
- }
1261
- }
1262
- }
1263
-
1264
- /**
1265
- * Dispatch every buffered contact event to the entities involved, via the
1266
- * dataset's per-entity event channel — `dataset.sendEvent(entity, name,
1267
- * payload)`, once for each of the pair's entities. This is the only
1268
- * contact-event path: a system with no dataset attached has nowhere to
1269
- * deliver and emits nothing.
1270
- *
1271
- * Payload is a reused scratch object; listeners must copy anything they
1272
- * intend to retain past the listener body.
1273
- * @private
1274
- */
1275
- __dispatch_contact_events() {
1276
- const events = this.contactEvents;
1277
- const n = events.count;
1278
- if (n === 0) return;
1279
-
1280
- const ecd = (this.entityManager !== null && this.entityManager !== undefined)
1281
- ? this.entityManager.dataset
1282
- : null;
1283
- // No dataset → nowhere to deliver. The buffer is cleared by
1284
- // diff_manifolds each step regardless, so nothing leaks.
1285
- if (ecd === null || ecd === undefined) return;
1286
-
1287
- const manifolds = this.manifolds;
1288
- const data = manifolds.data_buffer;
1289
-
1290
- const payload = this.__contact_payload;
1291
-
1292
- for (let i = 0; i < n; i++) {
1293
- const kind = events.kind_at(i);
1294
- const entA = events.entityA_at(i);
1295
- const entB = events.entityB_at(i);
1296
- const slot = events.slot_at(i);
1297
-
1298
- // Use the deepest contact of the manifold as the representative
1299
- // point/normal/depth for the event. v1: contact 0 only.
1300
- const slot_off = manifolds.slot_data_offset(slot);
1301
- const has_contact = manifolds.contact_count(slot) > 0;
1302
-
1303
- // Scratch payload — write the point/normal components by index to
1304
- // skip the Vector3 change-signal dispatch (nothing observes them).
1305
- const pt = payload.point;
1306
- const nm = payload.normal;
1307
- if (has_contact) {
1308
- const wax = data[slot_off], way = data[slot_off + 1], waz = data[slot_off + 2];
1309
- const wbx = data[slot_off + 3], wby = data[slot_off + 4], wbz = data[slot_off + 5];
1310
- pt[0] = (wax + wbx) * 0.5;
1311
- pt[1] = (way + wby) * 0.5;
1312
- pt[2] = (waz + wbz) * 0.5;
1313
- nm[0] = data[slot_off + 6];
1314
- nm[1] = data[slot_off + 7];
1315
- nm[2] = data[slot_off + 8];
1316
- payload.depth = data[slot_off + 9];
1317
- } else {
1318
- pt[0] = 0;
1319
- pt[1] = 0;
1320
- pt[2] = 0;
1321
- nm[0] = 0;
1322
- nm[1] = 0;
1323
- nm[2] = 0;
1324
- payload.depth = 0;
1325
- }
1326
- payload.entityA = entA;
1327
- payload.entityB = entB;
1328
-
1329
- let event_name;
1330
- if (kind === ContactEventKind.Begin) {
1331
- event_name = PhysicsEvents.ContactBegin;
1332
- } else if (kind === ContactEventKind.Stay) {
1333
- event_name = PhysicsEvents.ContactStay;
1334
- } else {
1335
- event_name = PhysicsEvents.ContactEnd;
1336
- }
1337
-
1338
- if (entA >= 0) ecd.sendEvent(entA, event_name, payload);
1339
- if (entB >= 0) ecd.sendEvent(entB, event_name, payload);
1340
- }
1341
- }
1342
-
1343
- /**
1344
- * Producer — restore pass. At the top of a fixed step, reset every awake
1345
- * {@link Interpolated} body's live components to their authoritative state
1346
- * from the previous tick's snapshot, undoing any render-time interpolation
1347
- * the {@link InterpolationSystem} wrote between frames so the sim integrates
1348
- * from truth, not an interpolated pose. A body with no previous snapshot
1349
- * (first step ever, or just woken) is left as-is — its live state is already
1350
- * authoritative. No-op unless {@link interpolationLog} is wired.
1351
- * @private
1352
- */
1353
- __interp_restore() {
1354
- const log = this.interpolationLog;
1355
- const em = this.entityManager;
1356
- if (log === null || em === null || em === undefined) return;
1357
- const dataset = em.dataset;
1358
- if (dataset === null || dataset === undefined) return;
1359
-
1360
- const prev_tick = em.fixedStepTick - 1;
1361
- if (prev_tick < 0) return;
1362
-
1363
- const storage = this.storage;
1364
- const count = storage.awake_count;
1365
- const scratch = this.__interp_scratch;
1366
-
1367
- for (let i = 0; i < count; i++) {
1368
- const idx = storage.awake_at(i);
1369
- const entity = storage.entity_at(idx);
1370
- const interpolated = dataset.getComponent(entity, Interpolated);
1371
- if (interpolated === undefined || interpolated === null) continue;
1372
- // A teleported body (snap set, e.g. via setPose) keeps its live pose
1373
- // this step — restoring the previous tick would undo the teleport.
1374
- if (interpolated.snap) continue;
1375
- const key = interpolated.key;
1376
- if (key < 0) continue;
1377
-
1378
- const interpolands = interpolated.interpolands;
1379
- for (let k = 0; k < interpolands.length; k++) {
1380
- const ip = interpolands[k];
1381
- scratch.position = 0;
1382
- // Snap to the previous tick's snapshot (both offsets equal → t irrelevant).
1383
- const ok = log.interpolate(scratch, key, ip.type_id, prev_tick, prev_tick, 0, ip.interpolation_adapter);
1384
- if (!ok) continue;
1385
- const target = dataset.getComponent(entity, ip.component_class);
1386
- if (target === undefined || target === null) continue;
1387
- scratch.position = 0;
1388
- ip.serialization_adapter.deserialize(scratch, target);
1389
- }
1390
- }
1391
- }
1392
-
1393
- /**
1394
- * Producer — record pass. At the end of a fixed step, snapshot every awake
1395
- * {@link Interpolated} body's live components into the shared log under the
1396
- * current `entityManager.fixedStepTick`. The render-time
1397
- * {@link InterpolationSystem} blends consecutive ticks from these snapshots.
1398
- * Only awake (moving) bodies are recorded, so the log stays sparse. No-op
1399
- * unless {@link interpolationLog} is wired.
1400
- * @private
1401
- */
1402
- __interp_record() {
1403
- const log = this.interpolationLog;
1404
- const em = this.entityManager;
1405
- if (log === null || em === null || em === undefined) return;
1406
- const dataset = em.dataset;
1407
- if (dataset === null || dataset === undefined) return;
1408
-
1409
- const tick = em.fixedStepTick;
1410
- const storage = this.storage;
1411
- const count = storage.awake_count;
1412
-
1413
- log.begin_tick(tick);
1414
- for (let i = 0; i < count; i++) {
1415
- const idx = storage.awake_at(i);
1416
- const entity = storage.entity_at(idx);
1417
- const interpolated = dataset.getComponent(entity, Interpolated);
1418
- if (interpolated === undefined || interpolated === null) continue;
1419
- const key = interpolated.key;
1420
- if (key < 0) continue;
1421
-
1422
- const interpolands = interpolated.interpolands;
1423
- for (let k = 0; k < interpolands.length; k++) {
1424
- const ip = interpolands[k];
1425
- const target = dataset.getComponent(entity, ip.component_class);
1426
- if (target === undefined || target === null) continue;
1427
- const buf = log.begin_record(key, ip.type_id);
1428
- ip.serialization_adapter.serialize(buf, target);
1429
- log.end_record();
1430
- }
1431
- }
1432
- log.end_tick();
1433
- }
1434
-
1435
- fixedUpdate(dt) {
1436
- // Producer: restore authoritative pose (undo render interpolation)
1437
- // before the sim reads any Transform this step.
1438
- this.__interp_restore();
1439
-
1440
- const gx = this.gravity.x;
1441
- const gy = this.gravity.y;
1442
- const gz = this.gravity.z;
1443
-
1444
- const storage = this.storage;
1445
- const bodies = this.__bodies;
1446
- const transforms = this.__transforms;
1447
- const joints = this.__joints;
1448
- const manifolds = this.manifolds;
1449
-
1450
-
1451
- const count = storage.awake_count;
1452
-
1453
- // Stage 1: consume the per-frame force / torque accumulators into
1454
- // velocity at the full `dt`, exactly once (a user force is a per-frame
1455
- // budget that must land in full regardless of substep count). Gravity
1456
- // is applied per substep below, so the trajectory integrates at the
1457
- // substep rate and — crucially — each substep's gravity is balanced by
1458
- // that substep's contact warm-start, keeping resting stacks at zero
1459
- // velocity.
1460
- for (let i = 0; i < count; i++) {
1461
- const idx = storage.awake_at(i);
1462
-
1463
- const rb = bodies[idx];
1464
- const tr = transforms[idx];
1465
-
1466
- integrate_velocity_forces(rb, tr, dt);
1467
- }
1468
-
1469
- // Stage 2: refit each awake body's collider leaves at the current
1470
- // pose, padded by the swept extent for the body's velocity. The fat
1471
- // margin uses the post-force velocity; this frame's gravity increment
1472
- // is a sub-millimetre slack difference, safely inside the margin.
1473
- const lists = this.__body_collider_lists;
1474
- for (let i = 0; i < count; i++) {
1475
-
1476
- const idx = storage.awake_at(i);
1477
- const rb = bodies[idx];
1478
- const list = lists[idx];
1479
-
1480
- if (list === undefined) {
1481
- continue;
1482
- }
1483
-
1484
- const lv = rb.linearVelocity;
1485
-
1486
- const list_length = list.length;
1487
-
1488
- for (let k = 0; k < list_length; k++) {
1489
-
1490
- const entry = list[k];
1491
-
1492
- compute_fat_world_aabb(
1493
- scratch_world_aabb, 0,
1494
- entry.collider.shape, entry.transform,
1495
- lv[0], lv[1], lv[2],
1496
- dt
1497
- );
1498
-
1499
- this.dynamicBvh.node_move_aabb(entry.bvhNode, scratch_world_aabb);
1500
- }
1501
- }
1502
-
1503
- // Stage 3: broadphase pair generation. The fat AABBs cover the full
1504
- // outer-step motion, so the pair set stays valid across all substeps
1505
- // — broadphase runs once.
1506
- generate_pairs(
1507
- storage,
1508
- this.dynamicBvh,
1509
- this.staticBvh,
1510
- manifolds,
1511
- lists,
1512
- this.pairs,
1513
- this.__pair_filter,
1514
- this,
1515
- );
1516
-
1517
- // Stage 4: wake propagation — through broadphase pairs, then through
1518
- // joints (a joint must not have one body awake and one asleep).
1519
- this.__wake_pairs();
1520
- this.__wake_joints();
1521
-
1522
- // Stage 5: narrowphase — once per outer step. The substep loop below
1523
- // re-derives each contact's penetration analytically from the moved
1524
- // poses rather than re-running geometry.
1525
- narrowphase_step(this.pairs, manifolds, this.__body_collider_lists);
1526
-
1527
- // Stage 6: partition awake bodies + touched contacts into islands.
1528
- // Consumed by the solver (flattened contact list) and the sleep test.
1529
- this.islands.build(storage, manifolds, bodies, this.__body_collider_lists, joints);
1530
-
1531
- // Stage 7: TGS substep loop.
1532
- //
1533
- // prepare_contacts captures per-contact anchors / effective masses /
1534
- // approach velocity (no warm-start — that's per-substep). Each substep
1535
- // integrates gravity by `h`, re-derives contact geometry from the
1536
- // current poses, replays warm-start, solves velocity (non-penetration
1537
- // + friction) and position (pseudo-velocity), and integrates the pose
1538
- // by `h`. Per-substep gravity + per-substep warm-start balance exactly
1539
- // at a resting contact (each cancels `h` of the other), so stacks hold
1540
- // at zero velocity and sleep; the position correction adapts as bodies
1541
- // separate between substeps — the TGS stack-stability mechanism —
1542
- // without re-running narrowphase. Restitution is applied once after
1543
- // the loop.
1544
- const N = this.substeps;
1545
- const h = dt / N;
1546
- const count_after_wake = storage.awake_count;
1547
-
1548
- // CCD: capture start-of-step positions for flagged bodies over the
1549
- // post-wake awake set (poses are unchanged until the substep loop below
1550
- // integrates them). The CCD pass after the solver sweeps from here to
1551
- // each body's final pose. Reads the primary collider's transform so the
1552
- // start matches the end the resolve pass reads. Zero-cost when no body
1553
- // is flagged.
1554
- const ccd_on = this.ccdEnabled;
1555
-
1556
- if (ccd_on) {
1557
-
1558
- const ccd_need = storage.high_water_mark * 3;
1559
-
1560
- if (this.__ccd_start_pos.length < ccd_need) {
1561
- this.__ccd_start_pos = new Float64Array(ccd_need);
1562
- }
1563
-
1564
- const ccd_start = this.__ccd_start_pos;
1565
-
1566
- for (let i = 0; i < count_after_wake; i++) {
1567
- const idx = storage.awake_at(i);
1568
- const rb = bodies[idx];
1569
-
1570
- if (rb.kind !== BodyKind.Dynamic) {
1571
- continue;
1572
- }
1573
- if ((rb.flags & RigidBodyFlags.CCD) === 0) {
1574
- continue;
1575
- }
1576
-
1577
- const list = this.__body_collider_lists[idx];
1578
-
1579
- if (list === undefined || list.length === 0) {
1580
- continue;
1581
- }
1582
-
1583
- const cp = list[0].transform.position;
1584
- const cb = idx * 3;
1585
-
1586
- ccd_start[cb] = cp[0];
1587
- ccd_start[cb + 1] = cp[1];
1588
- ccd_start[cb + 2] = cp[2];
1589
- }
1590
- }
1591
-
1592
- // Size the pseudo-velocity buffer ONCE (it may reallocate on growth),
1593
- // then capture the reference. Inside the loop we only zero its live
1594
- // region per substep — re-capturing is unnecessary since it won't
1595
- // reallocate again this step.
1596
- this.__reset_pseudo_velocity();
1597
- const pseudoVel = this.__pseudo_velocity;
1598
- const pseudo_len = storage.high_water_mark * 6;
1599
-
1600
- // Gather the data-oriented solver state for every body the substep loop
1601
- // will touch: the post-wake awake set (all dynamics, at their post-force
1602
- // velocity) plus the static / kinematic anchors and jointed partners
1603
- // referenced by this step's contacts and joints. From here the substep
1604
- // loop reads / writes velocity + orientation through `ss_data` with no
1605
- // component-object dereference; persistent velocity is scattered back
1606
- // onto the RigidBodies after the solve.
1607
- const solver_state = this.__solver_state;
1608
- solver_state.begin(storage.high_water_mark);
1609
-
1610
- for (let i = 0; i < count_after_wake; i++) {
1611
- const idx = storage.awake_at(i);
1612
- solver_state.gather(idx, bodies[idx], transforms[idx]);
1613
- }
1614
-
1615
- const island_contacts = this.islands.contact_data;
1616
- const island_contact_total = this.islands.contact_offsets[this.islands.island_count];
1617
-
1618
- for (let i = 0; i < island_contact_total; i++) {
1619
- const slot = island_contacts[i];
1620
-
1621
- const ia = body_id_index(manifolds.bodyA(slot));
1622
- const ib = body_id_index(manifolds.bodyB(slot));
1623
-
1624
- solver_state.gather(ia, bodies[ia], transforms[ia]);
1625
- solver_state.gather(ib, bodies[ib], transforms[ib]);
1626
- }
1627
-
1628
- const joint_count = joints.length;
1629
- for (let i = 0; i < joint_count; i++) {
1630
- const joint = joints[i];
1631
-
1632
- if (joint === undefined || joint === null) {
1633
- continue;
1634
- }
1635
-
1636
- if (!storage.is_valid(joint._bodyIdA)) {
1637
- continue;
1638
- }
1639
-
1640
- const ia = body_id_index(joint._bodyIdA);
1641
-
1642
- solver_state.gather(ia, bodies[ia], transforms[ia]);
1643
-
1644
- if (joint._bodyIdB !== JOINT_WORLD && storage.is_valid(joint._bodyIdB)) {
1645
-
1646
- const ib = body_id_index(joint._bodyIdB);
1647
-
1648
- solver_state.gather(ib, bodies[ib], transforms[ib]);
1649
-
1650
- }
1651
- }
1652
-
1653
- const ss_data = solver_state.data;
1654
-
1655
- prepare_contacts(manifolds, this, h);
1656
-
1657
- for (let s = 0; s < N; s++) {
1658
-
1659
- // Gravity (+ damping) for this substep.
1660
- for (let i = 0; i < count_after_wake; i++) {
1661
- const idx = storage.awake_at(i);
1662
- integrate_velocity_gravity(ss_data, idx * SBS_STRIDE, bodies[idx], gx, gy, gz, h);
1663
- }
1664
-
1665
- // Re-derive contact geometry at the current poses: concave pairs
1666
- // re-run narrowphase (fresh feature/normal as the body rocks),
1667
- // convex pairs rotate frozen anchors analytically. Then replay
1668
- // the per-substep warm-start and solve velocity.
1669
- redetect_concave_contacts(manifolds, this);
1670
- refresh_contacts(manifolds, this.__transforms);
1671
- warm_start_contacts(manifolds, this);
1672
- solve_velocity(manifolds, this, this.velocityIterations);
1673
-
1674
- // Joints share the substep: warm-start + velocity-solve the 6-DOF
1675
- // constraints on real velocity, coupled with the contacts above
1676
- // (a body touched by both sees one substep of Gauss-Seidel across
1677
- // contacts then joints). Position correction for locked DOFs is a
1678
- // SPOOK bias inside this solve, so no separate joint position pass.
1679
- if (joints.length > 0) {
1680
- solve_joints(joints, this, h, this.jointIterations);
1681
- }
1682
-
1683
- // Position correction writes pseudo-velocity (zeroed first so it
1684
- // is a fresh per-substep correction), folded into the pose by the
1685
- // position integrate and then discarded.
1686
- pseudoVel.fill(0, 0, pseudo_len);
1687
- solve_position(manifolds, this, this.positionIterations);
1688
-
1689
- for (let i = 0; i < count_after_wake; i++) {
1690
- const idx = storage.awake_at(i);
1691
- const rb = bodies[idx];
1692
- const tr = transforms[idx];
1693
- const base = idx * 6;
1694
- integrate_position(ss_data, idx * SBS_STRIDE, rb, tr, h,
1695
- pseudoVel[base], pseudoVel[base + 1], pseudoVel[base + 2],
1696
- pseudoVel[base + 3], pseudoVel[base + 4], pseudoVel[base + 5]);
1697
- }
1698
- }
1699
-
1700
- // Stage 8: one-shot restitution, after the substep loop, keyed off
1701
- // the approach velocity captured at prepare time.
1702
- apply_restitution(manifolds, this);
1703
-
1704
- // Scatter the solved persistent linear / angular velocity back onto the
1705
- // RigidBody components (pose was written through to the Transforms each
1706
- // substep). After this the bodies are authoritative again for CCD,
1707
- // interpolation recording, and the sleep test below.
1708
- solver_state.scatter(bodies);
1709
-
1710
- // Stage 8.5: continuous collision sweep CCD-flagged fast movers along
1711
- // their net step translation and stop them at the first blocker, so they
1712
- // can't tunnel through thin geometry between discrete steps. Runs on the
1713
- // final post-solve poses, before the sleep test sees the clamped
1714
- // velocities. No-op when no awake body is flagged.
1715
- if (ccd_on) {
1716
- ccd_resolve(this);
1717
- }
1718
-
1719
- // Producer: record the post-step authoritative pose of every awake
1720
- // interpolated body under this step's tick. Before the sleep test, so a
1721
- // body that settles this step still records its final pose for the last
1722
- // interpolation interval.
1723
- this.__interp_record();
1724
-
1725
- // Stage 9: sleep test.
1726
- this.__sleep_test(dt);
1727
-
1728
- // Stage 10: diff manifolds against the previous frame and dispatch
1729
- // Begin / Stay / End events. MUST run before advance_frame, which
1730
- // rolls the touched flags.
1731
- diff_manifolds(manifolds, storage, this.contactEvents);
1732
- this.__dispatch_contact_events();
1733
-
1734
- // Stage 11 (end-of-step): roll touched → prev_touched and evict slots
1735
- // whose pair has not been touched within the grace window.
1736
- manifolds.advance_frame();
1737
- }
1738
- }
1739
-
1740
- /**
1741
- * @readonly
1742
- * @type {boolean}
1743
- */
1744
- PhysicsSystem.prototype.isPhysicsSystem = true;
1745
-
1746
- // Re-export for convenience.
1747
- 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 { returnTrue } from "../../../core/function/returnTrue.js";
5
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
6
+ import Vector3 from "../../../core/geom/Vector3.js";
7
+ import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
8
+ import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
9
+ import { System } from "../../ecs/System.js";
10
+ import { Transform } from "../../ecs/transform/Transform.js";
11
+ import { Interpolated } from "../../interpolation/Interpolated.js";
12
+ import { body_id_index, BodyStorage } from "../body/BodyStorage.js";
13
+ import { SBS_STRIDE, SolverBodyState } from "../body/SolverBodyState.js";
14
+ import { compute_fat_world_aabb } from "../broadphase/compute_fat_world_aabb.js";
15
+ import { generate_pairs } from "../broadphase/generate_pairs.js";
16
+ import { PairList } from "../broadphase/PairList.js";
17
+ import { ccd_resolve } from "../ccd/linear_sweep.js";
18
+ import { solve_joints } from "../constraint/solve_constraints.js";
19
+ import { ManifoldStore } from "../contact/ManifoldStore.js";
20
+ import { ContactEventBuffer, ContactEventKind } from "../events/ContactEventBuffer.js";
21
+ import { diff_manifolds } from "../events/diff_manifolds.js";
22
+ import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
23
+ import { integrate_position } from "../integration/integrate_position.js";
24
+ import { integrate_velocity_forces, integrate_velocity_gravity } from "../integration/integrate_velocity.js";
25
+ import { IslandBuilder } from "../island/IslandBuilder.js";
26
+ import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
27
+ import { overlap_shape as overlap_shape_query } from "../queries/overlap_shape.js";
28
+ import { raycast as raycast_query } from "../queries/raycast.js";
29
+ import { shape_cast as shape_cast_query } from "../queries/shape_cast.js";
30
+ import {
31
+ apply_restitution,
32
+ prepare_contacts,
33
+ redetect_concave_contacts,
34
+ refresh_contacts,
35
+ solve_position,
36
+ solve_velocity,
37
+ warm_start_contacts,
38
+ } from "../solver/solve_contacts.js";
39
+ import { BodyKind } from "./BodyKind.js";
40
+ import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
41
+ import { JOINT_UNALLOCATED, JOINT_WORLD } from "./Joint.js";
42
+ import { PhysicsEvents } from "./PhysicsEvents.js";
43
+ import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
44
+ import { RigidBodyFlags } from "./RigidBodyFlags.js";
45
+ import { SleepState } from "./SleepState.js";
46
+
47
+ /**
48
+ * Scratch for {@link applyImpulseAt}'s angular delta calculation.
49
+ * @type {Float64Array}
50
+ */
51
+ const scratch_angular_delta = new Float64Array(3);
52
+
53
+ /**
54
+ * Reusable scratch buffer for world-AABB construction so the link path is
55
+ * allocation-free in steady state.
56
+ * @type {Float64Array}
57
+ */
58
+ const scratch_world_aabb = new Float64Array(6);
59
+
60
+ /**
61
+ * Reusable scratch buffer for the local AABB returned by
62
+ * {@link AbstractShape3D#compute_bounding_box}.
63
+ * @type {Float64Array}
64
+ */
65
+ const scratch_local_aabb = new Float64Array(6);
66
+
67
+ /**
68
+ * Rigid-body physics system.
69
+ *
70
+ * v1 scope: pool + active list + two BVHs (static / dynamic), Transform sync
71
+ * contract, and the user-facing API surface (gravity, force / impulse, layer
72
+ * filtering, wake / sleep, contact filter). The per-step simulation pipeline
73
+ * (broadphase pair generation, narrowphase, solver, islands, sleep test,
74
+ * contact-event emission) is built on top of this skeleton in subsequent
75
+ * iterations.
76
+ *
77
+ * Dependency tuple is `(RigidBody, Collider, Transform)` — every body has
78
+ * exactly one collider on the same entity. Compound bodies via child collider
79
+ * entities are an extension point handled by a follow-up sub-observer.
80
+ *
81
+ * @author Alex Goldring
82
+ * @copyright Company Named Limited (c) 2026
83
+ */
84
+ export class PhysicsSystem extends System {
85
+
86
+ constructor() {
87
+ super();
88
+
89
+ this.dependencies = [RigidBody, Transform];
90
+
91
+ this.components_used = [
92
+ ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Read | ResourceAccessKind.Write),
93
+ ResourceAccessSpecification.from(Collider, ResourceAccessKind.Read),
94
+ ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read | ResourceAccessKind.Write),
95
+ ];
96
+
97
+ /**
98
+ * @type {BodyStorage}
99
+ * @readonly
100
+ */
101
+ this.storage = new BodyStorage();
102
+
103
+ /**
104
+ * @type {BVH}
105
+ */
106
+ this.staticBvh = new BVH();
107
+
108
+ /**
109
+ * @type {BVH}
110
+ */
111
+ this.dynamicBvh = new BVH();
112
+
113
+ /**
114
+ * Persistent contact-manifold cache. One slot per active pair.
115
+ * @type {ManifoldStore}
116
+ * @readonly
117
+ */
118
+ this.manifolds = new ManifoldStore();
119
+
120
+ /**
121
+ * Per-frame list of broadphase-overlapping pairs (canonical
122
+ * `(min, max)`). Cleared at the top of each step.
123
+ * @type {PairList}
124
+ */
125
+ this.pairs = new PairList();
126
+
127
+ /**
128
+ * Per-frame contact-event buffer. Populated by the manifold diff
129
+ * pass at end-of-step and consumed by the dispatch pass.
130
+ * @type {ContactEventBuffer}
131
+ */
132
+ this.contactEvents = new ContactEventBuffer();
133
+
134
+ /**
135
+ * Per-frame island partitioning of the awake-body + contact graph.
136
+ * Rebuilt after narrowphase, consumed by the solver (and, in the
137
+ * follow-up slice, by the per-island atomic sleep test). Bodies
138
+ * static and kinematic act as constraint anchors and do not enlarge
139
+ * islands.
140
+ * @type {IslandBuilder}
141
+ */
142
+ this.islands = new IslandBuilder();
143
+
144
+ /**
145
+ * Data-oriented mirror of the per-body solver hot state (velocity,
146
+ * inverse mass / inertia, orientation), packed into one ArrayBuffer
147
+ * and indexed by body slot. Gathered from the `RigidBody` /
148
+ * `Transform` components once per step (after islands are built),
149
+ * mutated in place by the TGS substep loop, and the persistent
150
+ * velocity scattered back at the end. Keeps the solver's hottest inner
151
+ * loop free of component-object dereferences.
152
+ * @type {SolverBodyState}
153
+ */
154
+ this.__solver_state = new SolverBodyState();
155
+
156
+ /**
157
+ * Velocity-squared threshold below which a body is eligible to start
158
+ * accumulating sleep time. Combined linear + angular kinetic-ish
159
+ * metric: `vx²+vy²+vz² + ωx²+ωy²+ωz²`. Default 0.01 corresponds to
160
+ * ~0.1 m/s linear or ~0.1 rad/s angular.
161
+ * @type {number}
162
+ */
163
+ this.sleepVelocitySqrThreshold = 0.01;
164
+
165
+ /**
166
+ * Seconds of below-threshold motion before a body is moved to the
167
+ * sleeping set. Box2D default is 0.5 s.
168
+ * @type {number}
169
+ */
170
+ this.sleepTimeThreshold = 0.5;
171
+
172
+ /**
173
+ * Number of TGS substeps per `fixedUpdate`. Each substep re-runs the
174
+ * velocity + position solve at `dt / substeps` against contacts whose
175
+ * penetration is re-derived analytically from the bodies' moved poses
176
+ * — narrowphase still runs once per outer step. Higher counts buy
177
+ * stack stability, high-mass-ratio robustness, and smoother
178
+ * trajectories at a near-linear solver cost (sleeping islands are
179
+ * unaffected — they never enter the loop).
180
+ *
181
+ * `1` reproduces the non-substepped (single-step) solve.
182
+ * @type {number}
183
+ */
184
+ this.substeps = 4;
185
+
186
+ /**
187
+ * Velocity iterations per substep. Lower than a single-step solver
188
+ * would need, because the substep loop revisits the contact set
189
+ * `substeps` times.
190
+ * @type {number}
191
+ */
192
+ this.velocityIterations = 4;
193
+
194
+ /**
195
+ * Position (split-impulse) iterations per substep.
196
+ * @type {number}
197
+ */
198
+ this.positionIterations = 1;
199
+
200
+ /**
201
+ * Reusable contact-event payload. Listeners must copy any fields they
202
+ * intend to retain past their own scope. Reset before each dispatch.
203
+ * @private
204
+ */
205
+ this.__contact_payload = {
206
+ entityA: -1,
207
+ entityB: -1,
208
+ kind: 0,
209
+ depth: 0,
210
+ normal: new Vector3(),
211
+ point: new Vector3(),
212
+ };
213
+
214
+ /**
215
+ * World gravity, m/s². Applied each step scaled by per-body gravityScale.
216
+ * @readonly
217
+ * @type {Vector3}
218
+ */
219
+ this.gravity = new Vector3(0, -9.81, 0);
220
+
221
+ /**
222
+ * Optional global contact filter. Called for each surviving broadphase
223
+ * pair; returning `false` discards the pair.
224
+ * @type {((entityA: number, entityB: number, colliderA: Collider, colliderB: Collider) => boolean) | null}
225
+ * @private
226
+ */
227
+ this.__contact_filter = null;
228
+
229
+ /**
230
+ * Per-body component side-tables indexed by body index (NOT packed id).
231
+ * Sparse arrays — populated at link, cleared at unlink. The hot loop
232
+ * iterates `storage.awake_at(i)` and dereferences these.
233
+ * @type {RigidBody[]}
234
+ */
235
+ this.__bodies = [];
236
+
237
+ /**
238
+ * @type {Transform[]}
239
+ * @readonly
240
+ */
241
+ this.__transforms = [];
242
+
243
+ /**
244
+ * Per-body list of attached colliders. Each entry stores the
245
+ * Collider component, its world Transform, the entity that owns
246
+ * it (the body entity for same-entity attachments, a child entity
247
+ * for compound bodies), and the BVH leaf id assigned at attach
248
+ * time. A body may have zero or more attached colliders.
249
+ *
250
+ * @type {Array<Array<{collider: Collider, transform: Transform, entity: number, bvhNode: number}>>}
251
+ */
252
+ this.__body_collider_lists = [];
253
+
254
+ /**
255
+ * Live {@link Joint} (6-DOF constraint) instances, in a sparse array
256
+ * indexed by joint id. Solved alongside contacts inside the TGS
257
+ * substep loop. Holes (unlinked joints) are `undefined`.
258
+ * @type {Joint[]}
259
+ */
260
+ this.__joints = [];
261
+
262
+ /**
263
+ * Lowest free index in {@link __joints} for slot reuse on link.
264
+ * @private
265
+ * @type {number}
266
+ */
267
+ this.__joint_free = [];
268
+
269
+ /**
270
+ * Velocity iterations per substep for the joint solver. Joints (and
271
+ * especially joint chains) want a few more iterations than contacts to
272
+ * propagate impulses along the chain; cheap because joints are far
273
+ * fewer than contacts.
274
+ * @type {number}
275
+ */
276
+ this.jointIterations = 8;
277
+
278
+ /**
279
+ * Per-body pseudo-velocity for the Catto split-impulse position
280
+ * pass (TGS Phase 1). Flat layout, 6 doubles per body slot index:
281
+ * `[lin.x, lin.y, lin.z, ang.x, ang.y, ang.z]`. Sized to
282
+ * `storage.high_water_mark * 6` at the top of each fixedUpdate
283
+ * and zeroed in place so unwritten slots contribute nothing to
284
+ * `integrate_position`.
285
+ *
286
+ * The solver writes during its position pass; `integrate_position`
287
+ * reads and folds into the pose update on the same tick, then
288
+ * the next tick's zero-pass wipes the state. It NEVER lands in
289
+ * `linearVelocity` / `angularVelocity` — that's the point of
290
+ * split impulse: depth correction does not contaminate persistent
291
+ * velocity (so a `restitution = 0` impact stops cleanly).
292
+ *
293
+ * @type {Float64Array}
294
+ */
295
+ this.__pseudo_velocity = new Float64Array(0);
296
+
297
+ /**
298
+ * Master switch for the continuous-collision pass. When false the
299
+ * {@link RigidBodyFlags.CCD} flag is ignored and no swept queries run.
300
+ * @type {boolean}
301
+ */
302
+ this.ccdEnabled = true;
303
+
304
+ /**
305
+ * Start-of-step world positions for CCD-flagged bodies — 3 doubles per
306
+ * body slot index (`[x, y, z]`). Captured in Stage 1 before the substep
307
+ * loop integrates poses; the CCD pass ({@link ccd_resolve}) sweeps from
308
+ * here to the final pose. Grows to `storage.high_water_mark * 3`; only
309
+ * CCD-flagged slots are written each step.
310
+ * @type {Float64Array}
311
+ */
312
+ this.__ccd_start_pos = new Float64Array(0);
313
+
314
+
315
+ /**
316
+ * Optional shared interpolation log to produce per-step pose snapshots
317
+ * into (the {@link InterpolationSystem}'s log). When set — and only then —
318
+ * each fixedUpdate restores every awake {@link Interpolated} body's live
319
+ * components from the previous tick (undoing render-time interpolation so
320
+ * the sim reads authoritative state), then records the post-step state
321
+ * under the current `entityManager.fixedStepTick`. Null on a headless /
322
+ * non-rendering world, where the producer work is skipped entirely.
323
+ * @type {InterpolationLog|null}
324
+ */
325
+ this.interpolationLog = null;
326
+
327
+ /**
328
+ * Reusable decode buffer for restoring interpolated snapshots.
329
+ * @private
330
+ * @type {BinaryBuffer}
331
+ */
332
+ this.__interp_scratch = new BinaryBuffer();
333
+ this.__interp_scratch.fromArrayBuffer(new ArrayBuffer(256));
334
+ }
335
+
336
+ /**
337
+ * Symmetric layer/mask check + optional user callback. Called per
338
+ * candidate pair during broadphase. Returns `true` to accept the pair.
339
+ *
340
+ * Layer/mask rule: pair (A, B) collides iff
341
+ * `(A.layer & B.mask) !== 0 && (B.layer & A.mask) !== 0`.
342
+ * The user's contact-filter callback is consulted only for pairs that
343
+ * pass the bitmask test (cheap gate first).
344
+ *
345
+ * @private
346
+ * @param {number} idA packed body id
347
+ * @param {number} idB packed body id
348
+ * @returns {boolean}
349
+ */
350
+ __pair_filter(idA, idB) {
351
+ const idxA = body_id_index(idA);
352
+ const idxB = body_id_index(idB);
353
+
354
+ const rbA = this.__bodies[idxA];
355
+ const rbB = this.__bodies[idxB];
356
+
357
+ if (rbA === undefined || rbB === undefined) {
358
+ return false;
359
+ }
360
+
361
+ // Layer/mask gate (symmetric).
362
+ if (((rbA.layer & rbB.mask) | 0) === 0) {
363
+ return false;
364
+ }
365
+ if (((rbB.layer & rbA.mask) | 0) === 0) {
366
+ return false;
367
+ }
368
+
369
+ // User callback gate, if installed.
370
+ const fn = this.__contact_filter;
371
+ if (fn !== null) {
372
+ const entA = this.storage.entity_at(idxA);
373
+ const entB = this.storage.entity_at(idxB);
374
+ const colA = this.__primary_collider(idxA);
375
+ const colB = this.__primary_collider(idxB);
376
+ if (!fn(entA, entB, colA, colB)) return false;
377
+ }
378
+ return true;
379
+ }
380
+
381
+ /**
382
+ * First attached collider of a body, or `null` if none. Used for
383
+ * body-level checks that aren't yet per-collider: the sensor flag, the
384
+ * concave-shape dispatch flag, and the contact-filter callback's
385
+ * `colliderA` / `colliderB` arguments. Contact **materials** (friction /
386
+ * restitution) are NOT read here — the narrowphase tracks the specific
387
+ * source collider on each side of every contact and stamps the combined
388
+ * coefficients into the manifold per contact, so mixed-material compound
389
+ * bodies are accurate. (Per-contact source colliders for the filter
390
+ * callback / sensor flag remain a follow-up.)
391
+ *
392
+ * @private
393
+ * @param {number} body_idx
394
+ * @returns {Collider|null}
395
+ */
396
+ __primary_collider(body_idx) {
397
+ const list = this.__body_collider_lists[body_idx];
398
+ return (list !== undefined && list.length > 0) ? list[0].collider : null;
399
+ }
400
+
401
+ /**
402
+ * Resize {@link __pseudo_velocity} to cover every live body slot and
403
+ * zero its contents. Called at the top of each fixedUpdate so the
404
+ * split-impulse position pass starts from a clean state — bodies the
405
+ * solver doesn't touch contribute zero pseudo-velocity to
406
+ * {@link integrate_position}.
407
+ *
408
+ * Doubles on growth like the other physics scratches; never shrinks.
409
+ *
410
+ * @private
411
+ */
412
+ __reset_pseudo_velocity() {
413
+ const required = this.storage.high_water_mark * 6;
414
+ if (this.__pseudo_velocity.length < required) {
415
+ this.__pseudo_velocity = new Float64Array(required * 2);
416
+ } else if (required > 0) {
417
+ this.__pseudo_velocity.fill(0, 0, required);
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Replace the world gravity vector. Effective on the next step.
423
+ * @param {Vector3|{x:number,y:number,z:number}} v
424
+ */
425
+ setGravity(v) {
426
+ this.gravity.set(v.x, v.y, v.z);
427
+ }
428
+
429
+ /**
430
+ * Install (or remove with `null`) the contact filter callback.
431
+ * @param {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null} fn
432
+ */
433
+ setContactFilter(fn) {
434
+ this.__contact_filter = fn;
435
+ }
436
+
437
+ /**
438
+ * @returns {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null}
439
+ */
440
+ getContactFilter() {
441
+ return this.__contact_filter;
442
+ }
443
+
444
+ /**
445
+ * @private
446
+ * @param {RigidBody} rb
447
+ * @param {Collider} collider
448
+ * @param {Transform} transform
449
+ * @returns {number} BVH node id
450
+ */
451
+ __insert_into_broadphase(rb, collider, transform) {
452
+ const shape = collider.shape;
453
+
454
+ assert.notNull(shape, 'Collider.shape must be set before attaching');
455
+
456
+ shape.compute_bounding_box(scratch_local_aabb);
457
+
458
+ const p = transform.position;
459
+ const q = transform.rotation;
460
+
461
+ aabb3_transform_oriented(
462
+ scratch_world_aabb, 0,
463
+ scratch_local_aabb[0], scratch_local_aabb[1], scratch_local_aabb[2],
464
+ scratch_local_aabb[3], scratch_local_aabb[4], scratch_local_aabb[5],
465
+ p.x, p.y, p.z,
466
+ q.x, q.y, q.z, q.w
467
+ );
468
+
469
+ const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
470
+ const node = bvh.allocate_node();
471
+
472
+ bvh.node_set_aabb_primitive(
473
+ node,
474
+ scratch_world_aabb[0], scratch_world_aabb[1], scratch_world_aabb[2],
475
+ scratch_world_aabb[3], scratch_world_aabb[4], scratch_world_aabb[5]
476
+ );
477
+ bvh.node_set_user_data(node, rb._bodyId);
478
+ bvh.insert_leaf(node);
479
+
480
+ return node;
481
+ }
482
+
483
+ /**
484
+ * @private
485
+ * @param {RigidBody} rb
486
+ * @param {number} node
487
+ */
488
+ __remove_from_broadphase(rb, node) {
489
+ const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
490
+ bvh.remove_leaf(node);
491
+ bvh.release_node(node);
492
+ }
493
+
494
+ /**
495
+ * Lifecycle entry: invoked when an entity gains the (RigidBody, Transform)
496
+ * tuple. Allocates the body's slot in storage and seeds an empty collider
497
+ * list. Colliders are attached separately via {@link attach_collider} (the
498
+ * paired {@link ColliderObserverSystem} drives this from the dataset; in
499
+ * tests, call manually).
500
+ *
501
+ * @param {RigidBody} rigidBody
502
+ * @param {Transform} transform
503
+ * @param {number} entity
504
+ */
505
+ link(rigidBody, transform, entity) {
506
+ const packed = this.storage.allocate(entity);
507
+ rigidBody._bodyId = packed;
508
+ rigidBody.sleepState = SleepState.Awake;
509
+
510
+ const index = body_id_index(packed);
511
+ this.storage.set_kind(index, rigidBody.kind);
512
+ this.storage.set_flags(index, rigidBody.flags);
513
+
514
+ this.__bodies[index] = rigidBody;
515
+ this.__transforms[index] = transform;
516
+ this.__body_collider_lists[index] = [];
517
+
518
+ // Static bodies do not need to live in the active list — they never move.
519
+ if (rigidBody.kind === BodyKind.Static) {
520
+ this.storage.mark_sleeping(index);
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Detach every collider attached to this body, free its slot, and clear
526
+ * the side-tables.
527
+ *
528
+ * @param {RigidBody} rigidBody
529
+ * @param {Transform} transform
530
+ * @param {number} entity
531
+ */
532
+ unlink(rigidBody, transform, entity) {
533
+ const packed = rigidBody._bodyId;
534
+
535
+ assert.equal(this.storage.is_valid(packed), true, 'unlink: stale or absent body id');
536
+
537
+ // If the body is sleeping inside a multi-member sleep group, dissolve
538
+ // the group by waking every member first. Otherwise the surviving
539
+ // members would hold dangling indices in their sleep-group chain
540
+ // pointing at a slot that has just been freed.
541
+ if (rigidBody.sleep_group_next !== -1) {
542
+ this.__wake_body(rigidBody);
543
+ }
544
+
545
+ const index = body_id_index(packed);
546
+ const list = this.__body_collider_lists[index];
547
+ if (list !== undefined) {
548
+ for (let i = 0; i < list.length; i++) {
549
+ const entry = list[i];
550
+ if (entry.bvhNode !== COLLIDER_UNBOUND) {
551
+ this.__remove_from_broadphase(rigidBody, entry.bvhNode);
552
+ }
553
+ entry.collider._bvhNode = COLLIDER_UNBOUND;
554
+ entry.collider._bodyId = -1;
555
+ }
556
+ }
557
+
558
+ this.__bodies[index] = undefined;
559
+ this.__transforms[index] = undefined;
560
+ this.__body_collider_lists[index] = undefined;
561
+
562
+ this.storage.free(packed);
563
+ rigidBody._bodyId = RIGID_BODY_UNALLOCATED;
564
+ }
565
+
566
+ /**
567
+ * Attach a collider to an existing body. The collider can live on the
568
+ * same entity as the body (single-collider body) or on a child entity
569
+ * (compound body). The world transform passed here is the collider's
570
+ * own — for a same-entity collider it is the body's Transform; for a
571
+ * child collider it is the child entity's Transform.
572
+ *
573
+ * Idempotent — re-attaching the same collider is a no-op.
574
+ *
575
+ * @param {number} body_entity entity that owns the body
576
+ * @param {Collider} collider
577
+ * @param {Transform} transform world transform of the collider
578
+ * @param {number} [collider_entity] entity owning the collider (defaults to body_entity)
579
+ */
580
+ attach_collider(body_entity, collider, transform, collider_entity = body_entity) {
581
+ // Find the body by walking the storage entity table. The body must
582
+ // have been allocated via `link` before any colliders are attached.
583
+ const body_index = this.__find_body_index_by_entity(body_entity);
584
+ assert.notEqual(body_index, -1, `attach_collider: no body found for entity ${body_entity}`);
585
+
586
+ const rb = this.__bodies[body_index];
587
+ // Idempotent: skip if collider already attached.
588
+ if (collider._bvhNode !== COLLIDER_UNBOUND) return;
589
+
590
+ const node = this.__insert_into_broadphase(rb, collider, transform);
591
+
592
+ collider._bvhNode = node;
593
+ collider._bodyId = rb._bodyId;
594
+ this.__body_collider_lists[body_index].push({
595
+ collider, transform, entity: collider_entity, bvhNode: node,
596
+ });
597
+ }
598
+
599
+ /**
600
+ * Reverse of {@link attach_collider}. Idempotent.
601
+ *
602
+ * @param {number} body_entity
603
+ * @param {Collider} collider
604
+ */
605
+ detach_collider(body_entity, collider) {
606
+ if (collider._bvhNode === COLLIDER_UNBOUND) return;
607
+
608
+ const body_index = this.__find_body_index_by_entity(body_entity);
609
+ if (body_index === -1) return;
610
+
611
+ const rb = this.__bodies[body_index];
612
+ this.__remove_from_broadphase(rb, collider._bvhNode);
613
+
614
+ const list = this.__body_collider_lists[body_index];
615
+ for (let i = 0; i < list.length; i++) {
616
+ if (list[i].collider === collider) {
617
+ list.splice(i, 1);
618
+ break;
619
+ }
620
+ }
621
+
622
+ collider._bvhNode = COLLIDER_UNBOUND;
623
+ collider._bodyId = -1;
624
+ }
625
+
626
+ /**
627
+ * Resolve an entity to its body index, or -1 if no live body owns it.
628
+ * O(1) via {@link BodyStorage#index_of_entity}'s entity → index map — used
629
+ * on the collider attach / detach and joint link paths.
630
+ *
631
+ * @private
632
+ * @param {number} entity
633
+ * @returns {number} body index or -1
634
+ */
635
+ __find_body_index_by_entity(entity) {
636
+ return this.storage.index_of_entity(entity);
637
+ }
638
+
639
+ /**
640
+ * Register a {@link Joint} (6-DOF constraint). Resolves the joint's
641
+ * entities to packed body ids and adds it to the active set, where it is
642
+ * solved alongside contacts in the TGS substep loop. Body A must be a
643
+ * linked body; body B is either a linked body or {@link JOINT_WORLD}
644
+ * (anchoring A to a fixed world point — `localAnchorB` is then a world
645
+ * coordinate).
646
+ *
647
+ * @param {Joint} joint
648
+ */
649
+ link_joint(joint) {
650
+ const idxA = this.__find_body_index_by_entity(joint.entityA);
651
+ assert.notEqual(idxA, -1, `link_joint: no body for entityA ${joint.entityA}`);
652
+ joint._bodyIdA = this.__bodies[idxA]._bodyId;
653
+
654
+ if (joint.entityB === JOINT_WORLD) {
655
+ joint._bodyIdB = JOINT_WORLD;
656
+ } else {
657
+ const idxB = this.__find_body_index_by_entity(joint.entityB);
658
+ assert.notEqual(idxB, -1, `link_joint: no body for entityB ${joint.entityB}`);
659
+ joint._bodyIdB = this.__bodies[idxB]._bodyId;
660
+ }
661
+
662
+ // Reuse a freed slot if available so joint ids stay dense-ish.
663
+ let id;
664
+ if (this.__joint_free.length > 0) {
665
+ id = this.__joint_free.pop();
666
+ } else {
667
+ id = this.__joints.length;
668
+ }
669
+ joint._jointId = id;
670
+ this.__joints[id] = joint;
671
+ }
672
+
673
+ /**
674
+ * Remove a previously {@link link_joint}'d joint from the active set.
675
+ * Idempotent.
676
+ * @param {Joint} joint
677
+ */
678
+ unlink_joint(joint) {
679
+ const id = joint._jointId;
680
+ if (id === JOINT_UNALLOCATED || this.__joints[id] !== joint) return;
681
+ this.__joints[id] = undefined;
682
+ this.__joint_free.push(id);
683
+ joint._jointId = JOINT_UNALLOCATED;
684
+ }
685
+
686
+ /**
687
+ * Resolve a packed body id to its entity, or `-1` if the id is stale.
688
+ * @param {number} packed_body_id
689
+ * @returns {number}
690
+ */
691
+ entityOf(packed_body_id) {
692
+ if (!this.storage.is_valid(packed_body_id)) return -1;
693
+ return this.storage.entity_at(body_id_index(packed_body_id));
694
+ }
695
+
696
+ /**
697
+ * Number of live bodies (regardless of awake/sleeping state).
698
+ * @returns {number}
699
+ */
700
+ get bodyCount() {
701
+ return this.storage.size;
702
+ }
703
+
704
+ /**
705
+ * Apply an instantaneous change of momentum at the body's centre of mass.
706
+ * Linear-only — see {@link applyImpulseAt} for an off-centre impulse that
707
+ * also produces angular response.
708
+ *
709
+ * Wakes the body if it is asleep.
710
+ *
711
+ * @param {RigidBody} rigidBody
712
+ * @param {Vector3|{x:number,y:number,z:number}} impulse
713
+ */
714
+ applyImpulse(rigidBody, impulse) {
715
+ if (rigidBody.kind !== BodyKind.Dynamic) {
716
+ return;
717
+ }
718
+ const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
719
+ rigidBody.linearVelocity.addScaled(impulse, inv_m);
720
+
721
+ this.__wake_body(rigidBody);
722
+ }
723
+
724
+ /**
725
+ * Apply an instantaneous change of momentum at a specific world-space point.
726
+ * Off-centre impulses produce both linear (Δv = P/m) and angular
727
+ * (Δω = I_w⁻¹·(r × P)) response.
728
+ *
729
+ * Wakes the body if it is asleep.
730
+ *
731
+ * @param {RigidBody} rigidBody
732
+ * @param {Transform} transform body's current world Transform (used for r and I_w)
733
+ * @param {Vector3|{x:number,y:number,z:number}} impulse
734
+ * @param {Vector3|{x:number,y:number,z:number}} worldPoint
735
+ */
736
+ applyImpulseAt(rigidBody, transform, impulse, worldPoint) {
737
+ if (rigidBody.kind !== BodyKind.Dynamic) {
738
+ return;
739
+ }
740
+ const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
741
+
742
+ rigidBody.linearVelocity.set(
743
+ rigidBody.linearVelocity.x + impulse.x * inv_m,
744
+ rigidBody.linearVelocity.y + impulse.y * inv_m,
745
+ rigidBody.linearVelocity.z + impulse.z * inv_m
746
+ );
747
+
748
+ const rx = worldPoint.x - transform.position.x;
749
+ const ry = worldPoint.y - transform.position.y;
750
+ const rz = worldPoint.z - transform.position.z;
751
+
752
+ // Δω = I_w⁻¹ · (r × P)
753
+ const tx = ry * impulse.z - rz * impulse.y;
754
+ const ty = rz * impulse.x - rx * impulse.z;
755
+ const tz = rx * impulse.y - ry * impulse.x;
756
+
757
+ world_inverse_inertia_apply(scratch_angular_delta, 0, rigidBody.inverseInertiaLocal, transform.rotation, tx, ty, tz);
758
+
759
+ rigidBody.angularVelocity.set(
760
+ rigidBody.angularVelocity.x + scratch_angular_delta[0],
761
+ rigidBody.angularVelocity.y + scratch_angular_delta[1],
762
+ rigidBody.angularVelocity.z + scratch_angular_delta[2]
763
+ );
764
+
765
+ this.__wake_body(rigidBody);
766
+ }
767
+
768
+ /**
769
+ * Accumulate a continuous torque (world-space) for integration on the
770
+ * next fixedUpdate. Pairs with {@link applyForce} for the rotational case.
771
+ *
772
+ * Wakes the body if asleep.
773
+ *
774
+ * @param {RigidBody} rigidBody
775
+ * @param {Vector3|{x:number,y:number,z:number}} torque
776
+ */
777
+ applyTorque(rigidBody, torque) {
778
+ if (rigidBody.kind !== BodyKind.Dynamic) {
779
+ return;
780
+ }
781
+ rigidBody.accumulatedTorque.set(
782
+ rigidBody.accumulatedTorque.x + torque.x,
783
+ rigidBody.accumulatedTorque.y + torque.y,
784
+ rigidBody.accumulatedTorque.z + torque.z
785
+ );
786
+ this.__wake_body(rigidBody);
787
+ }
788
+
789
+ /**
790
+ * Apply a continuous force at a specific world-space point. The force
791
+ * generates both a linear acceleration (F/m) and a torque (r × F) about
792
+ * the body's centre of mass.
793
+ *
794
+ * Wakes the body if asleep.
795
+ *
796
+ * @param {RigidBody} rigidBody
797
+ * @param {Transform} transform body's current world Transform
798
+ * @param {Vector3|{x:number,y:number,z:number}} force
799
+ * @param {Vector3|{x:number,y:number,z:number}} worldPoint
800
+ */
801
+ applyForceAt(rigidBody, transform, force, worldPoint) {
802
+ if (rigidBody.kind !== BodyKind.Dynamic) {
803
+ return;
804
+ }
805
+ rigidBody.accumulatedForce.set(
806
+ rigidBody.accumulatedForce.x + force.x,
807
+ rigidBody.accumulatedForce.y + force.y,
808
+ rigidBody.accumulatedForce.z + force.z
809
+ );
810
+
811
+ const rx = worldPoint.x - transform.position.x;
812
+ const ry = worldPoint.y - transform.position.y;
813
+ const rz = worldPoint.z - transform.position.z;
814
+
815
+ rigidBody.accumulatedTorque.set(
816
+ rigidBody.accumulatedTorque.x + (ry * force.z - rz * force.y),
817
+ rigidBody.accumulatedTorque.y + (rz * force.x - rx * force.z),
818
+ rigidBody.accumulatedTorque.z + (rx * force.y - ry * force.x)
819
+ );
820
+
821
+ this.__wake_body(rigidBody);
822
+ }
823
+
824
+ /**
825
+ * Accumulate a continuous force to be integrated next fixedUpdate step.
826
+ * Wakes the body if asleep.
827
+ *
828
+ * @param {RigidBody} rigidBody
829
+ * @param {Vector3|{x:number,y:number,z:number}} force
830
+ */
831
+ applyForce(rigidBody, force) {
832
+ if (rigidBody.kind !== BodyKind.Dynamic) {
833
+ return;
834
+ }
835
+ rigidBody.accumulatedForce.add(force);
836
+ this.__wake_body(rigidBody);
837
+ }
838
+
839
+ /**
840
+ * Replace the linear velocity. Wakes the body if asleep.
841
+ *
842
+ * @param {RigidBody} rigidBody
843
+ * @param {Vector3|{x:number,y:number,z:number}} v
844
+ */
845
+ setLinearVelocity(rigidBody, v) {
846
+ rigidBody.linearVelocity.copy(v);
847
+ if (rigidBody.kind === BodyKind.Dynamic) {
848
+ this.__wake_body(rigidBody);
849
+ }
850
+ }
851
+
852
+ /**
853
+ * Teleport a body to a new pose, bypassing integration: writes the body's
854
+ * Transform directly and wakes it. For an interpolated body this also flags a
855
+ * render `snap` on its {@link Interpolated} component, so the producer keeps
856
+ * this pose (rather than restoring the previous tick over it) and the
857
+ * renderer shows the new pose without sliding across the jump.
858
+ *
859
+ * This is the authoritative way to reposition an interpolated body — a raw
860
+ * `Transform` write would be undone by the per-step restore. Velocity is left
861
+ * as-is; zero it via {@link setLinearVelocity} if the teleport should also
862
+ * stop the body.
863
+ *
864
+ * @param {RigidBody} rigidBody
865
+ * @param {Vector3|{x:number,y:number,z:number}} position world position
866
+ * @param {Quaternion|{x:number,y:number,z:number,w:number}} rotation world unit-quaternion rotation
867
+ */
868
+ setPose(rigidBody, position, rotation) {
869
+ const idx = body_id_index(rigidBody._bodyId);
870
+ const transform = this.__transforms[idx];
871
+ if (transform === undefined) {
872
+ return;
873
+ }
874
+
875
+ transform.position.set(position.x, position.y, position.z);
876
+ transform.rotation.set(rotation.x, rotation.y, rotation.z, rotation.w);
877
+
878
+ this.__wake_body(rigidBody);
879
+
880
+ // Flag a snap on the body's Interpolated component, if any, so the
881
+ // interpolation producer (restore) and consumer (blend) treat this pose
882
+ // as authoritative this frame instead of interpolating across the jump.
883
+ const em = this.entityManager;
884
+ if (em !== null && em !== undefined) {
885
+ const dataset = em.dataset;
886
+ if (dataset !== null && dataset !== undefined) {
887
+ const entity = this.storage.entity_at(idx);
888
+ const interpolated = dataset.getComponent(entity, Interpolated);
889
+ if (interpolated !== undefined && interpolated !== null) {
890
+ interpolated.snap = true;
891
+ }
892
+ }
893
+ }
894
+ }
895
+
896
+ /**
897
+ * Force the body awake. Static bodies are ignored.
898
+ * @param {RigidBody} rigidBody
899
+ */
900
+ wake(rigidBody) {
901
+ this.__wake_body(rigidBody);
902
+ }
903
+
904
+ /**
905
+ * Force the body asleep. Dynamic bodies will not re-enter the active list
906
+ * until a wake event occurs.
907
+ * @param {RigidBody} rigidBody
908
+ */
909
+ sleep(rigidBody) {
910
+ if (rigidBody.kind !== BodyKind.Dynamic) {
911
+ return;
912
+ }
913
+ if (rigidBody.sleepState === SleepState.Sleeping) {
914
+ return;
915
+ }
916
+ rigidBody.sleepState = SleepState.Sleeping;
917
+ const index = body_id_index(rigidBody._bodyId);
918
+ this.storage.mark_sleeping(index);
919
+ }
920
+
921
+ /**
922
+ * Wake a body and atomically wake every other body it was last sleeping
923
+ * with (its "sleep group"). Sleep groups are circular doubly-linked lists
924
+ * threaded through every member of an island when it sleeps atomically;
925
+ * waking any one member walks the chain and wakes the rest in the same
926
+ * call.
927
+ *
928
+ * Without this, a 100-block stack hit at the base would wake one block
929
+ * per frame as the broadphase propagated awareness up the stack — a
930
+ * visible ~1.6 s wave at 60 fps. Atomic wake eliminates the wave.
931
+ *
932
+ * No-op for non-dynamic bodies. Idempotent for already-awake bodies.
933
+ * @private
934
+ * @param {RigidBody} rb
935
+ */
936
+ __wake_body(rb) {
937
+ if (rb.kind !== BodyKind.Dynamic) return;
938
+ if (rb.sleepState === SleepState.Awake) return;
939
+ const index = body_id_index(rb._bodyId);
940
+
941
+ // Remember the next-in-chain before clearing the body's own pointers;
942
+ // the rest of the group is reached by walking forward from there.
943
+ const start_next = rb.sleep_group_next;
944
+
945
+ rb.sleepState = SleepState.Awake;
946
+ rb.sleep_timer = 0;
947
+ rb.sleep_group_next = -1;
948
+ rb.sleep_group_prev = -1;
949
+ this.storage.mark_awake(index);
950
+
951
+ if (start_next === -1 || start_next === index) return;
952
+
953
+ // Walk the (now-broken) chain forward until we loop back. The chain
954
+ // is circular so we know when to stop; defensive `-1` guards against
955
+ // corruption from a body being unlinked mid-sleep-group.
956
+ let cur = start_next;
957
+ while (cur !== -1 && cur !== index) {
958
+ const cur_rb = this.__bodies[cur];
959
+ if (cur_rb === undefined) break;
960
+ const nxt = cur_rb.sleep_group_next;
961
+ cur_rb.sleepState = SleepState.Awake;
962
+ cur_rb.sleep_timer = 0;
963
+ cur_rb.sleep_group_next = -1;
964
+ cur_rb.sleep_group_prev = -1;
965
+ this.storage.mark_awake(cur);
966
+ cur = nxt;
967
+ }
968
+ }
969
+
970
+ /**
971
+ * Atomically put every body in a contiguous range of island members to
972
+ * sleep. Members are threaded into a circular doubly-linked list so any
973
+ * future `wake` on any member walks the chain and revives them all.
974
+ *
975
+ * Velocities are zeroed because the body is by definition at rest at
976
+ * this point — the alternative (storing residual velocities for "softer"
977
+ * wake) is what Bullet does, but for a deterministic game-physics target
978
+ * fully resetting is simpler and avoids drift while sleeping.
979
+ *
980
+ * @private
981
+ * @param {Uint32Array} member_array view (or full array) of body indices
982
+ * @param {number} start
983
+ * @param {number} end
984
+ */
985
+ __atomic_sleep_island_range(member_array, start, end) {
986
+ const count = end - start;
987
+ if (count === 0) return;
988
+ const bodies = this.__bodies;
989
+ const storage = this.storage;
990
+
991
+ if (count === 1) {
992
+ const idx = member_array[start];
993
+ const rb = bodies[idx];
994
+ if (rb === undefined) return;
995
+ rb.sleep_group_next = -1;
996
+ rb.sleep_group_prev = -1;
997
+ rb.sleepState = SleepState.Sleeping;
998
+ rb.linearVelocity[0] = 0;
999
+ rb.linearVelocity[1] = 0;
1000
+ rb.linearVelocity[2] = 0;
1001
+ rb.angularVelocity[0] = 0;
1002
+ rb.angularVelocity[1] = 0;
1003
+ rb.angularVelocity[2] = 0;
1004
+ storage.mark_sleeping(idx);
1005
+ return;
1006
+ }
1007
+
1008
+ for (let i = 0; i < count; i++) {
1009
+ const idx = member_array[start + i];
1010
+ const rb = bodies[idx];
1011
+ if (rb === undefined) continue;
1012
+ const next_idx = member_array[start + ((i + 1) % count)];
1013
+ const prev_idx = member_array[start + ((i - 1 + count) % count)];
1014
+ rb.sleep_group_next = next_idx;
1015
+ rb.sleep_group_prev = prev_idx;
1016
+ rb.sleepState = SleepState.Sleeping;
1017
+ rb.linearVelocity[0] = 0;
1018
+ rb.linearVelocity[1] = 0;
1019
+ rb.linearVelocity[2] = 0;
1020
+ rb.angularVelocity[0] = 0;
1021
+ rb.angularVelocity[1] = 0;
1022
+ rb.angularVelocity[2] = 0;
1023
+ storage.mark_sleeping(idx);
1024
+ }
1025
+ }
1026
+
1027
+ /**
1028
+ * Broadphase raycast against both BVHs. Fills `result` with the nearest
1029
+ * hit and returns `true` on hit, `false` on miss.
1030
+ *
1031
+ * Narrowphase refinement against the actual shape geometry is a
1032
+ * follow-up — for now `result.t` is the distance to the leaf's
1033
+ * inflated AABB and `result.normal` is the AABB face normal. Both are
1034
+ * exact for AABB-shaped colliders.
1035
+ *
1036
+ * @param {Ray3} ray origin + unit direction + `tMax`
1037
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
1038
+ * @param {(entity:number, collider:Collider)=>boolean} [filter] defaults
1039
+ * to {@link returnTrue} (accept every candidate)
1040
+ * @returns {boolean}
1041
+ */
1042
+ raycast(ray, result, filter = returnTrue) {
1043
+ return raycast_query(this, ray, result, filter);
1044
+ }
1045
+
1046
+ /**
1047
+ * Sweep a convex shape along a ray and find the first body it
1048
+ * would hit. The shape starts at `ray.origin` oriented by
1049
+ * `rotation` and translates along `ray.direction` for up to
1050
+ * `ray.tMax` units. Returns the nearest impact within that
1051
+ * interval.
1052
+ *
1053
+ * The "swept AABB" broadphase finds candidate bodies whose BVH
1054
+ * leaves overlap the shape's swept volume; the narrowphase then
1055
+ * bisects [0, t] on GJK overlap to find the time-of-impact for
1056
+ * each candidate. Best-t early termination skips candidates that
1057
+ * can't tighten the answer.
1058
+ *
1059
+ * @param {Ray3} ray origin + unit direction + `tMax`
1060
+ * @param {AbstractShape3D} shape
1061
+ * @param {{x:number,y:number,z:number,w:number}} rotation
1062
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
1063
+ * @param {(entity:number, collider:Collider)=>boolean} [filter]
1064
+ * @returns {boolean}
1065
+ */
1066
+ shapeCast(ray, shape, rotation, result, filter = returnTrue) {
1067
+ return shape_cast_query(this, ray, shape, rotation, result, filter);
1068
+ }
1069
+
1070
+ /**
1071
+ * Speculative overlap query: find all bodies whose collider would
1072
+ * overlap the given convex shape placed at the given world pose,
1073
+ * without mutating the simulation. Intended for kinematic /
1074
+ * character controllers that need to test "would I collide if I
1075
+ * moved here?" before committing the move.
1076
+ *
1077
+ * Pipeline:
1078
+ * 1. The shape's world AABB is computed from `position` + `rotation`.
1079
+ * 2. Both broadphase trees (static + dynamic) are queried for
1080
+ * bodies whose leaf AABB overlaps that envelope.
1081
+ * 3. Each candidate is GJK-tested in world frame. Convex
1082
+ * candidates run one GJK call; concave candidates (heightmap,
1083
+ * mesh) run per-triangle GJK via the decomposition path.
1084
+ * 4. The optional `filter` is consulted before the GJK test —
1085
+ * useful for skipping the caller's own body, allies, sensors,
1086
+ * etc.
1087
+ *
1088
+ * The output buffer is filled with overlapping bodies' `body_id`
1089
+ * values (uint32 with packed generation), starting at
1090
+ * `output_offset`. The caller is responsible for sizing the buffer;
1091
+ * IDs past its end are dropped silently and the count caps at the
1092
+ * available space.
1093
+ *
1094
+ * The query shape must be convex. Concave query shapes throw —
1095
+ * they're typically static terrain and not used as kinematic
1096
+ * probes; the M×N triangle-pair cost wouldn't be worth the rare
1097
+ * use case.
1098
+ *
1099
+ * @param {AbstractShape3D} shape convex query shape, in its local frame
1100
+ * @param {{x:number,y:number,z:number}} position world position of
1101
+ * the query shape
1102
+ * @param {{x:number,y:number,z:number,w:number}} rotation world
1103
+ * rotation (unit quaternion)
1104
+ * @param {Uint32Array|number[]} output buffer to receive body_ids
1105
+ * @param {number} output_offset starting index in output
1106
+ * @param {(entity:number, collider:Collider)=>boolean} [filter]
1107
+ * defaults to {@link returnTrue}
1108
+ * @returns {number} number of overlapping bodies written
1109
+ */
1110
+ overlap(shape, position, rotation, output, output_offset, filter = returnTrue) {
1111
+ return overlap_shape_query(this, shape, position, rotation, output, output_offset, filter);
1112
+ }
1113
+
1114
+ /**
1115
+ * Wake any sleeping body that appears in this step's broadphase pair list.
1116
+ * A pair means the BVH AABBs overlap — even if the sleeper hasn't moved,
1117
+ * an awake neighbour has come into contact range and the sleeper must
1118
+ * participate in narrowphase / solve.
1119
+ * @private
1120
+ */
1121
+ __wake_pairs() {
1122
+ const list = this.pairs;
1123
+ const n = list.count;
1124
+ const bodies = this.__bodies;
1125
+ for (let i = 0; i < n; i++) {
1126
+ const idA = list.get_a(i);
1127
+ const idB = list.get_b(i);
1128
+ const idxA = body_id_index(idA);
1129
+ const idxB = body_id_index(idB);
1130
+ const a = bodies[idxA];
1131
+ const b = bodies[idxB];
1132
+ if (a !== undefined && a.sleepState === SleepState.Sleeping) {
1133
+ this.__wake_body(a);
1134
+ }
1135
+ if (b !== undefined && b.sleepState === SleepState.Sleeping) {
1136
+ this.__wake_body(b);
1137
+ }
1138
+ }
1139
+ }
1140
+
1141
+ /**
1142
+ * Wake propagation across joints: if a joint connects an awake body to a
1143
+ * sleeping one, wake the sleeper so the constraint stays coupled (a joint
1144
+ * with one body asleep and one awake would otherwise be skipped by the
1145
+ * solver, letting the awake side drift). A common trigger is a
1146
+ * kinematic/motor-driven body pulling on a sleeping chain.
1147
+ *
1148
+ * Bodies that slept together as one island share a sleep group, so the
1149
+ * usual atomic wake already revives the whole chain when any member is
1150
+ * hit; this catches the cases that atomic wake doesn't (joint spanning
1151
+ * separate groups, kinematic driver).
1152
+ * @private
1153
+ */
1154
+ __wake_joints() {
1155
+ const joints = this.__joints;
1156
+ const n = joints.length;
1157
+ if (n === 0) return;
1158
+ const bodies = this.__bodies;
1159
+ const storage = this.storage;
1160
+ for (let i = 0; i < n; i++) {
1161
+ const joint = joints[i];
1162
+ if (joint === undefined || joint === null) continue;
1163
+ if (joint._bodyIdB === JOINT_WORLD) continue;
1164
+ if (!storage.is_valid(joint._bodyIdA) || !storage.is_valid(joint._bodyIdB)) continue;
1165
+ const a = bodies[body_id_index(joint._bodyIdA)];
1166
+ const b = bodies[body_id_index(joint._bodyIdB)];
1167
+ if (a === undefined || b === undefined) continue;
1168
+ const aSleep = a.sleepState === SleepState.Sleeping;
1169
+ const bSleep = b.sleepState === SleepState.Sleeping;
1170
+ if (aSleep === bSleep) continue; // both awake or both asleep — leave as is
1171
+ if (aSleep) this.__wake_body(a); else this.__wake_body(b);
1172
+ }
1173
+ }
1174
+
1175
+ /**
1176
+ * Per-island atomic sleep test. Walks each island once and applies the
1177
+ * decision uniformly across all members:
1178
+ *
1179
+ * - If any member carries {@link RigidBodyFlags.DisableSleep}, the
1180
+ * entire island is exempt; every member's sleep_timer is reset.
1181
+ * - If `max(|v|² + |ω|²)` across all members is below
1182
+ * {@link sleepVelocitySqrThreshold}, every member's sleep_timer is
1183
+ * incremented by `dt`. When the smallest member's timer crosses
1184
+ * {@link sleepTimeThreshold}, the whole island sleeps atomically in
1185
+ * the same step (members get threaded into a sleep-group chain so
1186
+ * {@link __wake_body} can wake them all in one call).
1187
+ * - Otherwise the island has at least one active member, so every
1188
+ * member's timer is reset.
1189
+ *
1190
+ * This is the design-plan atomic-island sleep — replaces the per-body
1191
+ * approximation that lived in this slot during the previous slice.
1192
+ * Weakly-connected piles no longer chatter awake when a single member
1193
+ * blips above threshold; piles fall asleep and wake up as one.
1194
+ *
1195
+ * @private
1196
+ * @param {number} dt
1197
+ */
1198
+ __sleep_test(dt) {
1199
+ const threshold_sqr = this.sleepVelocitySqrThreshold;
1200
+ const time_threshold = this.sleepTimeThreshold;
1201
+ const bodies = this.__bodies;
1202
+ const islands = this.islands;
1203
+ const island_count = islands.island_count;
1204
+ const body_offsets = islands.body_offsets;
1205
+ const body_data = islands.body_data;
1206
+
1207
+ for (let isl = 0; isl < island_count; isl++) {
1208
+ const start = body_offsets[isl];
1209
+ const end = body_offsets[isl + 1];
1210
+ if (end === start) continue;
1211
+
1212
+ // Pass 1: find max v² + check DisableSleep across the island.
1213
+ let max_v_sqr = 0;
1214
+ let any_disable_sleep = false;
1215
+ for (let i = start; i < end; i++) {
1216
+ const idx = body_data[i];
1217
+ const rb = bodies[idx];
1218
+ if (rb === undefined) continue;
1219
+ if ((rb.flags & RigidBodyFlags.DisableSleep) !== 0) {
1220
+ any_disable_sleep = true;
1221
+ break;
1222
+ }
1223
+ const lv = rb.linearVelocity;
1224
+ const av = rb.angularVelocity;
1225
+ const v_sqr = lv[0] * lv[0] + lv[1] * lv[1] + lv[2] * lv[2]
1226
+ + av[0] * av[0] + av[1] * av[1] + av[2] * av[2];
1227
+ if (v_sqr > max_v_sqr) max_v_sqr = v_sqr;
1228
+ }
1229
+
1230
+ if (any_disable_sleep) {
1231
+ // Whole island is exempt — reset every member's timer.
1232
+ for (let i = start; i < end; i++) {
1233
+ const rb = bodies[body_data[i]];
1234
+ if (rb !== undefined) rb.sleep_timer = 0;
1235
+ }
1236
+ continue;
1237
+ }
1238
+
1239
+ if (max_v_sqr < threshold_sqr) {
1240
+ // Island is at rest — increment every member's timer; if the
1241
+ // slowest-stabilising member has crossed the time threshold,
1242
+ // every member has (they were incremented together this step),
1243
+ // so atomic-sleep the island.
1244
+ let min_timer = Infinity;
1245
+ for (let i = start; i < end; i++) {
1246
+ const rb = bodies[body_data[i]];
1247
+ if (rb === undefined) continue;
1248
+ rb.sleep_timer += dt;
1249
+ if (rb.sleep_timer < min_timer) min_timer = rb.sleep_timer;
1250
+ }
1251
+ if (min_timer >= time_threshold) {
1252
+ this.__atomic_sleep_island_range(body_data, start, end);
1253
+ }
1254
+ } else {
1255
+ // At least one member is active — reset every timer.
1256
+ for (let i = start; i < end; i++) {
1257
+ const rb = bodies[body_data[i]];
1258
+ if (rb !== undefined) rb.sleep_timer = 0;
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+
1264
+ /**
1265
+ * Dispatch every buffered contact event to the entities involved, via the
1266
+ * dataset's per-entity event channel — `dataset.sendEvent(entity, name,
1267
+ * payload)`, once for each of the pair's entities. This is the only
1268
+ * contact-event path: a system with no dataset attached has nowhere to
1269
+ * deliver and emits nothing.
1270
+ *
1271
+ * Payload is a reused scratch object; listeners must copy anything they
1272
+ * intend to retain past the listener body.
1273
+ * @private
1274
+ */
1275
+ __dispatch_contact_events() {
1276
+ const events = this.contactEvents;
1277
+ const n = events.count;
1278
+ if (n === 0) return;
1279
+
1280
+ const ecd = (this.entityManager !== null && this.entityManager !== undefined)
1281
+ ? this.entityManager.dataset
1282
+ : null;
1283
+ // No dataset → nowhere to deliver. The buffer is cleared by
1284
+ // diff_manifolds each step regardless, so nothing leaks.
1285
+ if (ecd === null || ecd === undefined) return;
1286
+
1287
+ const manifolds = this.manifolds;
1288
+ const data = manifolds.data_buffer;
1289
+
1290
+ const payload = this.__contact_payload;
1291
+
1292
+ for (let i = 0; i < n; i++) {
1293
+ const kind = events.kind_at(i);
1294
+ const entA = events.entityA_at(i);
1295
+ const entB = events.entityB_at(i);
1296
+ const slot = events.slot_at(i);
1297
+
1298
+ // Use the deepest contact of the manifold as the representative
1299
+ // point/normal/depth for the event. v1: contact 0 only.
1300
+ const slot_off = manifolds.slot_data_offset(slot);
1301
+ const has_contact = manifolds.contact_count(slot) > 0;
1302
+
1303
+ // Scratch payload — write the point/normal components by index to
1304
+ // skip the Vector3 change-signal dispatch (nothing observes them).
1305
+ const pt = payload.point;
1306
+ const nm = payload.normal;
1307
+ if (has_contact) {
1308
+ const wax = data[slot_off], way = data[slot_off + 1], waz = data[slot_off + 2];
1309
+ const wbx = data[slot_off + 3], wby = data[slot_off + 4], wbz = data[slot_off + 5];
1310
+ pt[0] = (wax + wbx) * 0.5;
1311
+ pt[1] = (way + wby) * 0.5;
1312
+ pt[2] = (waz + wbz) * 0.5;
1313
+ nm[0] = data[slot_off + 6];
1314
+ nm[1] = data[slot_off + 7];
1315
+ nm[2] = data[slot_off + 8];
1316
+ payload.depth = data[slot_off + 9];
1317
+ } else {
1318
+ pt[0] = 0;
1319
+ pt[1] = 0;
1320
+ pt[2] = 0;
1321
+ nm[0] = 0;
1322
+ nm[1] = 0;
1323
+ nm[2] = 0;
1324
+ payload.depth = 0;
1325
+ }
1326
+ payload.entityA = entA;
1327
+ payload.entityB = entB;
1328
+
1329
+ let event_name;
1330
+ if (kind === ContactEventKind.Begin) {
1331
+ event_name = PhysicsEvents.ContactBegin;
1332
+ } else if (kind === ContactEventKind.Stay) {
1333
+ event_name = PhysicsEvents.ContactStay;
1334
+ } else {
1335
+ event_name = PhysicsEvents.ContactEnd;
1336
+ }
1337
+
1338
+ if (entA >= 0) ecd.sendEvent(entA, event_name, payload);
1339
+ if (entB >= 0) ecd.sendEvent(entB, event_name, payload);
1340
+ }
1341
+ }
1342
+
1343
+ /**
1344
+ * Producer — restore pass. At the top of a fixed step, reset every awake
1345
+ * {@link Interpolated} body's live components to their authoritative state
1346
+ * from the previous tick's snapshot, undoing any render-time interpolation
1347
+ * the {@link InterpolationSystem} wrote between frames so the sim integrates
1348
+ * from truth, not an interpolated pose. A body with no previous snapshot
1349
+ * (first step ever, or just woken) is left as-is — its live state is already
1350
+ * authoritative. No-op unless {@link interpolationLog} is wired.
1351
+ * @private
1352
+ */
1353
+ __interp_restore() {
1354
+ const log = this.interpolationLog;
1355
+ const em = this.entityManager;
1356
+ if (log === null || em === null || em === undefined) return;
1357
+ const dataset = em.dataset;
1358
+ if (dataset === null || dataset === undefined) return;
1359
+
1360
+ const prev_tick = em.fixedStepTick - 1;
1361
+ if (prev_tick < 0) return;
1362
+
1363
+ const storage = this.storage;
1364
+ const count = storage.awake_count;
1365
+ const scratch = this.__interp_scratch;
1366
+
1367
+ for (let i = 0; i < count; i++) {
1368
+ const idx = storage.awake_at(i);
1369
+ const entity = storage.entity_at(idx);
1370
+ const interpolated = dataset.getComponent(entity, Interpolated);
1371
+ if (interpolated === undefined || interpolated === null) continue;
1372
+ // A teleported body (snap set, e.g. via setPose) keeps its live pose
1373
+ // this step — restoring the previous tick would undo the teleport.
1374
+ if (interpolated.snap) continue;
1375
+ const key = interpolated.key;
1376
+ if (key < 0) continue;
1377
+
1378
+ const interpolands = interpolated.interpolands;
1379
+ for (let k = 0; k < interpolands.length; k++) {
1380
+ const ip = interpolands[k];
1381
+ scratch.position = 0;
1382
+ // Snap to the previous tick's snapshot (both offsets equal → t irrelevant).
1383
+ const ok = log.interpolate(scratch, key, ip.type_id, prev_tick, prev_tick, 0, ip.interpolation_adapter);
1384
+ if (!ok) continue;
1385
+ const target = dataset.getComponent(entity, ip.component_class);
1386
+ if (target === undefined || target === null) continue;
1387
+ scratch.position = 0;
1388
+ ip.serialization_adapter.deserialize(scratch, target);
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ /**
1394
+ * Producer — record pass. At the end of a fixed step, snapshot every awake
1395
+ * {@link Interpolated} body's live components into the shared log under the
1396
+ * current `entityManager.fixedStepTick`. The render-time
1397
+ * {@link InterpolationSystem} blends consecutive ticks from these snapshots.
1398
+ * Only awake (moving) bodies are recorded, so the log stays sparse. No-op
1399
+ * unless {@link interpolationLog} is wired.
1400
+ * @private
1401
+ */
1402
+ __interp_record() {
1403
+ const log = this.interpolationLog;
1404
+ const em = this.entityManager;
1405
+ if (log === null || em === null || em === undefined) return;
1406
+ const dataset = em.dataset;
1407
+ if (dataset === null || dataset === undefined) return;
1408
+
1409
+ const tick = em.fixedStepTick;
1410
+ const storage = this.storage;
1411
+ const count = storage.awake_count;
1412
+
1413
+ log.begin_tick(tick);
1414
+ for (let i = 0; i < count; i++) {
1415
+ const idx = storage.awake_at(i);
1416
+ const entity = storage.entity_at(idx);
1417
+ const interpolated = dataset.getComponent(entity, Interpolated);
1418
+ if (interpolated === undefined || interpolated === null) continue;
1419
+ const key = interpolated.key;
1420
+ if (key < 0) continue;
1421
+
1422
+ const interpolands = interpolated.interpolands;
1423
+ for (let k = 0; k < interpolands.length; k++) {
1424
+ const ip = interpolands[k];
1425
+ const target = dataset.getComponent(entity, ip.component_class);
1426
+ if (target === undefined || target === null) continue;
1427
+ const buf = log.begin_record(key, ip.type_id);
1428
+ ip.serialization_adapter.serialize(buf, target);
1429
+ log.end_record();
1430
+ }
1431
+ }
1432
+ log.end_tick();
1433
+ }
1434
+
1435
+ fixedUpdate(dt) {
1436
+ // Producer: restore authoritative pose (undo render interpolation)
1437
+ // before the sim reads any Transform this step.
1438
+ this.__interp_restore();
1439
+
1440
+ const gx = this.gravity.x;
1441
+ const gy = this.gravity.y;
1442
+ const gz = this.gravity.z;
1443
+
1444
+ const storage = this.storage;
1445
+ const bodies = this.__bodies;
1446
+ const transforms = this.__transforms;
1447
+ const joints = this.__joints;
1448
+ const manifolds = this.manifolds;
1449
+
1450
+
1451
+ const count = storage.awake_count;
1452
+
1453
+ // Stage 1: consume the per-frame force / torque accumulators into
1454
+ // velocity at the full `dt`, exactly once (a user force is a per-frame
1455
+ // budget that must land in full regardless of substep count). Gravity
1456
+ // is applied per substep below, so the trajectory integrates at the
1457
+ // substep rate and — crucially — each substep's gravity is balanced by
1458
+ // that substep's contact warm-start, keeping resting stacks at zero
1459
+ // velocity.
1460
+ for (let i = 0; i < count; i++) {
1461
+ const idx = storage.awake_at(i);
1462
+
1463
+ const rb = bodies[idx];
1464
+ const tr = transforms[idx];
1465
+
1466
+ integrate_velocity_forces(rb, tr, dt);
1467
+ }
1468
+
1469
+ // Stage 2: refit each awake body's collider leaves at the current
1470
+ // pose, padded by the swept extent for the body's velocity. The fat
1471
+ // margin uses the post-force velocity; this frame's gravity increment
1472
+ // is a sub-millimetre slack difference, safely inside the margin.
1473
+ const lists = this.__body_collider_lists;
1474
+ for (let i = 0; i < count; i++) {
1475
+
1476
+ const idx = storage.awake_at(i);
1477
+ const rb = bodies[idx];
1478
+ const list = lists[idx];
1479
+
1480
+ if (list === undefined) {
1481
+ continue;
1482
+ }
1483
+
1484
+ const lv = rb.linearVelocity;
1485
+
1486
+ const list_length = list.length;
1487
+
1488
+ for (let k = 0; k < list_length; k++) {
1489
+
1490
+ const entry = list[k];
1491
+
1492
+ compute_fat_world_aabb(
1493
+ scratch_world_aabb, 0,
1494
+ entry.collider.shape, entry.transform,
1495
+ lv[0], lv[1], lv[2],
1496
+ dt
1497
+ );
1498
+
1499
+ this.dynamicBvh.node_move_aabb(entry.bvhNode, scratch_world_aabb);
1500
+ }
1501
+ }
1502
+
1503
+ // Stage 3: broadphase pair generation. The fat AABBs cover the full
1504
+ // outer-step motion, so the pair set stays valid across all substeps
1505
+ // — broadphase runs once.
1506
+ generate_pairs(
1507
+ storage,
1508
+ this.dynamicBvh,
1509
+ this.staticBvh,
1510
+ manifolds,
1511
+ lists,
1512
+ this.pairs,
1513
+ this.__pair_filter,
1514
+ this,
1515
+ );
1516
+
1517
+ // Stage 4: wake propagation — through broadphase pairs, then through
1518
+ // joints (a joint must not have one body awake and one asleep).
1519
+ this.__wake_pairs();
1520
+ this.__wake_joints();
1521
+
1522
+ // Stage 5: narrowphase — once per outer step. The substep loop below
1523
+ // re-derives each contact's penetration analytically from the moved
1524
+ // poses rather than re-running geometry.
1525
+ narrowphase_step(this.pairs, manifolds, this.__body_collider_lists);
1526
+
1527
+ // Stage 6: partition awake bodies + touched contacts into islands.
1528
+ // Consumed by the solver (flattened contact list) and the sleep test.
1529
+ this.islands.build(storage, manifolds, bodies, this.__body_collider_lists, joints);
1530
+
1531
+ // Stage 7: TGS substep loop.
1532
+ //
1533
+ // prepare_contacts captures per-contact anchors / effective masses /
1534
+ // approach velocity (no warm-start — that's per-substep). Each substep
1535
+ // integrates gravity by `h`, re-derives contact geometry from the
1536
+ // current poses, replays warm-start, solves velocity (non-penetration
1537
+ // + friction) and position (pseudo-velocity), and integrates the pose
1538
+ // by `h`. Per-substep gravity + per-substep warm-start balance exactly
1539
+ // at a resting contact (each cancels `h` of the other), so stacks hold
1540
+ // at zero velocity and sleep; the position correction adapts as bodies
1541
+ // separate between substeps — the TGS stack-stability mechanism —
1542
+ // without re-running narrowphase. Restitution is applied once after
1543
+ // the loop.
1544
+ const N = this.substeps;
1545
+ const h = dt / N;
1546
+ const count_after_wake = storage.awake_count;
1547
+
1548
+ // CCD: capture start-of-step positions for flagged bodies over the
1549
+ // post-wake awake set (poses are unchanged until the substep loop below
1550
+ // integrates them). The CCD pass after the solver sweeps from here to
1551
+ // each body's final pose. Reads the primary collider's transform so the
1552
+ // start matches the end the resolve pass reads. Zero-cost when no body
1553
+ // is flagged.
1554
+ const ccd_on = this.ccdEnabled;
1555
+
1556
+ if (ccd_on) {
1557
+
1558
+ const ccd_need = storage.high_water_mark * 3;
1559
+
1560
+ if (this.__ccd_start_pos.length < ccd_need) {
1561
+ this.__ccd_start_pos = new Float64Array(ccd_need);
1562
+ }
1563
+
1564
+ const ccd_start = this.__ccd_start_pos;
1565
+
1566
+ for (let i = 0; i < count_after_wake; i++) {
1567
+ const idx = storage.awake_at(i);
1568
+ const rb = bodies[idx];
1569
+
1570
+ if (rb.kind !== BodyKind.Dynamic) {
1571
+ continue;
1572
+ }
1573
+ if ((rb.flags & RigidBodyFlags.CCD) === 0) {
1574
+ continue;
1575
+ }
1576
+
1577
+ const list = this.__body_collider_lists[idx];
1578
+
1579
+ if (list === undefined || list.length === 0) {
1580
+ continue;
1581
+ }
1582
+
1583
+ const cp = list[0].transform.position;
1584
+ const cb = idx * 3;
1585
+
1586
+ ccd_start[cb] = cp[0];
1587
+ ccd_start[cb + 1] = cp[1];
1588
+ ccd_start[cb + 2] = cp[2];
1589
+ }
1590
+ }
1591
+
1592
+ // Size the pseudo-velocity buffer ONCE (it may reallocate on growth),
1593
+ // then capture the reference. Inside the loop we only zero its live
1594
+ // region per substep — re-capturing is unnecessary since it won't
1595
+ // reallocate again this step.
1596
+ this.__reset_pseudo_velocity();
1597
+ const pseudoVel = this.__pseudo_velocity;
1598
+ const pseudo_len = storage.high_water_mark * 6;
1599
+
1600
+ // Gather the data-oriented solver state for every body the substep loop
1601
+ // will touch: the post-wake awake set (all dynamics, at their post-force
1602
+ // velocity) plus the static / kinematic anchors and jointed partners
1603
+ // referenced by this step's contacts and joints. From here the substep
1604
+ // loop reads / writes velocity + orientation through `ss_data` with no
1605
+ // component-object dereference; persistent velocity is scattered back
1606
+ // onto the RigidBodies after the solve.
1607
+ const solver_state = this.__solver_state;
1608
+ solver_state.begin(storage.high_water_mark);
1609
+
1610
+ for (let i = 0; i < count_after_wake; i++) {
1611
+ const idx = storage.awake_at(i);
1612
+ solver_state.gather(idx, bodies[idx], transforms[idx]);
1613
+ }
1614
+
1615
+ const island_contacts = this.islands.contact_data;
1616
+ const island_contact_total = this.islands.contact_offsets[this.islands.island_count];
1617
+
1618
+ for (let i = 0; i < island_contact_total; i++) {
1619
+ const slot = island_contacts[i];
1620
+
1621
+ const ia = body_id_index(manifolds.bodyA(slot));
1622
+ const ib = body_id_index(manifolds.bodyB(slot));
1623
+
1624
+ solver_state.gather(ia, bodies[ia], transforms[ia]);
1625
+ solver_state.gather(ib, bodies[ib], transforms[ib]);
1626
+ }
1627
+
1628
+ const joint_count = joints.length;
1629
+ for (let i = 0; i < joint_count; i++) {
1630
+ const joint = joints[i];
1631
+
1632
+ if (joint === undefined || joint === null) {
1633
+ continue;
1634
+ }
1635
+
1636
+ if (!storage.is_valid(joint._bodyIdA)) {
1637
+ continue;
1638
+ }
1639
+
1640
+ const ia = body_id_index(joint._bodyIdA);
1641
+
1642
+ solver_state.gather(ia, bodies[ia], transforms[ia]);
1643
+
1644
+ if (joint._bodyIdB !== JOINT_WORLD && storage.is_valid(joint._bodyIdB)) {
1645
+
1646
+ const ib = body_id_index(joint._bodyIdB);
1647
+
1648
+ solver_state.gather(ib, bodies[ib], transforms[ib]);
1649
+
1650
+ }
1651
+ }
1652
+
1653
+ const ss_data = solver_state.data;
1654
+
1655
+ prepare_contacts(manifolds, this, h);
1656
+
1657
+ for (let s = 0; s < N; s++) {
1658
+
1659
+ // Gravity (+ damping) for this substep.
1660
+ for (let i = 0; i < count_after_wake; i++) {
1661
+ const idx = storage.awake_at(i);
1662
+ integrate_velocity_gravity(ss_data, idx * SBS_STRIDE, bodies[idx], gx, gy, gz, h);
1663
+ }
1664
+
1665
+ // Re-derive contact geometry at the current poses: concave pairs
1666
+ // re-run narrowphase (fresh feature/normal as the body rocks),
1667
+ // convex pairs rotate frozen anchors analytically. Then replay
1668
+ // the per-substep warm-start and solve velocity.
1669
+ redetect_concave_contacts(manifolds, this);
1670
+ refresh_contacts(manifolds, this.__transforms);
1671
+ warm_start_contacts(manifolds, this);
1672
+ solve_velocity(manifolds, this, this.velocityIterations);
1673
+
1674
+ // Joints share the substep: warm-start + velocity-solve the 6-DOF
1675
+ // constraints on real velocity, coupled with the contacts above
1676
+ // (a body touched by both sees one substep of Gauss-Seidel across
1677
+ // contacts then joints). Position correction for locked DOFs is a
1678
+ // SPOOK bias inside this solve, so no separate joint position pass.
1679
+ if (joints.length > 0) {
1680
+ // Alternate the joint sweep direction on odd substeps so the chain
1681
+ // is solved root→tip and tip→root across the step (symmetric
1682
+ // Gauss-Seidel), cancelling the one-direction propagation bias.
1683
+ solve_joints(joints, this, h, this.jointIterations, (s & 1) === 1);
1684
+ }
1685
+
1686
+ // Position correction writes pseudo-velocity (zeroed first so it
1687
+ // is a fresh per-substep correction), folded into the pose by the
1688
+ // position integrate and then discarded.
1689
+ pseudoVel.fill(0, 0, pseudo_len);
1690
+ solve_position(manifolds, this, this.positionIterations);
1691
+
1692
+ for (let i = 0; i < count_after_wake; i++) {
1693
+ const idx = storage.awake_at(i);
1694
+ const rb = bodies[idx];
1695
+ const tr = transforms[idx];
1696
+ const base = idx * 6;
1697
+ integrate_position(ss_data, idx * SBS_STRIDE, rb, tr, h,
1698
+ pseudoVel[base], pseudoVel[base + 1], pseudoVel[base + 2],
1699
+ pseudoVel[base + 3], pseudoVel[base + 4], pseudoVel[base + 5]);
1700
+ }
1701
+ }
1702
+
1703
+ // Stage 8: one-shot restitution, after the substep loop, keyed off
1704
+ // the approach velocity captured at prepare time.
1705
+ apply_restitution(manifolds, this);
1706
+
1707
+ // Scatter the solved persistent linear / angular velocity back onto the
1708
+ // RigidBody components (pose was written through to the Transforms each
1709
+ // substep). After this the bodies are authoritative again for CCD,
1710
+ // interpolation recording, and the sleep test below.
1711
+ solver_state.scatter(bodies);
1712
+
1713
+ // Stage 8.5: continuous collision sweep CCD-flagged fast movers along
1714
+ // their net step translation and stop them at the first blocker, so they
1715
+ // can't tunnel through thin geometry between discrete steps. Runs on the
1716
+ // final post-solve poses, before the sleep test sees the clamped
1717
+ // velocities. No-op when no awake body is flagged.
1718
+ if (ccd_on) {
1719
+ ccd_resolve(this);
1720
+ }
1721
+
1722
+ // Producer: record the post-step authoritative pose of every awake
1723
+ // interpolated body under this step's tick. Before the sleep test, so a
1724
+ // body that settles this step still records its final pose for the last
1725
+ // interpolation interval.
1726
+ this.__interp_record();
1727
+
1728
+ // Stage 9: sleep test.
1729
+ this.__sleep_test(dt);
1730
+
1731
+ // Stage 10: diff manifolds against the previous frame and dispatch
1732
+ // Begin / Stay / End events. MUST run before advance_frame, which
1733
+ // rolls the touched flags.
1734
+ diff_manifolds(manifolds, storage, this.contactEvents);
1735
+ this.__dispatch_contact_events();
1736
+
1737
+ // Stage 11 (end-of-step): roll touched → prev_touched and evict slots
1738
+ // whose pair has not been touched within the grace window.
1739
+ manifolds.advance_frame();
1740
+ }
1741
+ }
1742
+
1743
+ /**
1744
+ * @readonly
1745
+ * @type {boolean}
1746
+ */
1747
+ PhysicsSystem.prototype.isPhysicsSystem = true;
1748
+
1749
+ // Re-export for convenience.
1750
+ export { BodyKind, RigidBodyFlags };