@woosh/meep-engine 2.139.0 → 2.141.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 (199) hide show
  1. package/package.json +1 -1
  2. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts +3 -3
  3. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts.map +1 -1
  4. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js +4 -4
  5. package/src/{engine/physics/broadphase/aabb_transform_oriented.d.ts → core/geom/3d/aabb/aabb3_transform_oriented.d.ts} +2 -2
  6. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -0
  7. package/src/{engine/physics/broadphase/aabb_transform_oriented.js → core/geom/3d/aabb/aabb3_transform_oriented.js} +1 -1
  8. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
  9. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
  10. package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
  11. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts +54 -0
  12. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
  13. package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
  14. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
  15. package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
  16. package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
  17. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
  18. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
  19. package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
  20. package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
  21. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
  22. package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
  23. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  24. package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
  25. package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
  26. package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
  27. package/src/core/geom/3d/shape/Triangle3D.js +318 -0
  28. package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
  29. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
  30. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
  31. package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
  32. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
  33. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
  34. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
  35. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
  36. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
  37. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
  38. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
  39. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
  40. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
  41. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
  42. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
  43. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
  44. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
  45. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
  46. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
  47. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
  48. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
  49. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
  50. package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
  51. package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
  52. package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
  53. package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
  54. package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
  55. package/src/core/geom/vec3/v3_negate_array.js +2 -2
  56. package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
  57. package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
  58. package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
  59. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
  60. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
  61. package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
  62. package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
  63. package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
  64. package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
  65. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
  66. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  67. package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
  68. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
  69. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  70. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
  71. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
  72. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  73. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
  74. package/src/engine/control/first-person/TODO.md +173 -127
  75. package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
  76. package/src/engine/control/first-person/abilities/Slide.js +9 -1
  77. package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
  78. package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
  79. package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
  80. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
  81. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
  82. package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
  83. package/src/engine/physics/BULLET_REVIEW.md +945 -0
  84. package/src/engine/physics/CANNON_REVIEW.md +1300 -0
  85. package/src/engine/physics/JOLT_REVIEW.md +913 -0
  86. package/src/engine/physics/PLAN.md +578 -236
  87. package/src/engine/physics/RAPIER_REVIEW.md +934 -0
  88. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
  89. package/src/engine/physics/REVIEW_002.md +151 -0
  90. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
  91. package/src/engine/physics/constraint/DofMode.d.ts +28 -0
  92. package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
  93. package/src/engine/physics/constraint/DofMode.js +35 -0
  94. package/src/engine/physics/constraint/solve_constraints.d.ts +16 -0
  95. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
  96. package/src/engine/physics/constraint/solve_constraints.js +436 -0
  97. package/src/engine/physics/contact/ManifoldStore.d.ts +83 -10
  98. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  99. package/src/engine/physics/contact/ManifoldStore.js +608 -499
  100. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
  101. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
  102. package/src/engine/physics/ecs/Joint.d.ts +179 -0
  103. package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
  104. package/src/engine/physics/ecs/Joint.js +234 -0
  105. package/src/engine/physics/ecs/PhysicsSystem.d.ts +180 -20
  106. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  107. package/src/engine/physics/ecs/PhysicsSystem.js +1423 -1159
  108. package/src/engine/physics/fluid/FluidField.d.ts +14 -10
  109. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  110. package/src/engine/physics/fluid/FluidField.js +14 -10
  111. package/src/engine/physics/fluid/FluidSimulator.js +1 -1
  112. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
  113. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
  114. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
  115. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
  116. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
  117. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
  118. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
  119. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
  120. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
  121. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +30 -6
  122. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  123. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +44 -18
  124. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
  125. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
  126. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
  127. package/src/engine/physics/gjk/gjk.d.ts +28 -2
  128. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  129. package/src/engine/physics/gjk/gjk.js +421 -378
  130. package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
  131. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
  132. package/src/engine/physics/gjk/minkowski_support.js +75 -0
  133. package/src/engine/physics/gjk/mpr.d.ts +56 -0
  134. package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
  135. package/src/engine/physics/gjk/mpr.js +344 -0
  136. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
  137. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  138. package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
  139. package/src/engine/physics/integration/integrate_position.d.ts +25 -7
  140. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  141. package/src/engine/physics/integration/integrate_position.js +43 -12
  142. package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
  143. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  144. package/src/engine/physics/integration/integrate_velocity.js +82 -1
  145. package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
  146. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  147. package/src/engine/physics/island/IslandBuilder.js +33 -16
  148. package/src/engine/physics/narrowphase/PosedShape.d.ts +0 -8
  149. package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
  150. package/src/engine/physics/narrowphase/PosedShape.js +28 -30
  151. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  152. package/src/engine/physics/narrowphase/box_box_manifold.js +140 -18
  153. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
  154. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
  155. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
  156. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  157. package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
  158. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
  159. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
  160. package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
  161. package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
  162. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
  163. package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
  164. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
  165. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
  166. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
  167. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
  168. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
  169. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
  170. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
  171. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
  172. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
  173. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
  174. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
  175. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
  176. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
  177. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
  178. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
  179. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +41 -2
  180. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  181. package/src/engine/physics/narrowphase/narrowphase_step.js +1497 -382
  182. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  183. package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
  184. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
  185. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
  186. package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
  187. package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
  188. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
  189. package/src/engine/physics/queries/overlap_shape.js +183 -0
  190. package/src/engine/physics/queries/shape_cast.d.ts +56 -0
  191. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
  192. package/src/engine/physics/queries/shape_cast.js +387 -0
  193. package/src/engine/physics/solver/solve_contacts.d.ts +146 -32
  194. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  195. package/src/engine/physics/solver/solve_contacts.js +809 -223
  196. package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
  197. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
  198. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
  199. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
