@woosh/meep-engine 2.139.0 → 2.140.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 (172) 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_to_matrix3.d.ts +54 -0
  9. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
  10. package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
  11. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
  12. package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
  13. package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
  14. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
  15. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
  16. package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
  17. package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
  18. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
  19. package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
  20. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  21. package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
  22. package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
  23. package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
  24. package/src/core/geom/3d/shape/Triangle3D.js +318 -0
  25. package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
  26. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
  27. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
  28. package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
  29. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
  30. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
  31. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
  32. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
  33. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
  34. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
  35. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
  36. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
  37. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
  38. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
  39. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
  40. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
  41. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
  42. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
  43. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
  44. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
  45. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
  46. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
  47. package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
  48. package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
  49. package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
  50. package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
  51. package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
  52. package/src/core/geom/vec3/v3_negate_array.js +2 -2
  53. package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
  54. package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
  55. package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
  56. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
  57. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
  58. package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
  59. package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
  60. package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
  61. package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
  62. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
  63. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  64. package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
  65. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
  66. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  67. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
  68. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
  69. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  70. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
  71. package/src/engine/control/first-person/TODO.md +173 -127
  72. package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
  73. package/src/engine/control/first-person/abilities/Slide.js +9 -1
  74. package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
  75. package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
  76. package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
  77. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
  78. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
  79. package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
  80. package/src/engine/physics/BULLET_REVIEW.md +945 -0
  81. package/src/engine/physics/CANNON_REVIEW.md +1300 -0
  82. package/src/engine/physics/JOLT_REVIEW.md +913 -0
  83. package/src/engine/physics/PLAN.md +461 -236
  84. package/src/engine/physics/RAPIER_REVIEW.md +934 -0
  85. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
  86. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
  87. package/src/engine/physics/contact/ManifoldStore.d.ts +83 -10
  88. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  89. package/src/engine/physics/contact/ManifoldStore.js +608 -499
  90. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
  91. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
  92. package/src/engine/physics/ecs/PhysicsSystem.d.ts +128 -20
  93. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  94. package/src/engine/physics/ecs/PhysicsSystem.js +1301 -1159
  95. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  96. package/src/engine/physics/fluid/FluidSimulator.js +2 -1
  97. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +28 -6
  98. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  99. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +39 -17
  100. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
  101. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
  102. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
  103. package/src/engine/physics/gjk/gjk.d.ts +28 -2
  104. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  105. package/src/engine/physics/gjk/gjk.js +421 -378
  106. package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
  107. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
  108. package/src/engine/physics/gjk/minkowski_support.js +75 -0
  109. package/src/engine/physics/gjk/mpr.d.ts +56 -0
  110. package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
  111. package/src/engine/physics/gjk/mpr.js +344 -0
  112. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
  113. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  114. package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
  115. package/src/engine/physics/integration/integrate_position.d.ts +25 -7
  116. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  117. package/src/engine/physics/integration/integrate_position.js +43 -12
  118. package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
  119. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  120. package/src/engine/physics/integration/integrate_velocity.js +82 -1
  121. package/src/engine/physics/narrowphase/PosedShape.d.ts +0 -8
  122. package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
  123. package/src/engine/physics/narrowphase/PosedShape.js +28 -30
  124. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  125. package/src/engine/physics/narrowphase/box_box_manifold.js +113 -17
  126. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
  127. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
  128. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
  129. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  130. package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
  131. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
  132. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
  133. package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
  134. package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
  135. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
  136. package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
  137. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
  138. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
  139. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
  140. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
  141. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
  142. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
  143. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
  144. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
  145. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
  146. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
  147. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
  148. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
  149. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
  150. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
  151. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
  152. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +8 -2
  153. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  154. package/src/engine/physics/narrowphase/narrowphase_step.js +1422 -382
  155. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  156. package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
  157. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
  158. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
  159. package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
  160. package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
  161. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
  162. package/src/engine/physics/queries/overlap_shape.js +183 -0
  163. package/src/engine/physics/queries/shape_cast.d.ts +56 -0
  164. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
  165. package/src/engine/physics/queries/shape_cast.js +387 -0
  166. package/src/engine/physics/solver/solve_contacts.d.ts +116 -30
  167. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  168. package/src/engine/physics/solver/solve_contacts.js +641 -223
  169. package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
  170. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
  171. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
  172. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
@@ -2,8 +2,8 @@
2
2
  * Apply the world-frame inverse inertia tensor of a rigid body to a world-space
3
3
  * vector and write the result.
4
4
  *
5
- * Body-frame inverse inertia is stored as a diagonal in principal axes
6
- * (`rb.inverseInertiaLocal`). World inverse inertia is
5
+ * Body-frame inverse inertia is supplied as a diagonal in principal axes
6
+ * (a Vector3-like with `.x .y .z`). World inverse inertia is
7
7
  * `I_w⁻¹ = R · diag · R^T` where `R` is the body's orientation. We compute
8
8
  * `result = R · diag · R^T · v` without materialising the full 3x3 matrix:
9
9
  *
@@ -17,21 +17,26 @@
17
17
  * Sphere short-circuit: if the inverse inertia is isotropic (ix === iy === iz),
18
18
  * the rotation cancels and `result = ix * v`. Skip the trig and save the work.
19
19
  *
20
+ * Decoupled from RigidBody / Transform on purpose — callers pass only what
21
+ * is actually read. Lets `compute_penetration` and other standalone
22
+ * geometry helpers reuse this without manufacturing a body.
23
+ *
20
24
  * @param {number[]|Float64Array} result length >= 3
21
25
  * @param {number} result_offset