@@ -1,1159 +1,1423 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
3
- import Signal from "../../../core/events/signal/Signal.js";
4
- import Vector3 from "../../../core/geom/Vector3.js";
5
- import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
6
- import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
7
- import { System } from "../../ecs/System.js";
8
- import { Transform } from "../../ecs/transform/Transform.js";
9
- import { body_id_index, BodyStorage } from "../body/BodyStorage.js";
10
- import { aabb_transform_oriented } from "../broadphase/aabb_transform_oriented.js";
11
- import { compute_fat_world_aabb } from "../broadphase/compute_fat_world_aabb.js";
12
- import { generate_pairs } from "../broadphase/generate_pairs.js";
13
- import { PairList } from "../broadphase/PairList.js";
14
- import { CONTACT_STRIDE, ManifoldStore } from "../contact/ManifoldStore.js";
15
- import { ContactEventBuffer, ContactEventKind } from "../events/ContactEventBuffer.js";
16
- import { diff_manifolds } from "../events/diff_manifolds.js";
17
- import { integrate_position } from "../integration/integrate_position.js";
18
- import { integrate_velocity } from "../integration/integrate_velocity.js";
19
- import { IslandBuilder } from "../island/IslandBuilder.js";
20
- import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
21
- import { raycast as raycast_query } from "../queries/raycast.js";
22
- import { returnTrue } from "../../../core/function/returnTrue.js";
23
- import { solve_contacts } from "../solver/solve_contacts.js";
24
- import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
25
- import { PhysicsEvents } from "./PhysicsEvents.js";
26
-
27
- /**
28
- * Scratch for {@link applyImpulseAt}'s angular delta calculation.
29
- * @type {Float64Array}
30
- */
31
- const scratch_angular_delta = new Float64Array(3);
32
- import { BodyKind } from "./BodyKind.js";
33
- import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
34
- import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
35
- import { RigidBodyFlags } from "./RigidBodyFlags.js";
36
- import { SleepState } from "./SleepState.js";
37
-
38
- /**
39
- * Reusable scratch buffer for world-AABB construction so the link path is
40
- * allocation-free in steady state.
41
- * @type {Float64Array}
42
- */
43
- const scratch_world_aabb = new Float64Array(6);
44
-
45
- /**
46
- * Reusable scratch buffer for the local AABB returned by
47
- * {@link AbstractShape3D#compute_bounding_box}.
48
- * @type {Float64Array}
49
- */
50
- const scratch_local_aabb = new Float64Array(6);
51
-
52
- /**
53
- * Rigid-body physics system.
54
- *
55
- * v1 scope: pool + active list + two BVHs (static / dynamic), Transform sync
56
- * contract, and the user-facing API surface (gravity, force / impulse, layer
57
- * filtering, wake / sleep, contact filter). The per-step simulation pipeline
58
- * (broadphase pair generation, narrowphase, solver, islands, sleep test,
59
- * contact-event emission) is built on top of this skeleton in subsequent
60
- * iterations.
61
- *
62
- * Dependency tuple is `(RigidBody, Collider, Transform)` — every body has
63
- * exactly one collider on the same entity. Compound bodies via child collider
64
- * entities are an extension point handled by a follow-up sub-observer.
65
- *
66
- * @author Alex Goldring
67
- * @copyright Company Named Limited (c) 2026
68
- */
69
- export class PhysicsSystem extends System {
70
-
71
- constructor() {
72
- super();
73
-
74
- this.dependencies = [RigidBody, Transform];
75
-
76
- this.components_used = [
77
- ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Read | ResourceAccessKind.Write),
78
- ResourceAccessSpecification.from(Collider, ResourceAccessKind.Read),
79
- ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read | ResourceAccessKind.Write),
80
- ];
81
-
82
- /**
83
- * @type {BodyStorage}
84
- */
85
- this.storage = new BodyStorage();
86
-
87
- /**
88
- * @type {BVH}
89
- */
90
- this.staticBvh = new BVH();
91
-
92
- /**
93
- * @type {BVH}
94
- */
95
- this.dynamicBvh = new BVH();
96
-
97
- /**
98
- * Persistent contact-manifold cache. One slot per active pair.
99
- * @type {ManifoldStore}
100
- */
101
- this.manifolds = new ManifoldStore();
102
-
103
- /**
104
- * Per-frame list of broadphase-overlapping pairs (canonical
105
- * `(min, max)`). Cleared at the top of each step.
106
- * @type {PairList}
107
- */
108
- this.pairs = new PairList();
109
-
110
- /**
111
- * Per-frame contact-event buffer. Populated by the manifold diff
112
- * pass at end-of-step and consumed by the dispatch pass.
113
- * @type {ContactEventBuffer}
114
- */
115
- this.contactEvents = new ContactEventBuffer();
116
-
117
- /**
118
- * Per-frame island partitioning of the awake-body + contact graph.
119
- * Rebuilt after narrowphase, consumed by the solver (and, in the
120
- * follow-up slice, by the per-island atomic sleep test). Bodies
121
- * static and kinematic act as constraint anchors and do not enlarge
122
- * islands.
123
- * @type {IslandBuilder}
124
- */
125
- this.islands = new IslandBuilder();
126
-
127
- /**
128
- * Velocity-squared threshold below which a body is eligible to start
129
- * accumulating sleep time. Combined linear + angular kinetic-ish
130
- * metric: `vx²+vy²+vz² + ωx²+ωy²+ωz²`. Default 0.01 corresponds to
131
- * ~0.1 m/s linear or ~0.1 rad/s angular.
132
- * @type {number}
133
- */
134
- this.sleepVelocitySqrThreshold = 0.01;
135
-
136
- /**
137
- * Number of TGS substeps per fixed tick. Each substep does
138
- * `integrate_velocity → solve_contacts → integrate_position` with
139
- * `dt / substeps` as its time step; narrowphase + island building
140
- * run once per tick before the substep loop. Higher values give
141
- * better convergence on tall stacks (impulses propagate through
142
- * deeper contact chains thanks to per-substep position
143
- * integration) at proportionally higher solver cost.
144
- *
145
- * Box2D's default is 4; we match. Set to 1 to fall back to
146
- * classic PGS behaviour (one solve per tick at full `dt`).
147
- * @type {number}
148
- */
149
- this.solverSubsteps = 4;
150
-
151
- /**
152
- * Velocity iterations within each TGS substep. With 4 substeps,
153
- * the total iterations per tick is `solverSubsteps × this` = 8 —
154
- * comparable to the classic PGS default of 10, but distributed
155
- * across substeps with position integration between them, which
156
- * is dramatically more stable on stacks.
157
- * @type {number}
158
- */
159
- this.solverItersPerSubstep = 2;
160
-
161
- /**
162
- * Seconds of below-threshold motion before a body is moved to the
163
- * sleeping set. Box2D default is 0.5 s.
164
- * @type {number}
165
- */
166
- this.sleepTimeThreshold = 0.5;
167
-
168
- /**
169
- * Reusable contact-event payload. Listeners must copy any fields they
170
- * intend to retain past their own scope. Reset before each dispatch.
171
- * @private
172
- */
173
- this.__contact_payload = {
174
- entityA: -1,
175
- entityB: -1,
176
- kind: 0,
177
- depth: 0,
178
- normal: new Vector3(),
179
- point: new Vector3(),
180
- };
181
-
182
- /**
183
- * World gravity, m/s². Applied each step scaled by per-body gravityScale.
184
- * @readonly
185
- * @type {Vector3}
186
- */
187
- this.gravity = new Vector3(0, -9.81, 0);
188
-
189
- /**
190
- * Emitted at end-of-step for newly established contact pairs.
191
- * @readonly
192
- * @type {Signal}
193
- */
194
- this.onContactBegin = new Signal();
195
-
196
- /**
197
- * Emitted at end-of-step for contact pairs that persisted from the previous step.
198
- * @readonly
199
- * @type {Signal}
200
- */
201
- this.onContactStay = new Signal();
202
-
203
- /**
204
- * Emitted at end-of-step for contact pairs that disappeared.
205
- * @readonly
206
- * @type {Signal}
207
- */
208
- this.onContactEnd = new Signal();
209
-
210
- /**
211
- * Optional global contact filter. Called for each surviving broadphase
212
- * pair; returning `false` discards the pair.
213
- * @type {((entityA: number, entityB: number, colliderA: Collider, colliderB: Collider) => boolean) | null}
214
- * @private
215
- */
216
- this.__contact_filter = null;
217
-
218
- /**
219
- * Per-body component side-tables indexed by body index (NOT packed id).
220
- * Sparse arrays — populated at link, cleared at unlink. The hot loop
221
- * iterates `storage.awake_at(i)` and dereferences these.
222
- * @type {RigidBody[]}
223
- */
224
- this.__bodies = [];
225
- /** @type {Transform[]} */
226
- this.__transforms = [];
227
-
228
- /**
229
- * Per-body list of attached colliders. Each entry stores the
230
- * Collider component, its world Transform, the entity that owns
231
- * it (the body entity for same-entity attachments, a child entity
232
- * for compound bodies), and the BVH leaf id assigned at attach
233
- * time. A body may have zero or more attached colliders.
234
- *
235
- * @type {Array<Array<{collider: Collider, transform: Transform, entity: number, bvhNode: number}>>}
236
- */
237
- this.__body_collider_lists = [];
238
-
239
- /**
240
- * Bound reference to {@link __pair_filter} so we hand the same
241
- * callable to {@link generate_pairs} each step without per-step
242
- * allocation.
243
- * @private
244
- */
245
- this.__pair_filter_bound = (idA, idB) => this.__pair_filter(idA, idB);
246
- }
247
-
248
- /**
249
- * Symmetric layer/mask check + optional user callback. Called per
250
- * candidate pair during broadphase. Returns `true` to accept the pair.
251
- *
252
- * Layer/mask rule: pair (A, B) collides iff
253
- * `(A.layer & B.mask) !== 0 && (B.layer & A.mask) !== 0`.
254
- * The user's contact-filter callback is consulted only for pairs that
255
- * pass the bitmask test (cheap gate first).
256
- *
257
- * @private
258
- * @param {number} idA packed body id
259
- * @param {number} idB packed body id
260
- * @returns {boolean}
261
- */
262
- __pair_filter(idA, idB) {
263
- const idxA = body_id_index(idA);
264
- const idxB = body_id_index(idB);
265
- const rbA = this.__bodies[idxA];
266
- const rbB = this.__bodies[idxB];
267
- if (rbA === undefined || rbB === undefined) return false;
268
-
269
- // Layer/mask gate (symmetric).
270
- if (((rbA.layer & rbB.mask) | 0) === 0) return false;
271
- if (((rbB.layer & rbA.mask) | 0) === 0) return false;
272
-
273
- // User callback gate, if installed.
274
- const fn = this.__contact_filter;
275
- if (fn !== null) {
276
- const entA = this.storage.entity_at(idxA);
277
- const entB = this.storage.entity_at(idxB);
278
- const colA = this.__primary_collider(idxA);
279
- const colB = this.__primary_collider(idxB);
280
- if (!fn(entA, entB, colA, colB)) return false;
281
- }
282
- return true;
283
- }
284
-
285
- /**
286
- * First attached collider of a body, or `null` if none. Used by the
287
- * solver to read material parameters (friction / restitution) when the
288
- * per-contact source-collider identity isn't tracked yet (v1 limitation
289
- * multi-collider bodies with mixed materials lose precision here).
290
- *
291
- * @private
292
- * @param {number} body_idx
293
- * @returns {Collider|null}
294
- */
295
- __primary_collider(body_idx) {
296
- const list = this.__body_collider_lists[body_idx];
297
- return (list !== undefined && list.length > 0) ? list[0].collider : null;
298
- }
299
-
300
- /**
301
- * Replace the world gravity vector. Effective on the next step.
302
- * @param {Vector3|{x:number,y:number,z:number}} v
303
- */
304
- setGravity(v) {
305
- this.gravity.set(v.x, v.y, v.z);
306
- }
307
-
308
- /**
309
- * Install (or remove with `null`) the contact filter callback.
310
- * @param {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null} fn
311
- */
312
- setContactFilter(fn) {
313
- this.__contact_filter = fn;
314
- }
315
-
316
- /**
317
- * @returns {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null}
318
- */
319
- getContactFilter() {
320
- return this.__contact_filter;
321
- }
322
-
323
- /**
324
- * @private
325
- * @param {RigidBody} rb
326
- * @param {Collider} collider
327
- * @param {Transform} transform
328
- * @returns {number} BVH node id
329
- */
330
- __insert_into_broadphase(rb, collider, transform) {
331
- const shape = collider.shape;
332
-
333
- assert.notNull(shape, 'Collider.shape must be set before attaching');
334
-
335
- shape.compute_bounding_box(scratch_local_aabb);
336
-
337
- const p = transform.position;
338
- const q = transform.rotation;
339
-
340
- aabb_transform_oriented(
341
- scratch_world_aabb, 0,
342
- scratch_local_aabb[0], scratch_local_aabb[1], scratch_local_aabb[2],
343
- scratch_local_aabb[3], scratch_local_aabb[4], scratch_local_aabb[5],
344
- p.x, p.y, p.z,
345
- q.x, q.y, q.z, q.w
346
- );
347
-
348
- const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
349
- const node = bvh.allocate_node();
350
-
351
- bvh.node_set_aabb_primitive(
352
- node,
353
- scratch_world_aabb[0], scratch_world_aabb[1], scratch_world_aabb[2],
354
- scratch_world_aabb[3], scratch_world_aabb[4], scratch_world_aabb[5]
355
- );
356
- bvh.node_set_user_data(node, rb._bodyId);
357
- bvh.insert_leaf(node);
358
-
359
- return node;
360
- }
361
-
362
- /**
363
- * @private
364
- * @param {RigidBody} rb
365
- * @param {number} node
366
- */
367
- __remove_from_broadphase(rb, node) {
368
- const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
369
- bvh.remove_leaf(node);
370
- bvh.release_node(node);
371
- }
372
-
373
- /**
374
- * Lifecycle entry: invoked when an entity gains the (RigidBody, Transform)
375
- * tuple. Allocates the body's slot in storage and seeds an empty collider
376
- * list. Colliders are attached separately via {@link attach_collider} (the
377
- * paired {@link ColliderObserverSystem} drives this from the dataset; in
378
- * tests, call manually).
379
- *
380
- * @param {RigidBody} rigidBody
381
- * @param {Transform} transform
382
- * @param {number} entity
383
- */
384
- link(rigidBody, transform, entity) {
385
- const packed = this.storage.allocate(entity);
386
- rigidBody._bodyId = packed;
387
- rigidBody.sleepState = SleepState.Awake;
388
-
389
- const index = body_id_index(packed);
390
- this.storage.set_kind(index, rigidBody.kind);
391
- this.storage.set_flags(index, rigidBody.flags);
392
-
393
- this.__bodies[index] = rigidBody;
394
- this.__transforms[index] = transform;
395
- this.__body_collider_lists[index] = [];
396
-
397
- // Static bodies do not need to live in the active list — they never move.
398
- if (rigidBody.kind === BodyKind.Static) {
399
- this.storage.mark_sleeping(index);
400
- }
401
- }
402
-
403
- /**
404
- * Detach every collider attached to this body, free its slot, and clear
405
- * the side-tables.
406
- *
407
- * @param {RigidBody} rigidBody
408
- * @param {Transform} transform
409
- * @param {number} entity
410
- */
411
- unlink(rigidBody, transform, entity) {
412
- const packed = rigidBody._bodyId;
413
-
414
- assert.equal(this.storage.is_valid(packed), true, 'unlink: stale or absent body id');
415
-
416
- // If the body is sleeping inside a multi-member sleep group, dissolve
417
- // the group by waking every member first. Otherwise the surviving
418
- // members would hold dangling indices in their sleep-group chain
419
- // pointing at a slot that has just been freed.
420
- if (rigidBody.sleep_group_next !== -1) {
421
- this.__wake_body(rigidBody);
422
- }
423
-
424
- const index = body_id_index(packed);
425
- const list = this.__body_collider_lists[index];
426
- if (list !== undefined) {
427
- for (let i = 0; i < list.length; i++) {
428
- const entry = list[i];
429
- if (entry.bvhNode !== COLLIDER_UNBOUND) {
430
- this.__remove_from_broadphase(rigidBody, entry.bvhNode);
431
- }
432
- entry.collider._bvhNode = COLLIDER_UNBOUND;
433
- entry.collider._bodyId = -1;
434
- }
435
- }
436
-
437
- this.__bodies[index] = undefined;
438
- this.__transforms[index] = undefined;
439
- this.__body_collider_lists[index] = undefined;
440
-
441
- this.storage.free(packed);
442
- rigidBody._bodyId = RIGID_BODY_UNALLOCATED;
443
- }
444
-
445
- /**
446
- * Attach a collider to an existing body. The collider can live on the
447
- * same entity as the body (single-collider body) or on a child entity
448
- * (compound body). The world transform passed here is the collider's
449
- * own — for a same-entity collider it is the body's Transform; for a
450
- * child collider it is the child entity's Transform.
451
- *
452
- * Idempotent re-attaching the same collider is a no-op.
453
- *
454
- * @param {number} body_entity entity that owns the body
455
- * @param {Collider} collider
456
- * @param {Transform} transform world transform of the collider
457
- * @param {number} [collider_entity] entity owning the collider (defaults to body_entity)
458
- */
459
- attach_collider(body_entity, collider, transform, collider_entity = body_entity) {
460
- // Find the body by walking the storage entity table. The body must
461
- // have been allocated via `link` before any colliders are attached.
462
- const body_index = this.__find_body_index_by_entity(body_entity);
463
- assert.notEqual(body_index, -1, `attach_collider: no body found for entity ${body_entity}`);
464
-
465
- const rb = this.__bodies[body_index];
466
- // Idempotent: skip if collider already attached.
467
- if (collider._bvhNode !== COLLIDER_UNBOUND) return;
468
-
469
- const node = this.__insert_into_broadphase(rb, collider, transform);
470
-
471
- collider._bvhNode = node;
472
- collider._bodyId = rb._bodyId;
473
- this.__body_collider_lists[body_index].push({
474
- collider, transform, entity: collider_entity, bvhNode: node,
475
- });
476
- }
477
-
478
- /**
479
- * Reverse of {@link attach_collider}. Idempotent.
480
- *
481
- * @param {number} body_entity
482
- * @param {Collider} collider
483
- */
484
- detach_collider(body_entity, collider) {
485
- if (collider._bvhNode === COLLIDER_UNBOUND) return;
486
-
487
- const body_index = this.__find_body_index_by_entity(body_entity);
488
- if (body_index === -1) return;
489
-
490
- const rb = this.__bodies[body_index];
491
- this.__remove_from_broadphase(rb, collider._bvhNode);
492
-
493
- const list = this.__body_collider_lists[body_index];
494
- for (let i = 0; i < list.length; i++) {
495
- if (list[i].collider === collider) {
496
- list.splice(i, 1);
497
- break;
498
- }
499
- }
500
-
501
- collider._bvhNode = COLLIDER_UNBOUND;
502
- collider._bodyId = -1;
503
- }
504
-
505
- /**
506
- * Linear scan over body slots looking for the one whose entity matches.
507
- * O(N) where N is the live body count — only called on the link/unlink
508
- * paths, not during simulation, so the scan cost is bounded.
509
- *
510
- * @private
511
- * @param {number} entity
512
- * @returns {number} body index or -1
513
- */
514
- __find_body_index_by_entity(entity) {
515
- const hwm = this.storage.high_water_mark;
516
- for (let i = 0; i < hwm; i++) {
517
- if (this.storage.entity_at(i) === entity) return i;
518
- }
519
- return -1;
520
- }
521
-
522
- /**
523
- * Resolve a packed body id to its entity, or `-1` if the id is stale.
524
- * @param {number} packed_body_id
525
- * @returns {number}
526
- */
527
- entityOf(packed_body_id) {
528
- if (!this.storage.is_valid(packed_body_id)) return -1;
529
- return this.storage.entity_at(body_id_index(packed_body_id));
530
- }
531
-
532
- /**
533
- * Number of live bodies (regardless of awake/sleeping state).
534
- * @returns {number}
535
- */
536
- get bodyCount() {
537
- return this.storage.size;
538
- }
539
-
540
- /**
541
- * Apply an instantaneous change of momentum at the body's centre of mass.
542
- * Linear-only — see {@link applyImpulseAt} for an off-centre impulse that
543
- * also produces angular response.
544
- *
545
- * Wakes the body if it is asleep.
546
- *
547
- * @param {RigidBody} rigidBody
548
- * @param {Vector3|{x:number,y:number,z:number}} impulse
549
- */
550
- applyImpulse(rigidBody, impulse) {
551
- if (rigidBody.kind !== BodyKind.Dynamic) {
552
- return;
553
- }
554
- const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
555
- rigidBody.linearVelocity.addScaled(impulse, inv_m);
556
-
557
- this.__wake_body(rigidBody);
558
- }
559
-
560
- /**
561
- * Apply an instantaneous change of momentum at a specific world-space point.
562
- * Off-centre impulses produce both linear (Δv = P/m) and angular
563
- * (Δω = I_w⁻¹·(r × P)) response.
564
- *
565
- * Wakes the body if it is asleep.
566
- *
567
- * @param {RigidBody} rigidBody
568
- * @param {Transform} transform body's current world Transform (used for r and I_w)
569
- * @param {Vector3|{x:number,y:number,z:number}} impulse
570
- * @param {Vector3|{x:number,y:number,z:number}} worldPoint
571
- */
572
- applyImpulseAt(rigidBody, transform, impulse, worldPoint) {
573
- if (rigidBody.kind !== BodyKind.Dynamic) {
574
- return;
575
- }
576
- const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
577
-
578
- rigidBody.linearVelocity.set(
579
- rigidBody.linearVelocity.x + impulse.x * inv_m,
580
- rigidBody.linearVelocity.y + impulse.y * inv_m,
581
- rigidBody.linearVelocity.z + impulse.z * inv_m
582
- );
583
-
584
- const rx = worldPoint.x - transform.position.x;
585
- const ry = worldPoint.y - transform.position.y;
586
- const rz = worldPoint.z - transform.position.z;
587
-
588
- // Δω = I_w⁻¹ · (r × P)
589
- const tx = ry * impulse.z - rz * impulse.y;
590
- const ty = rz * impulse.x - rx * impulse.z;
591
- const tz = rx * impulse.y - ry * impulse.x;
592
-
593
- world_inverse_inertia_apply(scratch_angular_delta, 0, rigidBody, transform, tx, ty, tz);
594
-
595
- rigidBody.angularVelocity.set(
596
- rigidBody.angularVelocity.x + scratch_angular_delta[0],
597
- rigidBody.angularVelocity.y + scratch_angular_delta[1],
598
- rigidBody.angularVelocity.z + scratch_angular_delta[2]
599
- );
600
-
601
- this.__wake_body(rigidBody);
602
- }
603
-
604
- /**
605
- * Accumulate a continuous torque (world-space) for integration on the
606
- * next fixedUpdate. Pairs with {@link applyForce} for the rotational case.
607
- *
608
- * Wakes the body if asleep.
609
- *
610
- * @param {RigidBody} rigidBody
611
- * @param {Vector3|{x:number,y:number,z:number}} torque
612
- */
613
- applyTorque(rigidBody, torque) {
614
- if (rigidBody.kind !== BodyKind.Dynamic) {
615
- return;
616
- }
617
- rigidBody.accumulatedTorque.set(
618
- rigidBody.accumulatedTorque.x + torque.x,
619
- rigidBody.accumulatedTorque.y + torque.y,
620
- rigidBody.accumulatedTorque.z + torque.z
621
- );
622
- this.__wake_body(rigidBody);
623
- }
624
-
625
- /**
626
- * Apply a continuous force at a specific world-space point. The force
627
- * generates both a linear acceleration (F/m) and a torque (r × F) about
628
- * the body's centre of mass.
629
- *
630
- * Wakes the body if asleep.
631
- *
632
- * @param {RigidBody} rigidBody
633
- * @param {Transform} transform body's current world Transform
634
- * @param {Vector3|{x:number,y:number,z:number}} force
635
- * @param {Vector3|{x:number,y:number,z:number}} worldPoint
636
- */
637
- applyForceAt(rigidBody, transform, force, worldPoint) {
638
- if (rigidBody.kind !== BodyKind.Dynamic) {
639
- return;
640
- }
641
- rigidBody.accumulatedForce.set(
642
- rigidBody.accumulatedForce.x + force.x,
643
- rigidBody.accumulatedForce.y + force.y,
644
- rigidBody.accumulatedForce.z + force.z
645
- );
646
-
647
- const rx = worldPoint.x - transform.position.x;
648
- const ry = worldPoint.y - transform.position.y;
649
- const rz = worldPoint.z - transform.position.z;
650
-
651
- rigidBody.accumulatedTorque.set(
652
- rigidBody.accumulatedTorque.x + (ry * force.z - rz * force.y),
653
- rigidBody.accumulatedTorque.y + (rz * force.x - rx * force.z),
654
- rigidBody.accumulatedTorque.z + (rx * force.y - ry * force.x)
655
- );
656
-
657
- this.__wake_body(rigidBody);
658
- }
659
-
660
- /**
661
- * Accumulate a continuous force to be integrated next fixedUpdate step.
662
- * Wakes the body if asleep.
663
- *
664
- * @param {RigidBody} rigidBody
665
- * @param {Vector3|{x:number,y:number,z:number}} force
666
- */
667
- applyForce(rigidBody, force) {
668
- if (rigidBody.kind !== BodyKind.Dynamic) {
669
- return;
670
- }
671
- rigidBody.accumulatedForce.add( force);
672
- this.__wake_body(rigidBody);
673
- }
674
-
675
- /**
676
- * Replace the linear velocity. Wakes the body if asleep.
677
- *
678
- * @param {RigidBody} rigidBody
679
- * @param {Vector3|{x:number,y:number,z:number}} v
680
- */
681
- setLinearVelocity(rigidBody, v) {
682
- rigidBody.linearVelocity.copy(v);
683
- if (rigidBody.kind === BodyKind.Dynamic) {
684
- this.__wake_body(rigidBody);
685
- }
686
- }
687
-
688
- /**
689
- * Force the body awake. Static bodies are ignored.
690
- * @param {RigidBody} rigidBody
691
- */
692
- wake(rigidBody) {
693
- this.__wake_body(rigidBody);
694
- }
695
-
696
- /**
697
- * Force the body asleep. Dynamic bodies will not re-enter the active list
698
- * until a wake event occurs.
699
- * @param {RigidBody} rigidBody
700
- */
701
- sleep(rigidBody) {
702
- if (rigidBody.kind !== BodyKind.Dynamic) {
703
- return;
704
- }
705
- if (rigidBody.sleepState === SleepState.Sleeping) {
706
- return;
707
- }
708
- rigidBody.sleepState = SleepState.Sleeping;
709
- const index = body_id_index(rigidBody._bodyId);
710
- this.storage.mark_sleeping(index);
711
- }
712
-
713
- /**
714
- * Wake a body and atomically wake every other body it was last sleeping
715
- * with (its "sleep group"). Sleep groups are circular doubly-linked lists
716
- * threaded through every member of an island when it sleeps atomically;
717
- * waking any one member walks the chain and wakes the rest in the same
718
- * call.
719
- *
720
- * Without this, a 100-block stack hit at the base would wake one block
721
- * per frame as the broadphase propagated awareness up the stack — a
722
- * visible ~1.6 s wave at 60 fps. Atomic wake eliminates the wave.
723
- *
724
- * No-op for non-dynamic bodies. Idempotent for already-awake bodies.
725
- * @private
726
- * @param {RigidBody} rb
727
- */
728
- __wake_body(rb) {
729
- if (rb.kind !== BodyKind.Dynamic) return;
730
- if (rb.sleepState === SleepState.Awake) return;
731
- const index = body_id_index(rb._bodyId);
732
-
733
- // Remember the next-in-chain before clearing the body's own pointers;
734
- // the rest of the group is reached by walking forward from there.
735
- const start_next = rb.sleep_group_next;
736
-
737
- rb.sleepState = SleepState.Awake;
738
- rb.sleep_timer = 0;
739
- rb.sleep_group_next = -1;
740
- rb.sleep_group_prev = -1;
741
- this.storage.mark_awake(index);
742
-
743
- if (start_next === -1 || start_next === index) return;
744
-
745
- // Walk the (now-broken) chain forward until we loop back. The chain
746
- // is circular so we know when to stop; defensive `-1` guards against
747
- // corruption from a body being unlinked mid-sleep-group.
748
- let cur = start_next;
749
- while (cur !== -1 && cur !== index) {
750
- const cur_rb = this.__bodies[cur];
751
- if (cur_rb === undefined) break;
752
- const nxt = cur_rb.sleep_group_next;
753
- cur_rb.sleepState = SleepState.Awake;
754
- cur_rb.sleep_timer = 0;
755
- cur_rb.sleep_group_next = -1;
756
- cur_rb.sleep_group_prev = -1;
757
- this.storage.mark_awake(cur);
758
- cur = nxt;
759
- }
760
- }
761
-
762
- /**
763
- * Atomically put every body in a contiguous range of island members to
764
- * sleep. Members are threaded into a circular doubly-linked list so any
765
- * future `wake` on any member walks the chain and revives them all.
766
- *
767
- * Velocities are zeroed because the body is by definition at rest at
768
- * this point — the alternative (storing residual velocities for "softer"
769
- * wake) is what Bullet does, but for a deterministic game-physics target
770
- * fully resetting is simpler and avoids drift while sleeping.
771
- *
772
- * @private
773
- * @param {Uint32Array} member_array view (or full array) of body indices
774
- * @param {number} start
775
- * @param {number} end
776
- */
777
- __atomic_sleep_island_range(member_array, start, end) {
778
- const count = end - start;
779
- if (count === 0) return;
780
- const bodies = this.__bodies;
781
- const storage = this.storage;
782
-
783
- if (count === 1) {
784
- const idx = member_array[start];
785
- const rb = bodies[idx];
786
- if (rb === undefined) return;
787
- rb.sleep_group_next = -1;
788
- rb.sleep_group_prev = -1;
789
- rb.sleepState = SleepState.Sleeping;
790
- rb.linearVelocity[0] = 0; rb.linearVelocity[1] = 0; rb.linearVelocity[2] = 0;
791
- rb.angularVelocity[0] = 0; rb.angularVelocity[1] = 0; rb.angularVelocity[2] = 0;
792
- storage.mark_sleeping(idx);
793
- return;
794
- }
795
-
796
- for (let i = 0; i < count; i++) {
797
- const idx = member_array[start + i];
798
- const rb = bodies[idx];
799
- if (rb === undefined) continue;
800
- const next_idx = member_array[start + ((i + 1) % count)];
801
- const prev_idx = member_array[start + ((i - 1 + count) % count)];
802
- rb.sleep_group_next = next_idx;
803
- rb.sleep_group_prev = prev_idx;
804
- rb.sleepState = SleepState.Sleeping;
805
- rb.linearVelocity[0] = 0; rb.linearVelocity[1] = 0; rb.linearVelocity[2] = 0;
806
- rb.angularVelocity[0] = 0; rb.angularVelocity[1] = 0; rb.angularVelocity[2] = 0;
807
- storage.mark_sleeping(idx);
808
- }
809
- }
810
-
811
- /**
812
- * Get the body index for a packed body id without revalidation. Used by
813
- * query traversals that already trust the id came from a live BVH leaf.
814
- * @param {number} packed_body_id
815
- * @returns {number}
816
- */
817
- __index_of(packed_body_id) {
818
- return body_id_index(packed_body_id);
819
- }
820
-
821
- /**
822
- * Broadphase raycast against both BVHs. Fills `result` with the nearest
823
- * hit and returns `true` on hit, `false` on miss.
824
- *
825
- * Narrowphase refinement against the actual shape geometry is a
826
- * follow-up — for now `result.t` is the distance to the leaf's
827
- * inflated AABB and `result.normal` is the AABB face normal. Both are
828
- * exact for AABB-shaped colliders.
829
- *
830
- * @param {Ray3} ray origin + unit direction + `tMax`
831
- * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
832
- * @param {(entity:number, collider:Collider)=>boolean} [filter] defaults
833
- * to {@link returnTrue} (accept every candidate)
834
- * @returns {boolean}
835
- */
836
- raycast(ray, result, filter = returnTrue) {
837
- return raycast_query(this, ray, result, filter);
838
- }
839
-
840
- /**
841
- * Run one simulation step. v1 pipeline: integrate velocity integrate
842
- * position refit broadphase AABBs. Narrowphase, solver, islands and
843
- * contact-event emission land in subsequent slices.
844
- *
845
- * @param {number} dt
846
- */
847
- /**
848
- * Wake any sleeping body that appears in this step's broadphase pair list.
849
- * A pair means the BVH AABBs overlap — even if the sleeper hasn't moved,
850
- * an awake neighbour has come into contact range and the sleeper must
851
- * participate in narrowphase / solve.
852
- * @private
853
- */
854
- __wake_pairs() {
855
- const list = this.pairs;
856
- const n = list.count;
857
- const bodies = this.__bodies;
858
- for (let i = 0; i < n; i++) {
859
- const idA = list.get_a(i);
860
- const idB = list.get_b(i);
861
- const idxA = body_id_index(idA);
862
- const idxB = body_id_index(idB);
863
- const a = bodies[idxA];
864
- const b = bodies[idxB];
865
- if (a !== undefined && a.sleepState === SleepState.Sleeping) {
866
- this.__wake_body(a);
867
- }
868
- if (b !== undefined && b.sleepState === SleepState.Sleeping) {
869
- this.__wake_body(b);
870
- }
871
- }
872
- }
873
-
874
- /**
875
- * Per-island atomic sleep test. Walks each island once and applies the
876
- * decision uniformly across all members:
877
- *
878
- * - If any member carries {@link RigidBodyFlags.DisableSleep}, the
879
- * entire island is exempt; every member's sleep_timer is reset.
880
- * - If `max(|v|² + |ω|²)` across all members is below
881
- * {@link sleepVelocitySqrThreshold}, every member's sleep_timer is
882
- * incremented by `dt`. When the smallest member's timer crosses
883
- * {@link sleepTimeThreshold}, the whole island sleeps atomically in
884
- * the same step (members get threaded into a sleep-group chain so
885
- * {@link __wake_body} can wake them all in one call).
886
- * - Otherwise the island has at least one active member, so every
887
- * member's timer is reset.
888
- *
889
- * This is the design-plan atomic-island sleep replaces the per-body
890
- * approximation that lived in this slot during the previous slice.
891
- * Weakly-connected piles no longer chatter awake when a single member
892
- * blips above threshold; piles fall asleep and wake up as one.
893
- *
894
- * @private
895
- * @param {number} dt
896
- */
897
- __sleep_test(dt) {
898
- const threshold_sqr = this.sleepVelocitySqrThreshold;
899
- const time_threshold = this.sleepTimeThreshold;
900
- const bodies = this.__bodies;
901
- const islands = this.islands;
902
- const island_count = islands.island_count;
903
- const body_offsets = islands.body_offsets;
904
- const body_data = islands.body_data;
905
-
906
- for (let isl = 0; isl < island_count; isl++) {
907
- const start = body_offsets[isl];
908
- const end = body_offsets[isl + 1];
909
- if (end === start) continue;
910
-
911
- // Pass 1: find max v² + check DisableSleep across the island.
912
- let max_v_sqr = 0;
913
- let any_disable_sleep = false;
914
- for (let i = start; i < end; i++) {
915
- const idx = body_data[i];
916
- const rb = bodies[idx];
917
- if (rb === undefined) continue;
918
- if ((rb.flags & RigidBodyFlags.DisableSleep) !== 0) {
919
- any_disable_sleep = true;
920
- break;
921
- }
922
- const lv = rb.linearVelocity;
923
- const av = rb.angularVelocity;
924
- const v_sqr = lv[0] * lv[0] + lv[1] * lv[1] + lv[2] * lv[2]
925
- + av[0] * av[0] + av[1] * av[1] + av[2] * av[2];
926
- if (v_sqr > max_v_sqr) max_v_sqr = v_sqr;
927
- }
928
-
929
- if (any_disable_sleep) {
930
- // Whole island is exempt reset every member's timer.
931
- for (let i = start; i < end; i++) {
932
- const rb = bodies[body_data[i]];
933
- if (rb !== undefined) rb.sleep_timer = 0;
934
- }
935
- continue;
936
- }
937
-
938
- if (max_v_sqr < threshold_sqr) {
939
- // Island is at rest — increment every member's timer; if the
940
- // slowest-stabilising member has crossed the time threshold,
941
- // every member has (they were incremented together this step),
942
- // so atomic-sleep the island.
943
- let min_timer = Infinity;
944
- for (let i = start; i < end; i++) {
945
- const rb = bodies[body_data[i]];
946
- if (rb === undefined) continue;
947
- rb.sleep_timer += dt;
948
- if (rb.sleep_timer < min_timer) min_timer = rb.sleep_timer;
949
- }
950
- if (min_timer >= time_threshold) {
951
- this.__atomic_sleep_island_range(body_data, start, end);
952
- }
953
- } else {
954
- // At least one member is active — reset every timer.
955
- for (let i = start; i < end; i++) {
956
- const rb = bodies[body_data[i]];
957
- if (rb !== undefined) rb.sleep_timer = 0;
958
- }
959
- }
960
- }
961
- }
962
-
963
- /**
964
- * Dispatch every buffered contact event through:
965
- * - the system-level Signals ({@link onContactBegin}/Stay/End), always; and
966
- * - the entity-level event channel via `dataset.sendEvent(entity, name, payload)`,
967
- * when a dataset is attached.
968
- *
969
- * Payload is a reused scratch object; listeners must copy anything they
970
- * intend to retain past the listener body.
971
- * @private
972
- */
973
- __dispatch_contact_events() {
974
- const events = this.contactEvents;
975
- const n = events.count;
976
- if (n === 0) return;
977
-
978
- const ecd = (this.entityManager !== null && this.entityManager !== undefined)
979
- ? this.entityManager.dataset
980
- : null;
981
- const manifolds = this.manifolds;
982
- const data = manifolds.data_buffer;
983
-
984
- const payload = this.__contact_payload;
985
-
986
- for (let i = 0; i < n; i++) {
987
- const kind = events.kind_at(i);
988
- const entA = events.entityA_at(i);
989
- const entB = events.entityB_at(i);
990
- const slot = events.slot_at(i);
991
-
992
- // Use the deepest contact of the manifold as the representative
993
- // point/normal/depth for the event. v1: contact 0 only.
994
- const slot_off = manifolds.slot_data_offset(slot);
995
- const has_contact = manifolds.contact_count(slot) > 0;
996
-
997
- // Scratch payload no observers subscribe to its component
998
- // vectors; write indices directly to skip the Signal dispatch.
999
- const pt = payload.point;
1000
- const nm = payload.normal;
1001
- if (has_contact) {
1002
- const wax = data[slot_off], way = data[slot_off + 1], waz = data[slot_off + 2];
1003
- const wbx = data[slot_off + 3], wby = data[slot_off + 4], wbz = data[slot_off + 5];
1004
- pt[0] = (wax + wbx) * 0.5; pt[1] = (way + wby) * 0.5; pt[2] = (waz + wbz) * 0.5;
1005
- nm[0] = data[slot_off + 6]; nm[1] = data[slot_off + 7]; nm[2] = data[slot_off + 8];
1006
- payload.depth = data[slot_off + 9];
1007
- } else {
1008
- pt[0] = 0; pt[1] = 0; pt[2] = 0;
1009
- nm[0] = 0; nm[1] = 0; nm[2] = 0;
1010
- payload.depth = 0;
1011
- }
1012
- payload.entityA = entA;
1013
- payload.entityB = entB;
1014
-
1015
- let event_name;
1016
- let signal;
1017
- if (kind === ContactEventKind.Begin) { event_name = PhysicsEvents.ContactBegin; signal = this.onContactBegin; }
1018
- else if (kind === ContactEventKind.Stay) { event_name = PhysicsEvents.ContactStay; signal = this.onContactStay; }
1019
- else { event_name = PhysicsEvents.ContactEnd; signal = this.onContactEnd; }
1020
-
1021
- signal.send1(payload);
1022
-
1023
- if (ecd !== null && ecd !== undefined) {
1024
- if (entA >= 0) ecd.sendEvent(entA, event_name, payload);
1025
- if (entB >= 0) ecd.sendEvent(entB, event_name, payload);
1026
- }
1027
- }
1028
- }
1029
-
1030
- fixedUpdate(dt) {
1031
- const gx = this.gravity.x;
1032
- const gy = this.gravity.y;
1033
- const gz = this.gravity.z;
1034
-
1035
- const lists = this.__body_collider_lists;
1036
-
1037
- // ─── Once-per-tick stages (broadphase, narrowphase, islands) ────
1038
- //
1039
- // Position-dependent work — broadphase refit, pair generation,
1040
- // narrowphase contact extraction, island partitioning — runs ONCE
1041
- // at the top of the tick using the pose at tick-start. Substep
1042
- // position integration happens inside the inner loop further down;
1043
- // intra-tick motion is bounded by the fat AABB margin (it uses the
1044
- // full-tick dt for sweep volume) so manifolds stay valid across
1045
- // substeps without re-running narrowphase.
1046
-
1047
- // Stage 1: refit each awake body's collider leaves with a fat AABB
1048
- // padded for the full tick's swept extent.
1049
- const count = this.storage.awake_count;
1050
- for (let i = 0; i < count; i++) {
1051
- const idx = this.storage.awake_at(i);
1052
- const rb = this.__bodies[idx];
1053
- const list = lists[idx];
1054
- if (list === undefined) continue;
1055
- const lv = rb.linearVelocity;
1056
- for (let k = 0; k < list.length; k++) {
1057
- const entry = list[k];
1058
- compute_fat_world_aabb(
1059
- scratch_world_aabb, 0,
1060
- entry.collider.shape, entry.transform,
1061
- lv[0], lv[1], lv[2],
1062
- dt
1063
- );
1064
- this.dynamicBvh.node_move_aabb(entry.bvhNode, scratch_world_aabb);
1065
- }
1066
- }
1067
-
1068
- // Stage 2: broadphase pair generation (canonical min,max body pairs).
1069
- generate_pairs(
1070
- this.storage,
1071
- this.dynamicBvh,
1072
- this.staticBvh,
1073
- this.manifolds,
1074
- lists,
1075
- this.pairs,
1076
- this.__pair_filter_bound,
1077
- );
1078
-
1079
- // Stage 3: wake propagation. Any sleeping body whose leaf overlaps
1080
- // an awake leaf this tick must participate in narrowphase + solve.
1081
- this.__wake_pairs();
1082
-
1083
- // Stage 4: narrowphase — closed-form fast paths or GJK+EPA per pair.
1084
- narrowphase_step(this.pairs, this.manifolds, this);
1085
-
1086
- // Stage 5: partition awake bodies + touched contacts into islands.
1087
- this.islands.build(this.storage, this.manifolds, this.__bodies, this.__body_collider_lists);
1088
-
1089
- // ─── TGS substep loop ───────────────────────────────────────────
1090
- //
1091
- // Each substep advances velocity by `gravity * sub_dt`, runs a
1092
- // short velocity-iteration sweep with Baumgarte bias scaled to
1093
- // `sub_dt`, and integrates position by `sub_dt`. Warm-start only
1094
- // fires on substep 0 — subsequent substeps continue from the
1095
- // impulses already accumulated in the manifold data buffer (those
1096
- // are reflected in the current velocity state; re-applying would
1097
- // double-count).
1098
- //
1099
- // Position correction is K× stronger over a tick than classic PGS
1100
- // for the same Baumgarte beta, because the velocity-correcting
1101
- // impulse fires K times and each one's position contribution
1102
- // integrates immediately. This is the mechanism that lets TGS
1103
- // resolve tall stacks where single-shot PGS just spreads info
1104
- // through the chain without resolving cumulative drift.
1105
- const substeps = this.solverSubsteps | 0;
1106
- const iters_per_substep = this.solverItersPerSubstep | 0;
1107
- const sub_dt = dt / substeps;
1108
-
1109
- // Re-read awake_count because __wake_pairs may have grown it.
1110
- const count_after_wake = this.storage.awake_count;
1111
-
1112
- for (let s = 0; s < substeps; s++) {
1113
- // Integrate velocity (gravity + accumulated forces) for this substep.
1114
- for (let i = 0; i < count_after_wake; i++) {
1115
- const idx = this.storage.awake_at(i);
1116
- const rb = this.__bodies[idx];
1117
- const tr = this.__transforms[idx];
1118
- integrate_velocity(rb, tr, gx, gy, gz, sub_dt);
1119
- }
1120
-
1121
- // Solve velocity constraints over the current pose. Bias term
1122
- // inside the solver uses `sub_dt`, so per-substep Baumgarte
1123
- // correction is K× stronger than full-dt PGS would apply.
1124
- solve_contacts(this.manifolds, this, sub_dt, iters_per_substep, s === 0);
1125
-
1126
- // Advance position with the solved velocity.
1127
- for (let i = 0; i < count_after_wake; i++) {
1128
- const idx = this.storage.awake_at(i);
1129
- const rb = this.__bodies[idx];
1130
- const tr = this.__transforms[idx];
1131
- integrate_position(rb, tr, sub_dt);
1132
- }
1133
- }
1134
-
1135
- // ─── End-of-tick stages (sleep, events, manifold cleanup) ───────
1136
-
1137
- // Stage 6: sleep test using the full-tick `dt` for the sleep timer.
1138
- this.__sleep_test(dt);
1139
-
1140
- // Stage 7: diff manifolds against the previous frame and dispatch
1141
- // Begin / Stay / End events. MUST run before advance_frame, which
1142
- // rolls the touched flags.
1143
- diff_manifolds(this.manifolds, this.storage, this.contactEvents);
1144
- this.__dispatch_contact_events();
1145
-
1146
- // Stage 8 (end-of-step): roll touched → prev_touched and evict slots
1147
- // whose pair has not been touched within the grace window.
1148
- this.manifolds.advance_frame();
1149
- }
1150
- }
1151
-
1152
- /**
1153
- * @readonly
1154
- * @type {boolean}
1155
- */
1156
- PhysicsSystem.prototype.isPhysicsSystem = true;
1157
-
1158
- // Re-export for convenience.
1159
- export { BodyKind, RigidBodyFlags };
1
+ import { assert } from "../../../core/assert.js";
2
+ import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
3
+ import Signal from "../../../core/events/signal/Signal.js";
4
+ import Vector3 from "../../../core/geom/Vector3.js";
5
+ import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
6
+ import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
7
+ import { System } from "../../ecs/System.js";
8
+ import { Transform } from "../../ecs/transform/Transform.js";
9
+ import { body_id_index, BodyStorage } from "../body/BodyStorage.js";
10
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
11
+ import { compute_fat_world_aabb } from "../broadphase/compute_fat_world_aabb.js";
12
+ import { generate_pairs } from "../broadphase/generate_pairs.js";
13
+ import { PairList } from "../broadphase/PairList.js";
14
+ import { CONTACT_STRIDE, ManifoldStore } from "../contact/ManifoldStore.js";
15
+ import { ContactEventBuffer, ContactEventKind } from "../events/ContactEventBuffer.js";
16
+ import { diff_manifolds } from "../events/diff_manifolds.js";
17
+ import { integrate_position } from "../integration/integrate_position.js";
18
+ import { integrate_velocity_forces, integrate_velocity_gravity } from "../integration/integrate_velocity.js";
19
+ import { IslandBuilder } from "../island/IslandBuilder.js";
20
+ import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
21
+ import { overlap_shape as overlap_shape_query } from "../queries/overlap_shape.js";
22
+ import { raycast as raycast_query } from "../queries/raycast.js";
23
+ import { shape_cast as shape_cast_query } from "../queries/shape_cast.js";
24
+ import { returnTrue } from "../../../core/function/returnTrue.js";
25
+ import {
26
+ prepare_contacts,
27
+ refresh_contacts,
28
+ redetect_concave_contacts,
29
+ warm_start_contacts,
30
+ solve_velocity,
31
+ apply_restitution,
32
+ solve_position,
33
+ } from "../solver/solve_contacts.js";
34
+ import { solve_joints } from "../constraint/solve_constraints.js";
35
+ import { JOINT_WORLD, JOINT_UNALLOCATED } from "./Joint.js";
36
+ import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
37
+ import { PhysicsEvents } from "./PhysicsEvents.js";
38
+
39
+ /**
40
+ * Scratch for {@link applyImpulseAt}'s angular delta calculation.
41
+ * @type {Float64Array}
42
+ */
43
+ const scratch_angular_delta = new Float64Array(3);
44
+ import { BodyKind } from "./BodyKind.js";
45
+ import { Collider, COLLIDER_UNBOUND } from "./Collider.js";
46
+ import { RIGID_BODY_UNALLOCATED, RigidBody } from "./RigidBody.js";
47
+ import { RigidBodyFlags } from "./RigidBodyFlags.js";
48
+ import { SleepState } from "./SleepState.js";
49
+
50
+ /**
51
+ * Reusable scratch buffer for world-AABB construction so the link path is
52
+ * allocation-free in steady state.
53
+ * @type {Float64Array}
54
+ */
55
+ const scratch_world_aabb = new Float64Array(6);
56
+
57
+ /**
58
+ * Reusable scratch buffer for the local AABB returned by
59
+ * {@link AbstractShape3D#compute_bounding_box}.
60
+ * @type {Float64Array}
61
+ */
62
+ const scratch_local_aabb = new Float64Array(6);
63
+
64
+ /**
65
+ * Rigid-body physics system.
66
+ *
67
+ * v1 scope: pool + active list + two BVHs (static / dynamic), Transform sync
68
+ * contract, and the user-facing API surface (gravity, force / impulse, layer
69
+ * filtering, wake / sleep, contact filter). The per-step simulation pipeline
70
+ * (broadphase pair generation, narrowphase, solver, islands, sleep test,
71
+ * contact-event emission) is built on top of this skeleton in subsequent
72
+ * iterations.
73
+ *
74
+ * Dependency tuple is `(RigidBody, Collider, Transform)` — every body has
75
+ * exactly one collider on the same entity. Compound bodies via child collider
76
+ * entities are an extension point handled by a follow-up sub-observer.
77
+ *
78
+ * @author Alex Goldring
79
+ * @copyright Company Named Limited (c) 2026
80
+ */
81
+ export class PhysicsSystem extends System {
82
+
83
+ constructor() {
84
+ super();
85
+
86
+ this.dependencies = [RigidBody, Transform];
87
+
88
+ this.components_used = [
89
+ ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Read | ResourceAccessKind.Write),
90
+ ResourceAccessSpecification.from(Collider, ResourceAccessKind.Read),
91
+ ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read | ResourceAccessKind.Write),
92
+ ];
93
+
94
+ /**
95
+ * @type {BodyStorage}
96
+ */
97
+ this.storage = new BodyStorage();
98
+
99
+ /**
100
+ * @type {BVH}
101
+ */
102
+ this.staticBvh = new BVH();
103
+
104
+ /**
105
+ * @type {BVH}
106
+ */
107
+ this.dynamicBvh = new BVH();
108
+
109
+ /**
110
+ * Persistent contact-manifold cache. One slot per active pair.
111
+ * @type {ManifoldStore}
112
+ */
113
+ this.manifolds = new ManifoldStore();
114
+
115
+ /**
116
+ * Per-frame list of broadphase-overlapping pairs (canonical
117
+ * `(min, max)`). Cleared at the top of each step.
118
+ * @type {PairList}
119
+ */
120
+ this.pairs = new PairList();
121
+
122
+ /**
123
+ * Per-frame contact-event buffer. Populated by the manifold diff
124
+ * pass at end-of-step and consumed by the dispatch pass.
125
+ * @type {ContactEventBuffer}
126
+ */
127
+ this.contactEvents = new ContactEventBuffer();
128
+
129
+ /**
130
+ * Per-frame island partitioning of the awake-body + contact graph.
131
+ * Rebuilt after narrowphase, consumed by the solver (and, in the
132
+ * follow-up slice, by the per-island atomic sleep test). Bodies
133
+ * static and kinematic act as constraint anchors and do not enlarge
134
+ * islands.
135
+ * @type {IslandBuilder}
136
+ */
137
+ this.islands = new IslandBuilder();
138
+
139
+ /**
140
+ * Velocity-squared threshold below which a body is eligible to start
141
+ * accumulating sleep time. Combined linear + angular kinetic-ish
142
+ * metric: `vx²+vy²+vz² + ωx²+ωy²+ωz²`. Default 0.01 corresponds to
143
+ * ~0.1 m/s linear or ~0.1 rad/s angular.
144
+ * @type {number}
145
+ */
146
+ this.sleepVelocitySqrThreshold = 0.01;
147
+
148
+ /**
149
+ * Seconds of below-threshold motion before a body is moved to the
150
+ * sleeping set. Box2D default is 0.5 s.
151
+ * @type {number}
152
+ */
153
+ this.sleepTimeThreshold = 0.5;
154
+
155
+ /**
156
+ * Number of TGS substeps per `fixedUpdate`. Each substep re-runs the
157
+ * velocity + position solve at `dt / substeps` against contacts whose
158
+ * penetration is re-derived analytically from the bodies' moved poses
159
+ * — narrowphase still runs once per outer step. Higher counts buy
160
+ * stack stability, high-mass-ratio robustness, and smoother
161
+ * trajectories at a near-linear solver cost (sleeping islands are
162
+ * unaffected they never enter the loop).
163
+ *
164
+ * `1` reproduces the non-substepped (single-step) solve.
165
+ * @type {number}
166
+ */
167
+ this.substeps = 4;
168
+
169
+ /**
170
+ * Velocity iterations per substep. Lower than a single-step solver
171
+ * would need, because the substep loop revisits the contact set
172
+ * `substeps` times.
173
+ * @type {number}
174
+ */
175
+ this.velocityIterations = 4;
176
+
177
+ /**
178
+ * Position (split-impulse) iterations per substep.
179
+ * @type {number}
180
+ */
181
+ this.positionIterations = 1;
182
+
183
+ /**
184
+ * Reusable contact-event payload. Listeners must copy any fields they
185
+ * intend to retain past their own scope. Reset before each dispatch.
186
+ * @private
187
+ */
188
+ this.__contact_payload = {
189
+ entityA: -1,
190
+ entityB: -1,
191
+ kind: 0,
192
+ depth: 0,
193
+ normal: new Vector3(),
194
+ point: new Vector3(),
195
+ };
196
+
197
+ /**
198
+ * World gravity, m/s². Applied each step scaled by per-body gravityScale.
199
+ * @readonly
200
+ * @type {Vector3}
201
+ */
202
+ this.gravity = new Vector3(0, -9.81, 0);
203
+
204
+ /**
205
+ * Emitted at end-of-step for newly established contact pairs.
206
+ * @readonly
207
+ * @type {Signal}
208
+ */
209
+ this.onContactBegin = new Signal();
210
+
211
+ /**
212
+ * Emitted at end-of-step for contact pairs that persisted from the previous step.
213
+ * @readonly
214
+ * @type {Signal}
215
+ */
216
+ this.onContactStay = new Signal();
217
+
218
+ /**
219
+ * Emitted at end-of-step for contact pairs that disappeared.
220
+ * @readonly
221
+ * @type {Signal}
222
+ */
223
+ this.onContactEnd = new Signal();
224
+
225
+ /**
226
+ * Optional global contact filter. Called for each surviving broadphase
227
+ * pair; returning `false` discards the pair.
228
+ * @type {((entityA: number, entityB: number, colliderA: Collider, colliderB: Collider) => boolean) | null}
229
+ * @private
230
+ */
231
+ this.__contact_filter = null;
232
+
233
+ /**
234
+ * Per-body component side-tables indexed by body index (NOT packed id).
235
+ * Sparse arrays populated at link, cleared at unlink. The hot loop
236
+ * iterates `storage.awake_at(i)` and dereferences these.
237
+ * @type {RigidBody[]}
238
+ */
239
+ this.__bodies = [];
240
+ /** @type {Transform[]} */
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
+ * Bound reference to {@link __pair_filter} so we hand the same
299
+ * callable to {@link generate_pairs} each step without per-step
300
+ * allocation.
301
+ * @private
302
+ */
303
+ this.__pair_filter_bound = (idA, idB) => this.__pair_filter(idA, idB);
304
+ }
305
+
306
+ /**
307
+ * Symmetric layer/mask check + optional user callback. Called per
308
+ * candidate pair during broadphase. Returns `true` to accept the pair.
309
+ *
310
+ * Layer/mask rule: pair (A, B) collides iff
311
+ * `(A.layer & B.mask) !== 0 && (B.layer & A.mask) !== 0`.
312
+ * The user's contact-filter callback is consulted only for pairs that
313
+ * pass the bitmask test (cheap gate first).
314
+ *
315
+ * @private
316
+ * @param {number} idA packed body id
317
+ * @param {number} idB packed body id
318
+ * @returns {boolean}
319
+ */
320
+ __pair_filter(idA, idB) {
321
+ const idxA = body_id_index(idA);
322
+ const idxB = body_id_index(idB);
323
+ const rbA = this.__bodies[idxA];
324
+ const rbB = this.__bodies[idxB];
325
+ if (rbA === undefined || rbB === undefined) return false;
326
+
327
+ // Layer/mask gate (symmetric).
328
+ if (((rbA.layer & rbB.mask) | 0) === 0) return false;
329
+ if (((rbB.layer & rbA.mask) | 0) === 0) return false;
330
+
331
+ // User callback gate, if installed.
332
+ const fn = this.__contact_filter;
333
+ if (fn !== null) {
334
+ const entA = this.storage.entity_at(idxA);
335
+ const entB = this.storage.entity_at(idxB);
336
+ const colA = this.__primary_collider(idxA);
337
+ const colB = this.__primary_collider(idxB);
338
+ if (!fn(entA, entB, colA, colB)) return false;
339
+ }
340
+ return true;
341
+ }
342
+
343
+ /**
344
+ * First attached collider of a body, or `null` if none. Used by the
345
+ * solver to read material parameters (friction / restitution) when the
346
+ * per-contact source-collider identity isn't tracked yet (v1 limitation
347
+ * — multi-collider bodies with mixed materials lose precision here).
348
+ *
349
+ * @private
350
+ * @param {number} body_idx
351
+ * @returns {Collider|null}
352
+ */
353
+ __primary_collider(body_idx) {
354
+ const list = this.__body_collider_lists[body_idx];
355
+ return (list !== undefined && list.length > 0) ? list[0].collider : null;
356
+ }
357
+
358
+ /**
359
+ * Resize {@link __pseudo_velocity} to cover every live body slot and
360
+ * zero its contents. Called at the top of each fixedUpdate so the
361
+ * split-impulse position pass starts from a clean state — bodies the
362
+ * solver doesn't touch contribute zero pseudo-velocity to
363
+ * {@link integrate_position}.
364
+ *
365
+ * Doubles on growth like the other physics scratches; never shrinks.
366
+ *
367
+ * @private
368
+ */
369
+ __reset_pseudo_velocity() {
370
+ const required = this.storage.high_water_mark * 6;
371
+ if (this.__pseudo_velocity.length < required) {
372
+ this.__pseudo_velocity = new Float64Array(required * 2);
373
+ } else if (required > 0) {
374
+ this.__pseudo_velocity.fill(0, 0, required);
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Replace the world gravity vector. Effective on the next step.
380
+ * @param {Vector3|{x:number,y:number,z:number}} v
381
+ */
382
+ setGravity(v) {
383
+ this.gravity.set(v.x, v.y, v.z);
384
+ }
385
+
386
+ /**
387
+ * Install (or remove with `null`) the contact filter callback.
388
+ * @param {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null} fn
389
+ */
390
+ setContactFilter(fn) {
391
+ this.__contact_filter = fn;
392
+ }
393
+
394
+ /**
395
+ * @returns {((entityA:number, entityB:number, colliderA:Collider, colliderB:Collider) => boolean) | null}
396
+ */
397
+ getContactFilter() {
398
+ return this.__contact_filter;
399
+ }
400
+
401
+ /**
402
+ * @private
403
+ * @param {RigidBody} rb
404
+ * @param {Collider} collider
405
+ * @param {Transform} transform
406
+ * @returns {number} BVH node id
407
+ */
408
+ __insert_into_broadphase(rb, collider, transform) {
409
+ const shape = collider.shape;
410
+
411
+ assert.notNull(shape, 'Collider.shape must be set before attaching');
412
+
413
+ shape.compute_bounding_box(scratch_local_aabb);
414
+
415
+ const p = transform.position;
416
+ const q = transform.rotation;
417
+
418
+ aabb3_transform_oriented(
419
+ scratch_world_aabb, 0,
420
+ scratch_local_aabb[0], scratch_local_aabb[1], scratch_local_aabb[2],
421
+ scratch_local_aabb[3], scratch_local_aabb[4], scratch_local_aabb[5],
422
+ p.x, p.y, p.z,
423
+ q.x, q.y, q.z, q.w
424
+ );
425
+
426
+ const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
427
+ const node = bvh.allocate_node();
428
+
429
+ bvh.node_set_aabb_primitive(
430
+ node,
431
+ scratch_world_aabb[0], scratch_world_aabb[1], scratch_world_aabb[2],
432
+ scratch_world_aabb[3], scratch_world_aabb[4], scratch_world_aabb[5]
433
+ );
434
+ bvh.node_set_user_data(node, rb._bodyId);
435
+ bvh.insert_leaf(node);
436
+
437
+ return node;
438
+ }
439
+
440
+ /**
441
+ * @private
442
+ * @param {RigidBody} rb
443
+ * @param {number} node
444
+ */
445
+ __remove_from_broadphase(rb, node) {
446
+ const bvh = rb.kind === BodyKind.Static ? this.staticBvh : this.dynamicBvh;
447
+ bvh.remove_leaf(node);
448
+ bvh.release_node(node);
449
+ }
450
+
451
+ /**
452
+ * Lifecycle entry: invoked when an entity gains the (RigidBody, Transform)
453
+ * tuple. Allocates the body's slot in storage and seeds an empty collider
454
+ * list. Colliders are attached separately via {@link attach_collider} (the
455
+ * paired {@link ColliderObserverSystem} drives this from the dataset; in
456
+ * tests, call manually).
457
+ *
458
+ * @param {RigidBody} rigidBody
459
+ * @param {Transform} transform
460
+ * @param {number} entity
461
+ */
462
+ link(rigidBody, transform, entity) {
463
+ const packed = this.storage.allocate(entity);
464
+ rigidBody._bodyId = packed;
465
+ rigidBody.sleepState = SleepState.Awake;
466
+
467
+ const index = body_id_index(packed);
468
+ this.storage.set_kind(index, rigidBody.kind);
469
+ this.storage.set_flags(index, rigidBody.flags);
470
+
471
+ this.__bodies[index] = rigidBody;
472
+ this.__transforms[index] = transform;
473
+ this.__body_collider_lists[index] = [];
474
+
475
+ // Static bodies do not need to live in the active list — they never move.
476
+ if (rigidBody.kind === BodyKind.Static) {
477
+ this.storage.mark_sleeping(index);
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Detach every collider attached to this body, free its slot, and clear
483
+ * the side-tables.
484
+ *
485
+ * @param {RigidBody} rigidBody
486
+ * @param {Transform} transform
487
+ * @param {number} entity
488
+ */
489
+ unlink(rigidBody, transform, entity) {
490
+ const packed = rigidBody._bodyId;
491
+
492
+ assert.equal(this.storage.is_valid(packed), true, 'unlink: stale or absent body id');
493
+
494
+ // If the body is sleeping inside a multi-member sleep group, dissolve
495
+ // the group by waking every member first. Otherwise the surviving
496
+ // members would hold dangling indices in their sleep-group chain
497
+ // pointing at a slot that has just been freed.
498
+ if (rigidBody.sleep_group_next !== -1) {
499
+ this.__wake_body(rigidBody);
500
+ }
501
+
502
+ const index = body_id_index(packed);
503
+ const list = this.__body_collider_lists[index];
504
+ if (list !== undefined) {
505
+ for (let i = 0; i < list.length; i++) {
506
+ const entry = list[i];
507
+ if (entry.bvhNode !== COLLIDER_UNBOUND) {
508
+ this.__remove_from_broadphase(rigidBody, entry.bvhNode);
509
+ }
510
+ entry.collider._bvhNode = COLLIDER_UNBOUND;
511
+ entry.collider._bodyId = -1;
512
+ }
513
+ }
514
+
515
+ this.__bodies[index] = undefined;
516
+ this.__transforms[index] = undefined;
517
+ this.__body_collider_lists[index] = undefined;
518
+
519
+ this.storage.free(packed);
520
+ rigidBody._bodyId = RIGID_BODY_UNALLOCATED;
521
+ }
522
+
523
+ /**
524
+ * Attach a collider to an existing body. The collider can live on the
525
+ * same entity as the body (single-collider body) or on a child entity
526
+ * (compound body). The world transform passed here is the collider's
527
+ * own — for a same-entity collider it is the body's Transform; for a
528
+ * child collider it is the child entity's Transform.
529
+ *
530
+ * Idempotent — re-attaching the same collider is a no-op.
531
+ *
532
+ * @param {number} body_entity entity that owns the body
533
+ * @param {Collider} collider
534
+ * @param {Transform} transform world transform of the collider
535
+ * @param {number} [collider_entity] entity owning the collider (defaults to body_entity)
536
+ */
537
+ attach_collider(body_entity, collider, transform, collider_entity = body_entity) {
538
+ // Find the body by walking the storage entity table. The body must
539
+ // have been allocated via `link` before any colliders are attached.
540
+ const body_index = this.__find_body_index_by_entity(body_entity);
541
+ assert.notEqual(body_index, -1, `attach_collider: no body found for entity ${body_entity}`);
542
+
543
+ const rb = this.__bodies[body_index];
544
+ // Idempotent: skip if collider already attached.
545
+ if (collider._bvhNode !== COLLIDER_UNBOUND) return;
546
+
547
+ const node = this.__insert_into_broadphase(rb, collider, transform);
548
+
549
+ collider._bvhNode = node;
550
+ collider._bodyId = rb._bodyId;
551
+ this.__body_collider_lists[body_index].push({
552
+ collider, transform, entity: collider_entity, bvhNode: node,
553
+ });
554
+ }
555
+
556
+ /**
557
+ * Reverse of {@link attach_collider}. Idempotent.
558
+ *
559
+ * @param {number} body_entity
560
+ * @param {Collider} collider
561
+ */
562
+ detach_collider(body_entity, collider) {
563
+ if (collider._bvhNode === COLLIDER_UNBOUND) return;
564
+
565
+ const body_index = this.__find_body_index_by_entity(body_entity);
566
+ if (body_index === -1) return;
567
+
568
+ const rb = this.__bodies[body_index];
569
+ this.__remove_from_broadphase(rb, collider._bvhNode);
570
+
571
+ const list = this.__body_collider_lists[body_index];
572
+ for (let i = 0; i < list.length; i++) {
573
+ if (list[i].collider === collider) {
574
+ list.splice(i, 1);
575
+ break;
576
+ }
577
+ }
578
+
579
+ collider._bvhNode = COLLIDER_UNBOUND;
580
+ collider._bodyId = -1;
581
+ }
582
+
583
+ /**
584
+ * Linear scan over body slots looking for the one whose entity matches.
585
+ * O(N) where N is the live body count — only called on the link/unlink
586
+ * paths, not during simulation, so the scan cost is bounded.
587
+ *
588
+ * @private
589
+ * @param {number} entity
590
+ * @returns {number} body index or -1
591
+ */
592
+ __find_body_index_by_entity(entity) {
593
+ const hwm = this.storage.high_water_mark;
594
+ for (let i = 0; i < hwm; i++) {
595
+ if (this.storage.entity_at(i) === entity) return i;
596
+ }
597
+ return -1;
598
+ }
599
+
600
+ /**
601
+ * Register a {@link Joint} (6-DOF constraint). Resolves the joint's
602
+ * entities to packed body ids and adds it to the active set, where it is
603
+ * solved alongside contacts in the TGS substep loop. Body A must be a
604
+ * linked body; body B is either a linked body or {@link JOINT_WORLD}
605
+ * (anchoring A to a fixed world point `localAnchorB` is then a world
606
+ * coordinate).
607
+ *
608
+ * @param {Joint} joint
609
+ */
610
+ link_joint(joint) {
611
+ const idxA = this.__find_body_index_by_entity(joint.entityA);
612
+ assert.notEqual(idxA, -1, `link_joint: no body for entityA ${joint.entityA}`);
613
+ joint._bodyIdA = this.__bodies[idxA]._bodyId;
614
+
615
+ if (joint.entityB === JOINT_WORLD) {
616
+ joint._bodyIdB = JOINT_WORLD;
617
+ } else {
618
+ const idxB = this.__find_body_index_by_entity(joint.entityB);
619
+ assert.notEqual(idxB, -1, `link_joint: no body for entityB ${joint.entityB}`);
620
+ joint._bodyIdB = this.__bodies[idxB]._bodyId;
621
+ }
622
+
623
+ // Reuse a freed slot if available so joint ids stay dense-ish.
624
+ let id;
625
+ if (this.__joint_free.length > 0) {
626
+ id = this.__joint_free.pop();
627
+ } else {
628
+ id = this.__joints.length;
629
+ }
630
+ joint._jointId = id;
631
+ this.__joints[id] = joint;
632
+ }
633
+
634
+ /**
635
+ * Remove a previously {@link link_joint}'d joint from the active set.
636
+ * Idempotent.
637
+ * @param {Joint} joint
638
+ */
639
+ unlink_joint(joint) {
640
+ const id = joint._jointId;
641
+ if (id === JOINT_UNALLOCATED || this.__joints[id] !== joint) return;
642
+ this.__joints[id] = undefined;
643
+ this.__joint_free.push(id);
644
+ joint._jointId = JOINT_UNALLOCATED;
645
+ }
646
+
647
+ /**
648
+ * Resolve a packed body id to its entity, or `-1` if the id is stale.
649
+ * @param {number} packed_body_id
650
+ * @returns {number}
651
+ */
652
+ entityOf(packed_body_id) {
653
+ if (!this.storage.is_valid(packed_body_id)) return -1;
654
+ return this.storage.entity_at(body_id_index(packed_body_id));
655
+ }
656
+
657
+ /**
658
+ * Number of live bodies (regardless of awake/sleeping state).
659
+ * @returns {number}
660
+ */
661
+ get bodyCount() {
662
+ return this.storage.size;
663
+ }
664
+
665
+ /**
666
+ * Apply an instantaneous change of momentum at the body's centre of mass.
667
+ * Linear-only — see {@link applyImpulseAt} for an off-centre impulse that
668
+ * also produces angular response.
669
+ *
670
+ * Wakes the body if it is asleep.
671
+ *
672
+ * @param {RigidBody} rigidBody
673
+ * @param {Vector3|{x:number,y:number,z:number}} impulse
674
+ */
675
+ applyImpulse(rigidBody, impulse) {
676
+ if (rigidBody.kind !== BodyKind.Dynamic) {
677
+ return;
678
+ }
679
+ const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
680
+ rigidBody.linearVelocity.addScaled(impulse, inv_m);
681
+
682
+ this.__wake_body(rigidBody);
683
+ }
684
+
685
+ /**
686
+ * Apply an instantaneous change of momentum at a specific world-space point.
687
+ * Off-centre impulses produce both linear (Δv = P/m) and angular
688
+ * (Δω = I_w⁻¹·(r × P)) response.
689
+ *
690
+ * Wakes the body if it is asleep.
691
+ *
692
+ * @param {RigidBody} rigidBody
693
+ * @param {Transform} transform body's current world Transform (used for r and I_w)
694
+ * @param {Vector3|{x:number,y:number,z:number}} impulse
695
+ * @param {Vector3|{x:number,y:number,z:number}} worldPoint
696
+ */
697
+ applyImpulseAt(rigidBody, transform, impulse, worldPoint) {
698
+ if (rigidBody.kind !== BodyKind.Dynamic) {
699
+ return;
700
+ }
701
+ const inv_m = rigidBody.mass > 0 ? 1 / rigidBody.mass : 0;
702
+
703
+ rigidBody.linearVelocity.set(
704
+ rigidBody.linearVelocity.x + impulse.x * inv_m,
705
+ rigidBody.linearVelocity.y + impulse.y * inv_m,
706
+ rigidBody.linearVelocity.z + impulse.z * inv_m
707
+ );
708
+
709
+ const rx = worldPoint.x - transform.position.x;
710
+ const ry = worldPoint.y - transform.position.y;
711
+ const rz = worldPoint.z - transform.position.z;
712
+
713
+ // Δω = I_w⁻¹ · (r × P)
714
+ const tx = ry * impulse.z - rz * impulse.y;
715
+ const ty = rz * impulse.x - rx * impulse.z;
716
+ const tz = rx * impulse.y - ry * impulse.x;
717
+
718
+ world_inverse_inertia_apply(scratch_angular_delta, 0, rigidBody.inverseInertiaLocal, transform.rotation, tx, ty, tz);
719
+
720
+ rigidBody.angularVelocity.set(
721
+ rigidBody.angularVelocity.x + scratch_angular_delta[0],
722
+ rigidBody.angularVelocity.y + scratch_angular_delta[1],
723
+ rigidBody.angularVelocity.z + scratch_angular_delta[2]
724
+ );
725
+
726
+ this.__wake_body(rigidBody);
727
+ }
728
+
729
+ /**
730
+ * Accumulate a continuous torque (world-space) for integration on the
731
+ * next fixedUpdate. Pairs with {@link applyForce} for the rotational case.
732
+ *
733
+ * Wakes the body if asleep.
734
+ *
735
+ * @param {RigidBody} rigidBody
736
+ * @param {Vector3|{x:number,y:number,z:number}} torque
737
+ */
738
+ applyTorque(rigidBody, torque) {
739
+ if (rigidBody.kind !== BodyKind.Dynamic) {
740
+ return;
741
+ }
742
+ rigidBody.accumulatedTorque.set(
743
+ rigidBody.accumulatedTorque.x + torque.x,
744
+ rigidBody.accumulatedTorque.y + torque.y,
745
+ rigidBody.accumulatedTorque.z + torque.z
746
+ );
747
+ this.__wake_body(rigidBody);
748
+ }
749
+
750
+ /**
751
+ * Apply a continuous force at a specific world-space point. The force
752
+ * generates both a linear acceleration (F/m) and a torque (r × F) about
753
+ * the body's centre of mass.
754
+ *
755
+ * Wakes the body if asleep.
756
+ *
757
+ * @param {RigidBody} rigidBody
758
+ * @param {Transform} transform body's current world Transform
759
+ * @param {Vector3|{x:number,y:number,z:number}} force
760
+ * @param {Vector3|{x:number,y:number,z:number}} worldPoint
761
+ */
762
+ applyForceAt(rigidBody, transform, force, worldPoint) {
763
+ if (rigidBody.kind !== BodyKind.Dynamic) {
764
+ return;
765
+ }
766
+ rigidBody.accumulatedForce.set(
767
+ rigidBody.accumulatedForce.x + force.x,
768
+ rigidBody.accumulatedForce.y + force.y,
769
+ rigidBody.accumulatedForce.z + force.z
770
+ );
771
+
772
+ const rx = worldPoint.x - transform.position.x;
773
+ const ry = worldPoint.y - transform.position.y;
774
+ const rz = worldPoint.z - transform.position.z;
775
+
776
+ rigidBody.accumulatedTorque.set(
777
+ rigidBody.accumulatedTorque.x + (ry * force.z - rz * force.y),
778
+ rigidBody.accumulatedTorque.y + (rz * force.x - rx * force.z),
779
+ rigidBody.accumulatedTorque.z + (rx * force.y - ry * force.x)
780
+ );
781
+
782
+ this.__wake_body(rigidBody);
783
+ }
784
+
785
+ /**
786
+ * Accumulate a continuous force to be integrated next fixedUpdate step.
787
+ * Wakes the body if asleep.
788
+ *
789
+ * @param {RigidBody} rigidBody
790
+ * @param {Vector3|{x:number,y:number,z:number}} force
791
+ */
792
+ applyForce(rigidBody, force) {
793
+ if (rigidBody.kind !== BodyKind.Dynamic) {
794
+ return;
795
+ }
796
+ rigidBody.accumulatedForce.add( force);
797
+ this.__wake_body(rigidBody);
798
+ }
799
+
800
+ /**
801
+ * Replace the linear velocity. Wakes the body if asleep.
802
+ *
803
+ * @param {RigidBody} rigidBody
804
+ * @param {Vector3|{x:number,y:number,z:number}} v
805
+ */
806
+ setLinearVelocity(rigidBody, v) {
807
+ rigidBody.linearVelocity.copy(v);
808
+ if (rigidBody.kind === BodyKind.Dynamic) {
809
+ this.__wake_body(rigidBody);
810
+ }
811
+ }
812
+
813
+ /**
814
+ * Force the body awake. Static bodies are ignored.
815
+ * @param {RigidBody} rigidBody
816
+ */
817
+ wake(rigidBody) {
818
+ this.__wake_body(rigidBody);
819
+ }
820
+
821
+ /**
822
+ * Force the body asleep. Dynamic bodies will not re-enter the active list
823
+ * until a wake event occurs.
824
+ * @param {RigidBody} rigidBody
825
+ */
826
+ sleep(rigidBody) {
827
+ if (rigidBody.kind !== BodyKind.Dynamic) {
828
+ return;
829
+ }
830
+ if (rigidBody.sleepState === SleepState.Sleeping) {
831
+ return;
832
+ }
833
+ rigidBody.sleepState = SleepState.Sleeping;
834
+ const index = body_id_index(rigidBody._bodyId);
835
+ this.storage.mark_sleeping(index);
836
+ }
837
+
838
+ /**
839
+ * Wake a body and atomically wake every other body it was last sleeping
840
+ * with (its "sleep group"). Sleep groups are circular doubly-linked lists
841
+ * threaded through every member of an island when it sleeps atomically;
842
+ * waking any one member walks the chain and wakes the rest in the same
843
+ * call.
844
+ *
845
+ * Without this, a 100-block stack hit at the base would wake one block
846
+ * per frame as the broadphase propagated awareness up the stack — a
847
+ * visible ~1.6 s wave at 60 fps. Atomic wake eliminates the wave.
848
+ *
849
+ * No-op for non-dynamic bodies. Idempotent for already-awake bodies.
850
+ * @private
851
+ * @param {RigidBody} rb
852
+ */
853
+ __wake_body(rb) {
854
+ if (rb.kind !== BodyKind.Dynamic) return;
855
+ if (rb.sleepState === SleepState.Awake) return;
856
+ const index = body_id_index(rb._bodyId);
857
+
858
+ // Remember the next-in-chain before clearing the body's own pointers;
859
+ // the rest of the group is reached by walking forward from there.
860
+ const start_next = rb.sleep_group_next;
861
+
862
+ rb.sleepState = SleepState.Awake;
863
+ rb.sleep_timer = 0;
864
+ rb.sleep_group_next = -1;
865
+ rb.sleep_group_prev = -1;
866
+ this.storage.mark_awake(index);
867
+
868
+ if (start_next === -1 || start_next === index) return;
869
+
870
+ // Walk the (now-broken) chain forward until we loop back. The chain
871
+ // is circular so we know when to stop; defensive `-1` guards against
872
+ // corruption from a body being unlinked mid-sleep-group.
873
+ let cur = start_next;
874
+ while (cur !== -1 && cur !== index) {
875
+ const cur_rb = this.__bodies[cur];
876
+ if (cur_rb === undefined) break;
877
+ const nxt = cur_rb.sleep_group_next;
878
+ cur_rb.sleepState = SleepState.Awake;
879
+ cur_rb.sleep_timer = 0;
880
+ cur_rb.sleep_group_next = -1;
881
+ cur_rb.sleep_group_prev = -1;
882
+ this.storage.mark_awake(cur);
883
+ cur = nxt;
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Atomically put every body in a contiguous range of island members to
889
+ * sleep. Members are threaded into a circular doubly-linked list so any
890
+ * future `wake` on any member walks the chain and revives them all.
891
+ *
892
+ * Velocities are zeroed because the body is by definition at rest at
893
+ * this point — the alternative (storing residual velocities for "softer"
894
+ * wake) is what Bullet does, but for a deterministic game-physics target
895
+ * fully resetting is simpler and avoids drift while sleeping.
896
+ *
897
+ * @private
898
+ * @param {Uint32Array} member_array view (or full array) of body indices
899
+ * @param {number} start
900
+ * @param {number} end
901
+ */
902
+ __atomic_sleep_island_range(member_array, start, end) {
903
+ const count = end - start;
904
+ if (count === 0) return;
905
+ const bodies = this.__bodies;
906
+ const storage = this.storage;
907
+
908
+ if (count === 1) {
909
+ const idx = member_array[start];
910
+ const rb = bodies[idx];
911
+ if (rb === undefined) return;
912
+ rb.sleep_group_next = -1;
913
+ rb.sleep_group_prev = -1;
914
+ rb.sleepState = SleepState.Sleeping;
915
+ rb.linearVelocity[0] = 0; rb.linearVelocity[1] = 0; rb.linearVelocity[2] = 0;
916
+ rb.angularVelocity[0] = 0; rb.angularVelocity[1] = 0; rb.angularVelocity[2] = 0;
917
+ storage.mark_sleeping(idx);
918
+ return;
919
+ }
920
+
921
+ for (let i = 0; i < count; i++) {
922
+ const idx = member_array[start + i];
923
+ const rb = bodies[idx];
924
+ if (rb === undefined) continue;
925
+ const next_idx = member_array[start + ((i + 1) % count)];
926
+ const prev_idx = member_array[start + ((i - 1 + count) % count)];
927
+ rb.sleep_group_next = next_idx;
928
+ rb.sleep_group_prev = prev_idx;
929
+ rb.sleepState = SleepState.Sleeping;
930
+ rb.linearVelocity[0] = 0; rb.linearVelocity[1] = 0; rb.linearVelocity[2] = 0;
931
+ rb.angularVelocity[0] = 0; rb.angularVelocity[1] = 0; rb.angularVelocity[2] = 0;
932
+ storage.mark_sleeping(idx);
933
+ }
934
+ }
935
+
936
+ /**
937
+ * Get the body index for a packed body id without revalidation. Used by
938
+ * query traversals that already trust the id came from a live BVH leaf.
939
+ * @param {number} packed_body_id
940
+ * @returns {number}
941
+ */
942
+ __index_of(packed_body_id) {
943
+ return body_id_index(packed_body_id);
944
+ }
945
+
946
+ /**
947
+ * Broadphase raycast against both BVHs. Fills `result` with the nearest
948
+ * hit and returns `true` on hit, `false` on miss.
949
+ *
950
+ * Narrowphase refinement against the actual shape geometry is a
951
+ * follow-up — for now `result.t` is the distance to the leaf's
952
+ * inflated AABB and `result.normal` is the AABB face normal. Both are
953
+ * exact for AABB-shaped colliders.
954
+ *
955
+ * @param {Ray3} ray origin + unit direction + `tMax`
956
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
957
+ * @param {(entity:number, collider:Collider)=>boolean} [filter] defaults
958
+ * to {@link returnTrue} (accept every candidate)
959
+ * @returns {boolean}
960
+ */
961
+ raycast(ray, result, filter = returnTrue) {
962
+ return raycast_query(this, ray, result, filter);
963
+ }
964
+
965
+ /**
966
+ * Sweep a convex shape along a ray and find the first body it
967
+ * would hit. The shape starts at `ray.origin` oriented by
968
+ * `rotation` and translates along `ray.direction` for up to
969
+ * `ray.tMax` units. Returns the nearest impact within that
970
+ * interval.
971
+ *
972
+ * The "swept AABB" broadphase finds candidate bodies whose BVH
973
+ * leaves overlap the shape's swept volume; the narrowphase then
974
+ * bisects [0, t] on GJK overlap to find the time-of-impact for
975
+ * each candidate. Best-t early termination skips candidates that
976
+ * can't tighten the answer.
977
+ *
978
+ * @param {Ray3} ray origin + unit direction + `tMax`
979
+ * @param {AbstractShape3D} shape
980
+ * @param {{x:number,y:number,z:number,w:number}} rotation
981
+ * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
982
+ * @param {(entity:number, collider:Collider)=>boolean} [filter]
983
+ * @returns {boolean}
984
+ */
985
+ shapeCast(ray, shape, rotation, result, filter = returnTrue) {
986
+ return shape_cast_query(this, ray, shape, rotation, result, filter);
987
+ }
988
+
989
+ /**
990
+ * Speculative overlap query: find all bodies whose collider would
991
+ * overlap the given convex shape placed at the given world pose,
992
+ * without mutating the simulation. Intended for kinematic /
993
+ * character controllers that need to test "would I collide if I
994
+ * moved here?" before committing the move.
995
+ *
996
+ * Pipeline:
997
+ * 1. The shape's world AABB is computed from `position` + `rotation`.
998
+ * 2. Both broadphase trees (static + dynamic) are queried for
999
+ * bodies whose leaf AABB overlaps that envelope.
1000
+ * 3. Each candidate is GJK-tested in world frame. Convex
1001
+ * candidates run one GJK call; concave candidates (heightmap,
1002
+ * mesh) run per-triangle GJK via the decomposition path.
1003
+ * 4. The optional `filter` is consulted before the GJK test
1004
+ * useful for skipping the caller's own body, allies, sensors,
1005
+ * etc.
1006
+ *
1007
+ * The output buffer is filled with overlapping bodies' `body_id`
1008
+ * values (uint32 with packed generation), starting at
1009
+ * `output_offset`. The caller is responsible for sizing the buffer;
1010
+ * IDs past its end are dropped silently and the count caps at the
1011
+ * available space.
1012
+ *
1013
+ * The query shape must be convex. Concave query shapes throw —
1014
+ * they're typically static terrain and not used as kinematic
1015
+ * probes; the M×N triangle-pair cost wouldn't be worth the rare
1016
+ * use case.
1017
+ *
1018
+ * @param {AbstractShape3D} shape convex query shape, in its local frame
1019
+ * @param {{x:number,y:number,z:number}} position world position of
1020
+ * the query shape
1021
+ * @param {{x:number,y:number,z:number,w:number}} rotation world
1022
+ * rotation (unit quaternion)
1023
+ * @param {Uint32Array|number[]} output buffer to receive body_ids
1024
+ * @param {number} output_offset starting index in output
1025
+ * @param {(entity:number, collider:Collider)=>boolean} [filter]
1026
+ * defaults to {@link returnTrue}
1027
+ * @returns {number} number of overlapping bodies written
1028
+ */
1029
+ overlap(shape, position, rotation, output, output_offset, filter = returnTrue) {
1030
+ return overlap_shape_query(this, shape, position, rotation, output, output_offset, filter);
1031
+ }
1032
+
1033
+ /**
1034
+ * Run one simulation step. v1 pipeline: integrate velocity → integrate
1035
+ * position refit broadphase AABBs. Narrowphase, solver, islands and
1036
+ * contact-event emission land in subsequent slices.
1037
+ *
1038
+ * @param {number} dt
1039
+ */
1040
+ /**
1041
+ * Wake any sleeping body that appears in this step's broadphase pair list.
1042
+ * A pair means the BVH AABBs overlap — even if the sleeper hasn't moved,
1043
+ * an awake neighbour has come into contact range and the sleeper must
1044
+ * participate in narrowphase / solve.
1045
+ * @private
1046
+ */
1047
+ __wake_pairs() {
1048
+ const list = this.pairs;
1049
+ const n = list.count;
1050
+ const bodies = this.__bodies;
1051
+ for (let i = 0; i < n; i++) {
1052
+ const idA = list.get_a(i);
1053
+ const idB = list.get_b(i);
1054
+ const idxA = body_id_index(idA);
1055
+ const idxB = body_id_index(idB);
1056
+ const a = bodies[idxA];
1057
+ const b = bodies[idxB];
1058
+ if (a !== undefined && a.sleepState === SleepState.Sleeping) {
1059
+ this.__wake_body(a);
1060
+ }
1061
+ if (b !== undefined && b.sleepState === SleepState.Sleeping) {
1062
+ this.__wake_body(b);
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ /**
1068
+ * Wake propagation across joints: if a joint connects an awake body to a
1069
+ * sleeping one, wake the sleeper so the constraint stays coupled (a joint
1070
+ * with one body asleep and one awake would otherwise be skipped by the
1071
+ * solver, letting the awake side drift). A common trigger is a
1072
+ * kinematic/motor-driven body pulling on a sleeping chain.
1073
+ *
1074
+ * Bodies that slept together as one island share a sleep group, so the
1075
+ * usual atomic wake already revives the whole chain when any member is
1076
+ * hit; this catches the cases that atomic wake doesn't (joint spanning
1077
+ * separate groups, kinematic driver).
1078
+ * @private
1079
+ */
1080
+ __wake_joints() {
1081
+ const joints = this.__joints;
1082
+ const n = joints.length;
1083
+ if (n === 0) return;
1084
+ const bodies = this.__bodies;
1085
+ const storage = this.storage;
1086
+ for (let i = 0; i < n; i++) {
1087
+ const joint = joints[i];
1088
+ if (joint === undefined || joint === null) continue;
1089
+ if (joint._bodyIdB === JOINT_WORLD) continue;
1090
+ if (!storage.is_valid(joint._bodyIdA) || !storage.is_valid(joint._bodyIdB)) continue;
1091
+ const a = bodies[body_id_index(joint._bodyIdA)];
1092
+ const b = bodies[body_id_index(joint._bodyIdB)];
1093
+ if (a === undefined || b === undefined) continue;
1094
+ const aSleep = a.sleepState === SleepState.Sleeping;
1095
+ const bSleep = b.sleepState === SleepState.Sleeping;
1096
+ if (aSleep === bSleep) continue; // both awake or both asleep leave as is
1097
+ if (aSleep) this.__wake_body(a); else this.__wake_body(b);
1098
+ }
1099
+ }
1100
+
1101
+ /**
1102
+ * Per-island atomic sleep test. Walks each island once and applies the
1103
+ * decision uniformly across all members:
1104
+ *
1105
+ * - If any member carries {@link RigidBodyFlags.DisableSleep}, the
1106
+ * entire island is exempt; every member's sleep_timer is reset.
1107
+ * - If `max(|v|² + |ω|²)` across all members is below
1108
+ * {@link sleepVelocitySqrThreshold}, every member's sleep_timer is
1109
+ * incremented by `dt`. When the smallest member's timer crosses
1110
+ * {@link sleepTimeThreshold}, the whole island sleeps atomically in
1111
+ * the same step (members get threaded into a sleep-group chain so
1112
+ * {@link __wake_body} can wake them all in one call).
1113
+ * - Otherwise the island has at least one active member, so every
1114
+ * member's timer is reset.
1115
+ *
1116
+ * This is the design-plan atomic-island sleep — replaces the per-body
1117
+ * approximation that lived in this slot during the previous slice.
1118
+ * Weakly-connected piles no longer chatter awake when a single member
1119
+ * blips above threshold; piles fall asleep and wake up as one.
1120
+ *
1121
+ * @private
1122
+ * @param {number} dt
1123
+ */
1124
+ __sleep_test(dt) {
1125
+ const threshold_sqr = this.sleepVelocitySqrThreshold;
1126
+ const time_threshold = this.sleepTimeThreshold;
1127
+ const bodies = this.__bodies;
1128
+ const islands = this.islands;
1129
+ const island_count = islands.island_count;
1130
+ const body_offsets = islands.body_offsets;
1131
+ const body_data = islands.body_data;
1132
+
1133
+ for (let isl = 0; isl < island_count; isl++) {
1134
+ const start = body_offsets[isl];
1135
+ const end = body_offsets[isl + 1];
1136
+ if (end === start) continue;
1137
+
1138
+ // Pass 1: find max v² + check DisableSleep across the island.
1139
+ let max_v_sqr = 0;
1140
+ let any_disable_sleep = false;
1141
+ for (let i = start; i < end; i++) {
1142
+ const idx = body_data[i];
1143
+ const rb = bodies[idx];
1144
+ if (rb === undefined) continue;
1145
+ if ((rb.flags & RigidBodyFlags.DisableSleep) !== 0) {
1146
+ any_disable_sleep = true;
1147
+ break;
1148
+ }
1149
+ const lv = rb.linearVelocity;
1150
+ const av = rb.angularVelocity;
1151
+ const v_sqr = lv[0] * lv[0] + lv[1] * lv[1] + lv[2] * lv[2]
1152
+ + av[0] * av[0] + av[1] * av[1] + av[2] * av[2];
1153
+ if (v_sqr > max_v_sqr) max_v_sqr = v_sqr;
1154
+ }
1155
+
1156
+ if (any_disable_sleep) {
1157
+ // Whole island is exempt — reset every member's timer.
1158
+ for (let i = start; i < end; i++) {
1159
+ const rb = bodies[body_data[i]];
1160
+ if (rb !== undefined) rb.sleep_timer = 0;
1161
+ }
1162
+ continue;
1163
+ }
1164
+
1165
+ if (max_v_sqr < threshold_sqr) {
1166
+ // Island is at rest — increment every member's timer; if the
1167
+ // slowest-stabilising member has crossed the time threshold,
1168
+ // every member has (they were incremented together this step),
1169
+ // so atomic-sleep the island.
1170
+ let min_timer = Infinity;
1171
+ for (let i = start; i < end; i++) {
1172
+ const rb = bodies[body_data[i]];
1173
+ if (rb === undefined) continue;
1174
+ rb.sleep_timer += dt;
1175
+ if (rb.sleep_timer < min_timer) min_timer = rb.sleep_timer;
1176
+ }
1177
+ if (min_timer >= time_threshold) {
1178
+ this.__atomic_sleep_island_range(body_data, start, end);
1179
+ }
1180
+ } else {
1181
+ // At least one member is active — reset every timer.
1182
+ for (let i = start; i < end; i++) {
1183
+ const rb = bodies[body_data[i]];
1184
+ if (rb !== undefined) rb.sleep_timer = 0;
1185
+ }
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ /**
1191
+ * Dispatch every buffered contact event through:
1192
+ * - the system-level Signals ({@link onContactBegin}/Stay/End), always; and
1193
+ * - the entity-level event channel via `dataset.sendEvent(entity, name, payload)`,
1194
+ * when a dataset is attached.
1195
+ *
1196
+ * Payload is a reused scratch object; listeners must copy anything they
1197
+ * intend to retain past the listener body.
1198
+ * @private
1199
+ */
1200
+ __dispatch_contact_events() {
1201
+ const events = this.contactEvents;
1202
+ const n = events.count;
1203
+ if (n === 0) return;
1204
+
1205
+ const ecd = (this.entityManager !== null && this.entityManager !== undefined)
1206
+ ? this.entityManager.dataset
1207
+ : null;
1208
+ const manifolds = this.manifolds;
1209
+ const data = manifolds.data_buffer;
1210
+
1211
+ const payload = this.__contact_payload;
1212
+
1213
+ for (let i = 0; i < n; i++) {
1214
+ const kind = events.kind_at(i);
1215
+ const entA = events.entityA_at(i);
1216
+ const entB = events.entityB_at(i);
1217
+ const slot = events.slot_at(i);
1218
+
1219
+ // Use the deepest contact of the manifold as the representative
1220
+ // point/normal/depth for the event. v1: contact 0 only.
1221
+ const slot_off = manifolds.slot_data_offset(slot);
1222
+ const has_contact = manifolds.contact_count(slot) > 0;
1223
+
1224
+ // Scratch payload — no observers subscribe to its component
1225
+ // vectors; write indices directly to skip the Signal dispatch.
1226
+ const pt = payload.point;
1227
+ const nm = payload.normal;
1228
+ if (has_contact) {
1229
+ const wax = data[slot_off], way = data[slot_off + 1], waz = data[slot_off + 2];
1230
+ const wbx = data[slot_off + 3], wby = data[slot_off + 4], wbz = data[slot_off + 5];
1231
+ pt[0] = (wax + wbx) * 0.5; pt[1] = (way + wby) * 0.5; pt[2] = (waz + wbz) * 0.5;
1232
+ nm[0] = data[slot_off + 6]; nm[1] = data[slot_off + 7]; nm[2] = data[slot_off + 8];
1233
+ payload.depth = data[slot_off + 9];
1234
+ } else {
1235
+ pt[0] = 0; pt[1] = 0; pt[2] = 0;
1236
+ nm[0] = 0; nm[1] = 0; nm[2] = 0;
1237
+ payload.depth = 0;
1238
+ }
1239
+ payload.entityA = entA;
1240
+ payload.entityB = entB;
1241
+
1242
+ let event_name;
1243
+ let signal;
1244
+ if (kind === ContactEventKind.Begin) { event_name = PhysicsEvents.ContactBegin; signal = this.onContactBegin; }
1245
+ else if (kind === ContactEventKind.Stay) { event_name = PhysicsEvents.ContactStay; signal = this.onContactStay; }
1246
+ else { event_name = PhysicsEvents.ContactEnd; signal = this.onContactEnd; }
1247
+
1248
+ signal.send1(payload);
1249
+
1250
+ if (ecd !== null && ecd !== undefined) {
1251
+ if (entA >= 0) ecd.sendEvent(entA, event_name, payload);
1252
+ if (entB >= 0) ecd.sendEvent(entB, event_name, payload);
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ fixedUpdate(dt) {
1258
+ const gx = this.gravity.x;
1259
+ const gy = this.gravity.y;
1260
+ const gz = this.gravity.z;
1261
+
1262
+ const count = this.storage.awake_count;
1263
+
1264
+ // Stage 1: consume the per-frame force / torque accumulators into
1265
+ // velocity at the full `dt`, exactly once (a user force is a per-frame
1266
+ // budget that must land in full regardless of substep count). Gravity
1267
+ // is applied per substep below, so the trajectory integrates at the
1268
+ // substep rate and — crucially — each substep's gravity is balanced by
1269
+ // that substep's contact warm-start, keeping resting stacks at zero
1270
+ // velocity.
1271
+ for (let i = 0; i < count; i++) {
1272
+ const idx = this.storage.awake_at(i);
1273
+ const rb = this.__bodies[idx];
1274
+ const tr = this.__transforms[idx];
1275
+ integrate_velocity_forces(rb, tr, dt);
1276
+ }
1277
+
1278
+ // Stage 2: refit each awake body's collider leaves at the current
1279
+ // pose, padded by the swept extent for the body's velocity. The fat
1280
+ // margin uses the post-force velocity; this frame's gravity increment
1281
+ // is a sub-millimetre slack difference, safely inside the margin.
1282
+ const lists = this.__body_collider_lists;
1283
+ for (let i = 0; i < count; i++) {
1284
+ const idx = this.storage.awake_at(i);
1285
+ const rb = this.__bodies[idx];
1286
+ const list = lists[idx];
1287
+ if (list === undefined) continue;
1288
+ const lv = rb.linearVelocity;
1289
+ for (let k = 0; k < list.length; k++) {
1290
+ const entry = list[k];
1291
+ compute_fat_world_aabb(
1292
+ scratch_world_aabb, 0,
1293
+ entry.collider.shape, entry.transform,
1294
+ lv[0], lv[1], lv[2],
1295
+ dt
1296
+ );
1297
+ this.dynamicBvh.node_move_aabb(entry.bvhNode, scratch_world_aabb);
1298
+ }
1299
+ }
1300
+
1301
+ // Stage 3: broadphase pair generation. The fat AABBs cover the full
1302
+ // outer-step motion, so the pair set stays valid across all substeps
1303
+ // — broadphase runs once.
1304
+ generate_pairs(
1305
+ this.storage,
1306
+ this.dynamicBvh,
1307
+ this.staticBvh,
1308
+ this.manifolds,
1309
+ lists,
1310
+ this.pairs,
1311
+ this.__pair_filter_bound,
1312
+ );
1313
+
1314
+ // Stage 4: wake propagation — through broadphase pairs, then through
1315
+ // joints (a joint must not have one body awake and one asleep).
1316
+ this.__wake_pairs();
1317
+ this.__wake_joints();
1318
+
1319
+ // Stage 5: narrowphase — once per outer step. The substep loop below
1320
+ // re-derives each contact's penetration analytically from the moved
1321
+ // poses rather than re-running geometry.
1322
+ narrowphase_step(this.pairs, this.manifolds, this.__body_collider_lists);
1323
+
1324
+ // Stage 6: partition awake bodies + touched contacts into islands.
1325
+ // Consumed by the solver (flattened contact list) and the sleep test.
1326
+ this.islands.build(this.storage, this.manifolds, this.__bodies, this.__body_collider_lists, this.__joints);
1327
+
1328
+ // Stage 7: TGS substep loop.
1329
+ //
1330
+ // prepare_contacts captures per-contact anchors / effective masses /
1331
+ // approach velocity (no warm-start — that's per-substep). Each substep
1332
+ // integrates gravity by `h`, re-derives contact geometry from the
1333
+ // current poses, replays warm-start, solves velocity (non-penetration
1334
+ // + friction) and position (pseudo-velocity), and integrates the pose
1335
+ // by `h`. Per-substep gravity + per-substep warm-start balance exactly
1336
+ // at a resting contact (each cancels `h` of the other), so stacks hold
1337
+ // at zero velocity and sleep; the position correction adapts as bodies
1338
+ // separate between substeps — the TGS stack-stability mechanism —
1339
+ // without re-running narrowphase. Restitution is applied once after
1340
+ // the loop.
1341
+ const N = this.substeps;
1342
+ const h = dt / N;
1343
+ const count_after_wake = this.storage.awake_count;
1344
+
1345
+ // Size the pseudo-velocity buffer ONCE (it may reallocate on growth),
1346
+ // then capture the reference. Inside the loop we only zero its live
1347
+ // region per substep — re-capturing is unnecessary since it won't
1348
+ // reallocate again this step.
1349
+ this.__reset_pseudo_velocity();
1350
+ const pseudoVel = this.__pseudo_velocity;
1351
+ const pseudo_len = this.storage.high_water_mark * 6;
1352
+
1353
+ prepare_contacts(this.manifolds, this, h);
1354
+
1355
+ for (let s = 0; s < N; s++) {
1356
+ // Gravity (+ damping) for this substep.
1357
+ for (let i = 0; i < count_after_wake; i++) {
1358
+ const idx = this.storage.awake_at(i);
1359
+ integrate_velocity_gravity(this.__bodies[idx], this.__transforms[idx], gx, gy, gz, h);
1360
+ }
1361
+
1362
+ // Re-derive contact geometry at the current poses: concave pairs
1363
+ // re-run narrowphase (fresh feature/normal as the body rocks),
1364
+ // convex pairs rotate frozen anchors analytically. Then replay
1365
+ // the per-substep warm-start and solve velocity.
1366
+ redetect_concave_contacts(this.manifolds, this);
1367
+ refresh_contacts(this.manifolds, this);
1368
+ warm_start_contacts(this.manifolds, this);
1369
+ solve_velocity(this.manifolds, this, this.velocityIterations);
1370
+
1371
+ // Joints share the substep: warm-start + velocity-solve the 6-DOF
1372
+ // constraints on real velocity, coupled with the contacts above
1373
+ // (a body touched by both sees one substep of Gauss-Seidel across
1374
+ // contacts then joints). Position correction for locked DOFs is a
1375
+ // SPOOK bias inside this solve, so no separate joint position pass.
1376
+ if (this.__joints.length > 0) {
1377
+ solve_joints(this.__joints, this, h, this.jointIterations);
1378
+ }
1379
+
1380
+ // Position correction writes pseudo-velocity (zeroed first so it
1381
+ // is a fresh per-substep correction), folded into the pose by the
1382
+ // position integrate and then discarded.
1383
+ pseudoVel.fill(0, 0, pseudo_len);
1384
+ solve_position(this.manifolds, this, this.positionIterations);
1385
+
1386
+ for (let i = 0; i < count_after_wake; i++) {
1387
+ const idx = this.storage.awake_at(i);
1388
+ const rb = this.__bodies[idx];
1389
+ const tr = this.__transforms[idx];
1390
+ const base = idx * 6;
1391
+ integrate_position(rb, tr, h,
1392
+ pseudoVel[base], pseudoVel[base + 1], pseudoVel[base + 2],
1393
+ pseudoVel[base + 3], pseudoVel[base + 4], pseudoVel[base + 5]);
1394
+ }
1395
+ }
1396
+
1397
+ // Stage 8: one-shot restitution, after the substep loop, keyed off
1398
+ // the approach velocity captured at prepare time.
1399
+ apply_restitution(this.manifolds, this);
1400
+
1401
+ // Stage 9: sleep test.
1402
+ this.__sleep_test(dt);
1403
+
1404
+ // Stage 10: diff manifolds against the previous frame and dispatch
1405
+ // Begin / Stay / End events. MUST run before advance_frame, which
1406
+ // rolls the touched flags.
1407
+ diff_manifolds(this.manifolds, this.storage, this.contactEvents);
1408
+ this.__dispatch_contact_events();
1409
+
1410
+ // Stage 11 (end-of-step): roll touched → prev_touched and evict slots
1411
+ // whose pair has not been touched within the grace window.
1412
+ this.manifolds.advance_frame();
1413
+ }
1414
+ }
1415
+
1416
+ /**
1417
+ * @readonly
1418
+ * @type {boolean}
1419
+ */
1420
+ PhysicsSystem.prototype.isPhysicsSystem = true;
1421
+
1422
+ // Re-export for convenience.
1423
+ export { BodyKind, RigidBodyFlags };