22
- * @param {RigidBody} rb
23
- * @param {Transform} transform
26
+ * @param {{x: number, y: number, z: number}} inv_inertia_local
27
+ * Body-frame inverse inertia diagonal (typically `rb.inverseInertiaLocal`).
28
+ * @param {{x: number, y: number, z: number, w: number}} rotation
29
+ * Body orientation as a unit quaternion (typically `transform.rotation`).
24
30
  * @param {number} vx
25
31
  * @param {number} vy
26
32
  * @param {number} vz
27
33
  */
28
34
  export function world_inverse_inertia_apply(
29
35
  result, result_offset,
30
- rb, transform,
36
+ inv_inertia_local, rotation,
31
37
  vx, vy, vz
32
38
  ) {
33
- const ii = rb.inverseInertiaLocal;
34
- const ix = ii.x, iy = ii.y, iz = ii.z;
39
+ const ix = inv_inertia_local.x, iy = inv_inertia_local.y, iz = inv_inertia_local.z;
35
40
 
36
41
  if (ix === iy && iy === iz) {
37
42
  // Isotropic — rotation cancels.
@@ -41,39 +46,32 @@ export function world_inverse_inertia_apply(
41
46
  return;
42
47
  }
43
48
 
44
- const q = transform.rotation;
45
- const qx = q.x, qy = q.y, qz = q.z, qw = q.w;
49
+ const qx = rotation.x, qy = rotation.y, qz = rotation.z, qw = rotation.w;
46
50
 
47
51
  // Step 1: v_body = R^T · v. R^T corresponds to applying the conjugate
48
- // quaternion (-qx, -qy, -qz, qw).
49
- // Identity: v' = q · v · q*. For inverse rotation use q* on either side.
50
- // Equivalent expanded form:
51
- // t = (q* · v_quat) where v_quat = (vx, vy, vz, 0)
52
- // v_body = (t · q).xyz
53
- {
54
- const cx = -qx, cy = -qy, cz = -qz;
55
- // t = c · v (Hamilton product, treating v as pure quaternion w=0)
56
- const tx = qw * vx + cy * vz - cz * vy;
57
- const ty = qw * vy + cz * vx - cx * vz;
58
- const tz = qw * vz + cx * vy - cy * vx;
59
- const tw = -cx * vx - cy * vy - cz * vz;
60
- // v_body = t · (c*) where c* = (qx, qy, qz, qw)
61
- const vbx = tx * qw + tw * qx + ty * qz - tz * qy;
62
- const vby = ty * qw + tw * qy + tz * qx - tx * qz;
63
- const vbz = tz * qw + tw * qz + tx * qy - ty * qx;
52
+ // quaternion (-qx, -qy, -qz, qw). Inlined for V8 inliner — see
53
+ // PosedShape.support and core/geom/vec3/v3_quat3_apply_inverse.js
54
+ // for the canonical form.
55
+ const cx = -qx, cy = -qy, cz = -qz;
56
+ const tx = qw * vx + cy * vz - cz * vy;
57
+ const ty = qw * vy + cz * vx - cx * vz;
58
+ const tz = qw * vz + cx * vy - cy * vx;
59
+ const tw = -cx * vx - cy * vy - cz * vz;
60
+ const vbx = tx * qw + tw * qx + ty * qz - tz * qy;
61
+ const vby = ty * qw + tw * qy + tz * qx - tx * qz;
62
+ const vbz = tz * qw + tw * qz + tx * qy - ty * qx;
64
63
 
65
- // Step 2: scale by diagonal.
66
- const sx = vbx * ix;
67
- const sy = vby * iy;
68
- const sz = vbz * iz;
64
+ // Step 2: scale by the principal-frame diagonal.
65
+ const sx = vbx * ix;
66
+ const sy = vby * iy;
67
+ const sz = vbz * iz;
69
68
 
70
- // Step 3: result = R · scaled = q · scaled · q*
71
- const ux = qw * sx + qy * sz - qz * sy;
72
- const uy = qw * sy + qz * sx - qx * sz;
73
- const uz = qw * sz + qx * sy - qy * sx;
74
- const uw = -qx * sx - qy * sy - qz * sz;
75
- result[result_offset] = ux * qw + uw * (-qx) + uy * (-qz) - uz * (-qy);
76
- result[result_offset + 1] = uy * qw + uw * (-qy) + uz * (-qx) - ux * (-qz);
77
- result[result_offset + 2] = uz * qw + uw * (-qz) + ux * (-qy) - uy * (-qx);
78
- }
69
+ // Step 3: result = R · scaled = q · scaled · q*. Inlined likewise.
70
+ const ux = qw * sx + qy * sz - qz * sy;
71
+ const uy = qw * sy + qz * sx - qx * sz;
72
+ const uz = qw * sz + qx * sy - qy * sx;
73
+ const uw = -qx * sx - qy * sy - qz * sz;
74
+ result[result_offset] = ux * qw + uw * (-qx) + uy * (-qz) - uz * (-qy);
75
+ result[result_offset + 1] = uy * qw + uw * (-qy) + uz * (-qx) - ux * (-qz);
76
+ result[result_offset + 2] = uz * qw + uw * (-qz) + ux * (-qy) - uy * (-qx);
79
77
  }
@@ -1,16 +1,34 @@
1
1
  /**
2
2
  * Integrate `transform.position` and `transform.rotation` from the body's
3
- * current linear and angular velocity over a step `dt`. Static bodies are not
4
- * touched.
3
+ * current linear and angular velocity over a step `dt`. Static bodies are
4
+ * not touched.
5
5
  *
6
- * KinematicPosition bodies are treated like Dynamic for the position update
7
- * if the gameplay code is driving them, their velocity is whatever the user
8
- * wrote (typically zero); the step is effectively a no-op in that case.
9
- * KinematicVelocity bodies advance under their user-set velocity.
6
+ * The trailing six arguments are the body's *pseudo-velocity*: the
7
+ * position-pass output from the constraint solver (Catto split-impulse).
8
+ * Pseudo-velocity exists only inside the step it's folded into the
9
+ * integration so the position update reflects depth correction, then
10
+ * implicitly discarded. The body's persistent `linearVelocity` /
11
+ * `angularVelocity` (which carry restitution + warm-start across steps)
12
+ * are never contaminated by it.
13
+ *
14
+ * Callers with no position-pass output should pass zeros for the six
15
+ * pseudo arguments — folding zero is free (one add per axis).
16
+ *
17
+ * KinematicPosition bodies are treated like Dynamic for the position
18
+ * update — if the gameplay code is driving them, their velocity is
19
+ * whatever the user wrote (typically zero); the step is effectively a
20
+ * no-op in that case. KinematicVelocity bodies advance under their
21
+ * user-set velocity.
10
22
  *
11
23
  * @param {RigidBody} rb
12
24
  * @param {Transform} transform
13
25
  * @param {number} dt
26
+ * @param {number} ps_lin_x pseudo-linear-velocity x (0 when no contacts)
27
+ * @param {number} ps_lin_y
28
+ * @param {number} ps_lin_z
29
+ * @param {number} ps_ang_x pseudo-angular-velocity x (0 when no contacts)
30
+ * @param {number} ps_ang_y
31
+ * @param {number} ps_ang_z
14
32
  */
15
- export function integrate_position(rb: RigidBody, transform: Transform, dt: number): void;
33
+ export function integrate_position(rb: RigidBody, transform: Transform, dt: number, ps_lin_x: number, ps_lin_y: number, ps_lin_z: number, ps_ang_x: number, ps_ang_y: number, ps_ang_z: number): void;
16
34
  //# sourceMappingURL=integrate_position.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"integrate_position.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/integration/integrate_position.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;GAaG;AACH,4EAFW,MAAM,QA8BhB"}
1
+ {"version":3,"file":"integrate_position.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/integration/integrate_position.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,4EARW,MAAM,YACN,MAAM,YACN,MAAM,YACN,MAAM,YACN,MAAM,YACN,MAAM,YACN,MAAM,QA2ChB"}
@@ -5,19 +5,41 @@ const scratch_q = new Float64Array(4);
5
5
 
6
6
  /**
7
7
  * Integrate `transform.position` and `transform.rotation` from the body's
8
- * current linear and angular velocity over a step `dt`. Static bodies are not
9
- * touched.
8
+ * current linear and angular velocity over a step `dt`. Static bodies are
9
+ * not touched.
10
10
  *
11
- * KinematicPosition bodies are treated like Dynamic for the position update
12
- * if the gameplay code is driving them, their velocity is whatever the user
13
- * wrote (typically zero); the step is effectively a no-op in that case.
14
- * KinematicVelocity bodies advance under their user-set velocity.
11
+ * The trailing six arguments are the body's *pseudo-velocity*: the
12
+ * position-pass output from the constraint solver (Catto split-impulse).
13
+ * Pseudo-velocity exists only inside the step it's folded into the
14
+ * integration so the position update reflects depth correction, then
15
+ * implicitly discarded. The body's persistent `linearVelocity` /
16
+ * `angularVelocity` (which carry restitution + warm-start across steps)
17
+ * are never contaminated by it.
18
+ *
19
+ * Callers with no position-pass output should pass zeros for the six
20
+ * pseudo arguments — folding zero is free (one add per axis).
21
+ *
22
+ * KinematicPosition bodies are treated like Dynamic for the position
23
+ * update — if the gameplay code is driving them, their velocity is
24
+ * whatever the user wrote (typically zero); the step is effectively a
25
+ * no-op in that case. KinematicVelocity bodies advance under their
26
+ * user-set velocity.
15
27
  *
16
28
  * @param {RigidBody} rb
17
29
  * @param {Transform} transform
18
30
  * @param {number} dt
31
+ * @param {number} ps_lin_x pseudo-linear-velocity x (0 when no contacts)
32
+ * @param {number} ps_lin_y
33
+ * @param {number} ps_lin_z
34
+ * @param {number} ps_ang_x pseudo-angular-velocity x (0 when no contacts)
35
+ * @param {number} ps_ang_y
36
+ * @param {number} ps_ang_z
19
37
  */
20
- export function integrate_position(rb, transform, dt) {
38
+ export function integrate_position(
39
+ rb, transform, dt,
40
+ ps_lin_x, ps_lin_y, ps_lin_z,
41
+ ps_ang_x, ps_ang_y, ps_ang_z,
42
+ ) {
21
43
  if (rb.kind === BodyKind.Static) {
22
44
  return;
23
45
  }
@@ -25,22 +47,31 @@ export function integrate_position(rb, transform, dt) {
25
47
  const lv = rb.linearVelocity;
26
48
  const p = transform.position;
27
49
 
50
+ // Combined integration velocity: persistent + pseudo. The pseudo
51
+ // contribution exists only for this step and never lands in `lv`.
52
+ const vx = lv[0] + ps_lin_x;
53
+ const vy = lv[1] + ps_lin_y;
54
+ const vz = lv[2] + ps_lin_z;
55
+
28
56
  // Direct reads via typed-array index; the write goes through .set() so
29
57
  // Transform's onChanged subscribers (matrix recompose, parent/child sync,
30
58
  // viewport position, fog-of-war reveal) fire once per body per step.
31
59
  p.set(
32
- p[0] + lv[0] * dt,
33
- p[1] + lv[1] * dt,
34
- p[2] + lv[2] * dt
60
+ p[0] + vx * dt,
61
+ p[1] + vy * dt,
62
+ p[2] + vz * dt
35
63
  );
36
64
 
37
65
  const av = rb.angularVelocity;
38
- if (av[0] !== 0 || av[1] !== 0 || av[2] !== 0) {
66
+ const wx = av[0] + ps_ang_x;
67
+ const wy = av[1] + ps_ang_y;
68
+ const wz = av[2] + ps_ang_z;
69
+ if (wx !== 0 || wy !== 0 || wz !== 0) {
39
70
  const q = transform.rotation;
40
71
  quat_integrate(
41
72
  scratch_q,
42
73
  q[0], q[1], q[2], q[3],
43
- av[0], av[1], av[2],
74
+ wx, wy, wz,
44
75
  dt
45
76
  );
46
77
  q.set(scratch_q[0], scratch_q[1], scratch_q[2], scratch_q[3]);
@@ -22,4 +22,34 @@
22
22
  * @param {number} dt step size in seconds
23
23
  */
24
24
  export function integrate_velocity(rb: RigidBody, transform: Transform, gx: number, gy: number, gz: number, dt: number): void;
25
+ /**
26
+ * Apply the accumulated force / torque to velocity over a step `dt`, then
27
+ * zero the accumulators. Gravity and damping are NOT applied here.
28
+ *
29
+ * The "once per outer step" half of the TGS integrator: a user force is a
30
+ * per-frame budget that must land in full exactly once, independent of
31
+ * substep count. Gravity is the continuous field re-applied every substep
32
+ * by {@link integrate_velocity_gravity}.
33
+ *
34
+ * @param {RigidBody} rb
35
+ * @param {Transform} transform used to rotate body-frame inertia into world
36
+ * @param {number} dt full outer-step size in seconds
37
+ */
38
+ export function integrate_velocity_forces(rb: RigidBody, transform: Transform, dt: number): void;
39
+ /**
40
+ * Apply gravity and velocity damping over a (sub)step `dt`. Does NOT touch
41
+ * the force / torque accumulators (consumed once by
42
+ * {@link integrate_velocity_forces}).
43
+ *
44
+ * The "every substep" half of the TGS integrator. Per substep the body sees
45
+ * only `g·h` of gravity, which the per-substep contact warm-start + solve
46
+ * cancels exactly — keeping a resting stack at zero velocity. Damping is the
47
+ * stable implicit decay `v *= 1/(1 + d·dt)` applied at the substep `dt`.
48
+ *
49
+ * @param {RigidBody} rb
50
+ * @param {Transform} transform (unused; signature symmetry with the family)
51
+ * @param {number} gx @param {number} gy @param {number} gz world gravity
52
+ * @param {number} dt sub-step size in seconds
53
+ */
54
+ export function integrate_velocity_gravity(rb: RigidBody, transform: Transform, gx: number, gy: number, gz: number, dt: number): void;
25
55
  //# sourceMappingURL=integrate_velocity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"integrate_velocity.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/integration/integrate_velocity.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,4EALW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,QAoDhB"}
1
+ {"version":3,"file":"integrate_velocity.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/integration/integrate_velocity.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,4EALW,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,QAoDhB;AAED;;;;;;;;;;;;GAYG;AACH,mFAFW,MAAM,QA6BhB;AAED;;;;;;;;;;;;;;GAcG;AACH,oFAHW,MAAM,MAAa,MAAM,MAAa,MAAM,MAC5C,MAAM,QAwBhB"}
@@ -60,7 +60,7 @@ export function integrate_velocity(rb, transform, gx, gy, gz, dt) {
60
60
  let avy = av[1];
61
61
  let avz = av[2];
62
62
  if (at[0] !== 0 || at[1] !== 0 || at[2] !== 0) {
63
- world_inverse_inertia_apply(scratch_angular, 0, rb, transform, at[0], at[1], at[2]);
63
+ world_inverse_inertia_apply(scratch_angular, 0, rb.inverseInertiaLocal, transform.rotation, at[0], at[1], at[2]);
64
64
  avx += scratch_angular[0] * dt;
65
65
  avy += scratch_angular[1] * dt;
66
66
  avz += scratch_angular[2] * dt;
@@ -77,3 +77,84 @@ export function integrate_velocity(rb, transform, gx, gy, gz, dt) {
77
77
  af[0] = 0; af[1] = 0; af[2] = 0;
78
78
  at[0] = 0; at[1] = 0; at[2] = 0;
79
79
  }
80
+
81
+ /**
82
+ * Apply the accumulated force / torque to velocity over a step `dt`, then
83
+ * zero the accumulators. Gravity and damping are NOT applied here.
84
+ *
85
+ * The "once per outer step" half of the TGS integrator: a user force is a
86
+ * per-frame budget that must land in full exactly once, independent of
87
+ * substep count. Gravity is the continuous field re-applied every substep
88
+ * by {@link integrate_velocity_gravity}.
89
+ *
90
+ * @param {RigidBody} rb
91
+ * @param {Transform} transform used to rotate body-frame inertia into world
92
+ * @param {number} dt full outer-step size in seconds
93
+ */
94
+ export function integrate_velocity_forces(rb, transform, dt) {
95
+ const af = rb.accumulatedForce;
96
+ const at = rb.accumulatedTorque;
97
+
98
+ if (rb.kind !== BodyKind.Dynamic) {
99
+ af[0] = 0; af[1] = 0; af[2] = 0;
100
+ at[0] = 0; at[1] = 0; at[2] = 0;
101
+ return;
102
+ }
103
+
104
+ const inv_m = rb.mass > 0 ? 1 / rb.mass : 0;
105
+
106
+ const lv = rb.linearVelocity;
107
+ lv[0] += af[0] * inv_m * dt;
108
+ lv[1] += af[1] * inv_m * dt;
109
+ lv[2] += af[2] * inv_m * dt;
110
+
111
+ if (at[0] !== 0 || at[1] !== 0 || at[2] !== 0) {
112
+ world_inverse_inertia_apply(scratch_angular, 0, rb.inverseInertiaLocal, transform.rotation, at[0], at[1], at[2]);
113
+ const av = rb.angularVelocity;
114
+ av[0] += scratch_angular[0] * dt;
115
+ av[1] += scratch_angular[1] * dt;
116
+ av[2] += scratch_angular[2] * dt;
117
+ }
118
+
119
+ af[0] = 0; af[1] = 0; af[2] = 0;
120
+ at[0] = 0; at[1] = 0; at[2] = 0;
121
+ }
122
+
123
+ /**
124
+ * Apply gravity and velocity damping over a (sub)step `dt`. Does NOT touch
125
+ * the force / torque accumulators (consumed once by
126
+ * {@link integrate_velocity_forces}).
127
+ *
128
+ * The "every substep" half of the TGS integrator. Per substep the body sees
129
+ * only `g·h` of gravity, which the per-substep contact warm-start + solve
130
+ * cancels exactly — keeping a resting stack at zero velocity. Damping is the
131
+ * stable implicit decay `v *= 1/(1 + d·dt)` applied at the substep `dt`.
132
+ *
133
+ * @param {RigidBody} rb
134
+ * @param {Transform} transform (unused; signature symmetry with the family)
135
+ * @param {number} gx @param {number} gy @param {number} gz world gravity
136
+ * @param {number} dt sub-step size in seconds
137
+ */
138
+ export function integrate_velocity_gravity(rb, transform, gx, gy, gz, dt) {
139
+ if (rb.kind !== BodyKind.Dynamic) {
140
+ return;
141
+ }
142
+
143
+ const gs = rb.gravityScale;
144
+ const lv = rb.linearVelocity;
145
+ let lvx = lv[0] + gx * gs * dt;
146
+ let lvy = lv[1] + gy * gs * dt;
147
+ let lvz = lv[2] + gz * gs * dt;
148
+
149
+ if (rb.linearDamping > 0) {
150
+ const k = 1 / (1 + rb.linearDamping * dt);
151
+ lvx *= k; lvy *= k; lvz *= k;
152
+ }
153
+ lv[0] = lvx; lv[1] = lvy; lv[2] = lvz;
154
+
155
+ if (rb.angularDamping > 0) {
156
+ const av = rb.angularVelocity;
157
+ const k = 1 / (1 + rb.angularDamping * dt);
158
+ av[0] *= k; av[1] *= k; av[2] *= k;
159
+ }
160
+ }
@@ -45,14 +45,6 @@ export class PosedShape extends AbstractShape3D {
45
45
  z: number;
46
46
  w: number;
47
47
  }): void;
48
- /**
49
- * @param {number[]|Float32Array|Float64Array} result
50
- * @param {number} result_offset
51
- * @param {number} dx world-space query direction x (unit)
52
- * @param {number} dy world-space query direction y
53
- * @param {number} dz world-space query direction z
54
- */
55
- support(result: number[] | Float32Array | Float64Array, result_offset: number, dx: number, dy: number, dz: number): void;
56
48
  compute_bounding_box(result: any): void;
57
49
  }
58
50
  import { AbstractShape3D } from "../../../core/geom/3d/shape/AbstractShape3D.js";
@@ -1 +1 @@
1
- {"version":3,"file":"PosedShape.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/PosedShape.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAKQ,mCAAmC;IACnC,OADW,eAAe,GAAC,IAAI,CACd;IAEjB,WAAW;IAAE,WAAW;IAAE,WAAW;IACrC,WAAW;IAAE,WAAW;IAAE,WAAW;IAAE,WAAW;IAGtD;;;;;;;OAOG;IACH,aAJW,eAAe,YACf,UAAQ;QAAC,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAA;KAAC,YACpC,aAAW;QAAC,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAA;KAAC,QAM1D;IAED;;;;;;OAMG;IACH,gBANW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,iBAClC,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,QAyChB;IAED,wCASC;CACJ;gCA7G+B,gDAAgD"}
1
+ {"version":3,"file":"PosedShape.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/PosedShape.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAKQ,mCAAmC;IACnC,OADW,eAAe,GAAC,IAAI,CACd;IAEjB,WAAW;IAAE,WAAW;IAAE,WAAW;IACrC,WAAW;IAAE,WAAW;IAAE,WAAW;IAAE,WAAW;IAGtD;;;;;;;OAOG;IACH,aAJW,eAAe,YACf,UAAQ;QAAC,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAA;KAAC,YACpC,aAAW;QAAC,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAA;KAAC,QAM1D;IAgDD,wCASC;CACJ;gCA1G+B,gDAAgD"}
@@ -1,5 +1,5 @@
1
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
1
2
  import { AbstractShape3D } from "../../../core/geom/3d/shape/AbstractShape3D.js";
2
- import { aabb_transform_oriented } from "../broadphase/aabb_transform_oriented.js";
3
3
 
4
4
  const scratch_local = new Float64Array(3);
5
5
  const scratch_local_aabb = new Float64Array(6);
@@ -57,49 +57,47 @@ export class PosedShape extends AbstractShape3D {
57
57
  * @param {number} dz world-space query direction z
58
58
  */
59
59
  support(result, result_offset, dx, dy, dz) {
60
- // Rotate world direction into body frame using the conjugate quaternion.
61
- // Conjugate: (-qx, -qy, -qz, qw). v_local = q* · v_world · q
60
+ // Inline q·v·q* math: PosedShape.support is the hottest GJK call
61
+ // (twice per iteration of the body-level fallback, once per
62
+ // triangle for concave dispatch). V8 declines to inline
63
+ // v3_quat3_apply at 9-arg call sites, so the math is repeated
64
+ // inline here at the cost of ~7 extra lines per direction.
65
+ // See core/geom/vec3/v3_quat3_apply.js for the canonical form
66
+ // — this is a literal expansion of it.
62
67
  const qx = this.qx, qy = this.qy, qz = this.qz, qw = this.qw;
63
68
 
64
- // Apply conjugate quaternion to dir: same formula as Vector3.applyQuaternion
65
- // but with (qx,qy,qz) negated.
66
- const cx = -qx, cy = -qy, cz = -qz;
69
+ // Rotate world direction into body frame via the conjugate
70
+ // quaternion: v_local = q* · v_world · q. Conjugate xyz are
71
+ // negated (cx, cy, cz below).
72
+ const tdx = qw * dx - qy * dz + qz * dy;
73
+ const tdy = qw * dy - qz * dx + qx * dz;
74
+ const tdz = qw * dz - qx * dy + qy * dx;
75
+ const tdw = qx * dx + qy * dy + qz * dz;
67
76
 
68
- // t = c · v
69
- const tx = qw * dx + cy * dz - cz * dy;
70
- const ty = qw * dy + cz * dx - cx * dz;
71
- const tz = qw * dz + cx * dy - cy * dx;
72
- const tw = -cx * dx - cy * dy - cz * dz;
73
-
74
- // result_local = t · c^-1; conjugate of c is (-cx,-cy,-cz,qw) = (qx,qy,qz,qw) = original q
75
- const ldx = tx * qw + tw * (-cx) + ty * (-cz) - tz * (-cy);
76
- const ldy = ty * qw + tw * (-cy) + tz * (-cx) - tx * (-cz);
77
- const ldz = tz * qw + tw * (-cz) + tx * (-cy) - ty * (-cx);
77
+ const ldx = tdx * qw + tdw * qx + tdy * qz - tdz * qy;
78
+ const ldy = tdy * qw + tdw * qy + tdz * qx - tdx * qz;
79
+ const ldz = tdz * qw + tdw * qz + tdx * qy - tdy * qx;
78
80
 
79
81
  // Ask the local shape for its support point in body-local space.
80
82
  this.shape.support(scratch_local, 0, ldx, ldy, ldz);
81
83
 
82
- // Rotate result back to world frame using original quaternion.
84
+ // Rotate the body-local support point back to world: v_world =
85
+ // q · v_local · q*.
83
86
  const lsx = scratch_local[0], lsy = scratch_local[1], lsz = scratch_local[2];
84
87
 
85
- // v_world = q · v_local · q*
86
- const ix = qw * lsx + qy * lsz - qz * lsy;
87
- const iy = qw * lsy + qz * lsx - qx * lsz;
88
- const iz = qw * lsz + qx * lsy - qy * lsx;
89
- const iw = -qx * lsx - qy * lsy - qz * lsz;
90
-
91
- const wx = ix * qw + iw * (-qx) + iy * (-qz) - iz * (-qy);
92
- const wy = iy * qw + iw * (-qy) + iz * (-qx) - ix * (-qz);
93
- const wz = iz * qw + iw * (-qz) + ix * (-qy) - iy * (-qx);
88
+ const tsx = qw * lsx + qy * lsz - qz * lsy;
89
+ const tsy = qw * lsy + qz * lsx - qx * lsz;
90
+ const tsz = qw * lsz + qx * lsy - qy * lsx;
91
+ const tsw = -qx * lsx - qy * lsy - qz * lsz;
94
92
 
95
- result[result_offset] = wx + this.px;
96
- result[result_offset + 1] = wy + this.py;
97
- result[result_offset + 2] = wz + this.pz;
93
+ result[result_offset] = tsx * qw - tsw * qx - tsy * qz + tsz * qy + this.px;
94
+ result[result_offset + 1] = tsy * qw - tsw * qy - tsz * qx + tsx * qz + this.py;
95
+ result[result_offset + 2] = tsz * qw - tsw * qz - tsx * qy + tsy * qx + this.pz;
98
96
  }
99
97
 
100
98
  compute_bounding_box(result) {
101
99
  this.shape.compute_bounding_box(scratch_local_aabb);
102
- aabb_transform_oriented(
100
+ aabb3_transform_oriented(
103
101
  result, 0,
104
102
  scratch_local_aabb[0], scratch_local_aabb[1], scratch_local_aabb[2],
105
103
  scratch_local_aabb[3], scratch_local_aabb[4], scratch_local_aabb[5],
@@ -1 +1 @@
1
- {"version":3,"file":"box_box_manifold.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/box_box_manifold.js"],"names":[],"mappings":"AAiIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,sCAvBW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,GACJ,OAAO,CAwUnB;AA1bD;;;GAGG;AACH,iCAFU,MAAM,CAEoD"}
1
+ {"version":3,"file":"box_box_manifold.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/box_box_manifold.js"],"names":[],"mappings":"AA+HA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,sCAvBW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,GACJ,OAAO,CA0anB;AAvhBD;;;GAGG;AACH,iCAFU,MAAM,CAEoD"}
@@ -1,3 +1,6 @@
1
+ import { line3_closest_points_segment_segment } from "../../../core/geom/3d/line/line3_closest_points_segment_segment.js";
2
+ import { quat3_to_matrix3 } from "../../../core/geom/3d/quaternion/quat3_to_matrix3.js";
3
+
1
4
  /**
2
5
  * Multi-point manifold construction for two oriented bounding boxes.
3
6
  *
@@ -61,16 +64,11 @@ const clipped_uv_out = new Float64Array(8 * 2);
61
64
  // Stride 4 per candidate: x, y, z, depth.
62
65
  const candidates = new Float64Array(8 * 4);
63
66
 
64
- // --- helpers ---------------------------------------------------------------
67
+ // Output of line3_closest_points_segment_segment for the edge-cross
68
+ // SAT-winner path: (s, t) parameters along the two edges.
69
+ const scratch_edge_st = new Float64Array(2);
65
70
 
66
- function quat_to_axes(out, off, qx, qy, qz, qw) {
67
- const xx = qx * qx, yy = qy * qy, zz = qz * qz;
68
- const xy = qx * qy, xz = qx * qz, yz = qy * qz;
69
- const wx = qw * qx, wy = qw * qy, wz = qw * qz;
70
- out[off] = 1 - 2 * (yy + zz); out[off + 1] = 2 * (xy + wz); out[off + 2] = 2 * (xz - wy);
71
- out[off + 3] = 2 * (xy - wz); out[off + 4] = 1 - 2 * (xx + zz); out[off + 5] = 2 * (yz + wx);
72
- out[off + 6] = 2 * (xz + wy); out[off + 7] = 2 * (yz - wx); out[off + 8] = 1 - 2 * (xx + yy);
73
- }
71
+ // --- helpers ---------------------------------------------------------------
74
72
 
75
73
  function projected_half_extent(axes, off, hx, hy, hz, lx, ly, lz) {
76
74
  const px = lx * axes[off] + ly * axes[off + 1] + lz * axes[off + 2];
@@ -157,8 +155,8 @@ export function box_box_manifold(
157
155
  ax, ay, az, aqx, aqy, aqz, aqw, ahx, ahy, ahz,
158
156
  bx, by, bz, bqx, bqy, bqz, bqw, bhx, bhy, bhz
159
157
  ) {
160
- quat_to_axes(scratch_axes_a, 0, aqx, aqy, aqz, aqw);
161
- quat_to_axes(scratch_axes_b, 0, bqx, bqy, bqz, bqw);
158
+ quat3_to_matrix3(scratch_axes_a, 0, aqx, aqy, aqz, aqw);
159
+ quat3_to_matrix3(scratch_axes_b, 0, bqx, bqy, bqz, bqw);
162
160
 
163
161
  const ta = scratch_axes_a;
164
162
  const tb = scratch_axes_b;
@@ -217,14 +215,112 @@ export function box_box_manifold(
217
215
  const nx = best_nx, ny = best_ny, nz = best_nz;
218
216
  out[0] = nx; out[1] = ny; out[2] = nz;
219
217
 
220
- // ---- Edge-cross axis: single-point fallback ----
218
+ // ---- Edge-cross axis: closest-pair on the two involved edges ----
219
+ //
220
+ // When SAT identifies an edge-edge separating axis (source 6..14 =
221
+ // 6 + i*3 + j, where `i` is box A's local edge-axis index and
222
+ // `j` is box B's), the contact happens where one specific edge of
223
+ // A crosses (or is closest to) one specific edge of B. The earlier
224
+ // body-centre-midpoint fallback was sufficient for "we know they
225
+ // collide" but produced lever arms that confused the solver — for
226
+ // skewed-box stacks this led to slow drift.
227
+ //
228
+ // Procedure:
229
+ // 1. Decode (i, j) from `best_source`.
230
+ // 2. Among box A's 4 parallel edges along axis i, pick the one
231
+ // whose corner is most in the −n direction (closest to B).
232
+ // 3. Same for box B with +n direction.
233
+ // 4. `line3_closest_points_segment_segment` returns (s, t)
234
+ // parameters; reconstruct the world contact on each edge.
221
235
  if (best_source >= 6) {
222
- const mx = (ax + bx) * 0.5;
223
- const my = (ay + by) * 0.5;
224
- const mz = (az + bz) * 0.5;
236
+ const ax_src = best_source - 6;
237
+ const i_a = (ax_src / 3) | 0;
238
+ const j_b = ax_src - i_a * 3;
239
+
240
+ // Edge directions in world.
241
+ const edge_a_dx = ta[i_a * 3];
242
+ const edge_a_dy = ta[i_a * 3 + 1];
243
+ const edge_a_dz = ta[i_a * 3 + 2];
244
+ const edge_b_dx = tb[j_b * 3];
245
+ const edge_b_dy = tb[j_b * 3 + 1];
246
+ const edge_b_dz = tb[j_b * 3 + 2];
247
+
248
+ // Edge half-extents (along the chosen axis).
249
+ const edge_a_half = i_a === 0 ? ahx : (i_a === 1 ? ahy : ahz);
250
+ const edge_b_half = j_b === 0 ? bhx : (j_b === 1 ? bhy : bhz);
251
+
252
+ // Perpendicular axes (the other two) and their half-extents.
253
+ let u_a_idx, v_a_idx;
254
+ if (i_a === 0) { u_a_idx = 1; v_a_idx = 2; }
255
+ else if (i_a === 1) { u_a_idx = 2; v_a_idx = 0; }
256
+ else { u_a_idx = 0; v_a_idx = 1; }
257
+ const u_a_x = ta[u_a_idx * 3], u_a_y = ta[u_a_idx * 3 + 1], u_a_z = ta[u_a_idx * 3 + 2];
258
+ const v_a_x = ta[v_a_idx * 3], v_a_y = ta[v_a_idx * 3 + 1], v_a_z = ta[v_a_idx * 3 + 2];
259
+ const half_u_a = u_a_idx === 0 ? ahx : (u_a_idx === 1 ? ahy : ahz);
260
+ const half_v_a = v_a_idx === 0 ? ahx : (v_a_idx === 1 ? ahy : ahz);
261
+
262
+ let u_b_idx, v_b_idx;
263
+ if (j_b === 0) { u_b_idx = 1; v_b_idx = 2; }
264
+ else if (j_b === 1) { u_b_idx = 2; v_b_idx = 0; }
265
+ else { u_b_idx = 0; v_b_idx = 1; }
266
+ const u_b_x = tb[u_b_idx * 3], u_b_y = tb[u_b_idx * 3 + 1], u_b_z = tb[u_b_idx * 3 + 2];
267
+ const v_b_x = tb[v_b_idx * 3], v_b_y = tb[v_b_idx * 3 + 1], v_b_z = tb[v_b_idx * 3 + 2];
268
+ const half_u_b = u_b_idx === 0 ? bhx : (u_b_idx === 1 ? bhy : bhz);
269
+ const half_v_b = v_b_idx === 0 ? bhx : (v_b_idx === 1 ? bhy : bhz);
270
+
271
+ // The contact normal `n` (= best_n) points B → A. From A's
272
+ // perspective B is in the −n direction; pick the A-corner most
273
+ // in that direction. From B's perspective A is in +n; pick the
274
+ // B-corner most in +n.
275
+ const minus_n_dot_u_a = -(nx * u_a_x + ny * u_a_y + nz * u_a_z);
276
+ const minus_n_dot_v_a = -(nx * v_a_x + ny * v_a_y + nz * v_a_z);
277
+ const u_sign_a = minus_n_dot_u_a >= 0 ? 1 : -1;
278
+ const v_sign_a = minus_n_dot_v_a >= 0 ? 1 : -1;
279
+
280
+ const plus_n_dot_u_b = nx * u_b_x + ny * u_b_y + nz * u_b_z;
281
+ const plus_n_dot_v_b = nx * v_b_x + ny * v_b_y + nz * v_b_z;
282
+ const u_sign_b = plus_n_dot_u_b >= 0 ? 1 : -1;
283
+ const v_sign_b = plus_n_dot_v_b >= 0 ? 1 : -1;
284
+
285
+ // Box-A edge centre = A_centre + u_sign_a * half_u_a * u_a
286
+ // + v_sign_a * half_v_a * v_a.
287
+ const corner_a_cx = ax + u_a_x * u_sign_a * half_u_a + v_a_x * v_sign_a * half_v_a;
288
+ const corner_a_cy = ay + u_a_y * u_sign_a * half_u_a + v_a_y * v_sign_a * half_v_a;
289
+ const corner_a_cz = az + u_a_z * u_sign_a * half_u_a + v_a_z * v_sign_a * half_v_a;
290
+ const edge_a_p1_x = corner_a_cx - edge_a_dx * edge_a_half;
291
+ const edge_a_p1_y = corner_a_cy - edge_a_dy * edge_a_half;
292
+ const edge_a_p1_z = corner_a_cz - edge_a_dz * edge_a_half;
293
+ const edge_a_p2_x = corner_a_cx + edge_a_dx * edge_a_half;
294
+ const edge_a_p2_y = corner_a_cy + edge_a_dy * edge_a_half;
295
+ const edge_a_p2_z = corner_a_cz + edge_a_dz * edge_a_half;
296
+
297
+ const corner_b_cx = bx + u_b_x * u_sign_b * half_u_b + v_b_x * v_sign_b * half_v_b;
298
+ const corner_b_cy = by + u_b_y * u_sign_b * half_u_b + v_b_y * v_sign_b * half_v_b;
299
+ const corner_b_cz = bz + u_b_z * u_sign_b * half_u_b + v_b_z * v_sign_b * half_v_b;
300
+ const edge_b_p1_x = corner_b_cx - edge_b_dx * edge_b_half;
301
+ const edge_b_p1_y = corner_b_cy - edge_b_dy * edge_b_half;
302
+ const edge_b_p1_z = corner_b_cz - edge_b_dz * edge_b_half;
303
+ const edge_b_p2_x = corner_b_cx + edge_b_dx * edge_b_half;
304
+ const edge_b_p2_y = corner_b_cy + edge_b_dy * edge_b_half;
305
+ const edge_b_p2_z = corner_b_cz + edge_b_dz * edge_b_half;
306
+
307
+ line3_closest_points_segment_segment(
308
+ scratch_edge_st,
309
+ edge_a_p1_x, edge_a_p1_y, edge_a_p1_z, edge_a_p2_x, edge_a_p2_y, edge_a_p2_z,
310
+ edge_b_p1_x, edge_b_p1_y, edge_b_p1_z, edge_b_p2_x, edge_b_p2_y, edge_b_p2_z
311
+ );
312
+ const s = scratch_edge_st[0];
313
+ const t = scratch_edge_st[1];
314
+ const contact_a_x = edge_a_p1_x + s * (edge_a_p2_x - edge_a_p1_x);
315
+ const contact_a_y = edge_a_p1_y + s * (edge_a_p2_y - edge_a_p1_y);
316
+ const contact_a_z = edge_a_p1_z + s * (edge_a_p2_z - edge_a_p1_z);
317
+ const contact_b_x = edge_b_p1_x + t * (edge_b_p2_x - edge_b_p1_x);
318
+ const contact_b_y = edge_b_p1_y + t * (edge_b_p2_y - edge_b_p1_y);
319
+ const contact_b_z = edge_b_p1_z + t * (edge_b_p2_z - edge_b_p1_z);
320
+
225
321
  out[3] = 1;
226
- out[4] = mx; out[5] = my; out[6] = mz;
227
- out[7] = mx; out[8] = my; out[9] = mz;
322
+ out[4] = contact_a_x; out[5] = contact_a_y; out[6] = contact_a_z;
323
+ out[7] = contact_b_x; out[8] = contact_b_y; out[9] = contact_b_z;
228
324
  out[10] = best_overlap;
229
325
  return true;
230
326
  }