@woosh/meep-engine 2.140.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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
  3. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
  4. package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
  5. package/src/engine/physics/PLAN.md +152 -35
  6. package/src/engine/physics/REVIEW_002.md +151 -0
  7. package/src/engine/physics/constraint/DofMode.d.ts +28 -0
  8. package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
  9. package/src/engine/physics/constraint/DofMode.js +35 -0
  10. package/src/engine/physics/constraint/solve_constraints.d.ts +16 -0
  11. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
  12. package/src/engine/physics/constraint/solve_constraints.js +436 -0
  13. package/src/engine/physics/ecs/Joint.d.ts +179 -0
  14. package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
  15. package/src/engine/physics/ecs/Joint.js +234 -0
  16. package/src/engine/physics/ecs/PhysicsSystem.d.ts +52 -0
  17. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  18. package/src/engine/physics/ecs/PhysicsSystem.js +126 -4
  19. package/src/engine/physics/fluid/FluidField.d.ts +14 -10
  20. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  21. package/src/engine/physics/fluid/FluidField.js +14 -10
  22. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  23. package/src/engine/physics/fluid/FluidSimulator.js +0 -1
  24. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
  25. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
  26. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
  27. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
  28. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
  29. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
  30. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
  31. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
  32. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
  33. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +24 -22
  34. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  35. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +26 -22
  36. package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
  37. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  38. package/src/engine/physics/island/IslandBuilder.js +33 -16
  39. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  40. package/src/engine/physics/narrowphase/box_box_manifold.js +27 -1
  41. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +33 -0
  42. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  43. package/src/engine/physics/narrowphase/narrowphase_step.js +75 -0
  44. package/src/engine/physics/solver/solve_contacts.d.ts +28 -0
  45. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  46. package/src/engine/physics/solver/solve_contacts.js +169 -1
@@ -1420,3 +1420,78 @@ export function narrowphase_step(pair_list, manifolds, lists) {
1420
1420
  }
1421
1421
  }
1422
1422
  }
1423
+
1424
+ /**
1425
+ * Re-detect contact GEOMETRY for one existing manifold slot at the bodies'
1426
+ * current poses, updating the witness points / normal / depth of the slot's
1427
+ * existing contacts in place. Does NOT change the contact count, the
1428
+ * feature ids, or the accumulated impulses — it only refreshes geometry.
1429
+ *
1430
+ * This is the per-substep concave path (TGS): for a contact pair involving a
1431
+ * concave body, the contact *feature* (which triangle is deepest, and its
1432
+ * normal) genuinely changes as the body rocks, so the solver's cheap analytic
1433
+ * refresh — which freezes the feature for the whole outer step — pumps energy
1434
+ * in. Re-running the narrowphase geometry each substep gives a fresh,
1435
+ * correct normal so the body settles instead of rocking. Convex pairs keep
1436
+ * the analytic refresh and never call this (their feature is stable).
1437
+ *
1438
+ * Matching is by feature id (stable per-triangle for the decomposition path),
1439
+ * so a contact's geometry tracks the same triangle across substeps. A contact
1440
+ * whose triangle isn't found this substep keeps its previous geometry (a rare
1441
+ * transient; the once-per-frame {@link narrowphase_step} re-establishes the
1442
+ * contact set next outer step). Count never changes here, so the solver's
1443
+ * per-contact scratch (sized once at prepare) stays aligned.
1444
+ *
1445
+ * @param {ManifoldStore} manifolds
1446
+ * @param {number} slot
1447
+ * @param {Array<{collider: Collider, transform: Transform}>} list_a
1448
+ * @param {Array<{collider: Collider, transform: Transform}>} list_b
1449
+ */
1450
+ export function redetect_pair_geometry(manifolds, slot, list_a, list_b) {
1451
+ if (list_a === undefined || list_b === undefined) return;
1452
+ const la_len = list_a.length;
1453
+ const lb_len = list_b.length;
1454
+ if (la_len === 0 || lb_len === 0) return;
1455
+
1456
+ const count = manifolds.contact_count(slot);
1457
+ if (count === 0) return;
1458
+
1459
+ const gjk_axis_buf = manifolds.slot_axis_buffer;
1460
+ const gjk_axis_off = manifolds.slot_axis_offset(slot);
1461
+
1462
+ let cc = 0;
1463
+ for (let a = 0; a < la_len; a++) {
1464
+ const ea = list_a[a];
1465
+ for (let b = 0; b < lb_len; b++) {
1466
+ const eb = list_b[b];
1467
+ cc = dispatch_pair(cc, ea.collider, ea.transform, eb.collider, eb.transform,
1468
+ gjk_axis_buf, gjk_axis_off);
1469
+ }
1470
+ }
1471
+ if (cc === 0) return; // nothing re-detected this substep — keep frozen geometry
1472
+
1473
+ const data = manifolds.data_buffer;
1474
+ const slot_off = manifolds.slot_data_offset(slot);
1475
+ for (let j = 0; j < count; j++) {
1476
+ const off = slot_off + j * CONTACT_STRIDE;
1477
+ const fid = data[off + 13];
1478
+ if (fid === 0) continue; // no feature info to match on
1479
+ for (let k = 0; k < cc; k++) {
1480
+ const co = k * CANDIDATE_STRIDE;
1481
+ if (candidates[co + 10] === fid) {
1482
+ // Overwrite geometry only: witnesses, normal, depth.
1483
+ data[off] = candidates[co];
1484
+ data[off + 1] = candidates[co + 1];
1485
+ data[off + 2] = candidates[co + 2];
1486
+ data[off + 3] = candidates[co + 3];
1487
+ data[off + 4] = candidates[co + 4];
1488
+ data[off + 5] = candidates[co + 5];
1489
+ data[off + 6] = candidates[co + 6];
1490
+ data[off + 7] = candidates[co + 7];
1491
+ data[off + 8] = candidates[co + 8];
1492
+ data[off + 9] = candidates[co + 9];
1493
+ break;
1494
+ }
1495
+ }
1496
+ }
1497
+ }
@@ -51,6 +51,34 @@ export function warm_start_contacts(manifolds: ManifoldStore, system: PhysicsSys
51
51
  * @param {PhysicsSystem} system
52
52
  */
53
53
  export function refresh_contacts(manifolds: ManifoldStore, system: PhysicsSystem): void;
54
+ /**
55
+ * Stage 2b (per substep) — the concave counterpart of {@link refresh_contacts}.
56
+ *
57
+ * For each concave-involved slot, re-runs the narrowphase geometry at the
58
+ * current substep pose ({@link redetect_pair_geometry}, which rewrites the
59
+ * manifold's witness / normal / depth in place), then re-derives the solver
60
+ * scratch (lever arms, tangent basis, effective masses, position bias) for
61
+ * that slot's contacts from the fresh geometry. This is the whole point of
62
+ * the concave path: a body rocking on a mesh changes which triangle (and
63
+ * normal) it rests on within a single outer step, and freezing that — as the
64
+ * convex analytic refresh does — pumps energy in. Re-detecting gives a
65
+ * correct per-substep normal so the body settles.
66
+ *
67
+ * Cost is ~one narrowphase dispatch per concave slot per substep — acceptable
68
+ * because concave-involved pairs are rare (the common concave case, a convex
69
+ * body on static terrain, is convex on the moving side and never lands here).
70
+ * The contact count is held fixed (redetect updates geometry only), so the
71
+ * scratch stays aligned with prepare.
72
+ *
73
+ * Must run before {@link refresh_contacts} / {@link warm_start_contacts} each
74
+ * substep. `rest_bias` (captured at prepare) and the `scratch_max_jn`
75
+ * restitution gate are preserved.
76
+ *
77
+ * @param {ManifoldStore} manifolds
78
+ * @param {PhysicsSystem} system reads `__body_collider_lists`, `__bodies`,
79
+ * `__transforms`.
80
+ */
81
+ export function redetect_concave_contacts(manifolds: ManifoldStore, system: PhysicsSystem): void;
54
82
  /**
55
83
  * Stage 3 (per substep) — velocity iterations enforcing pure
56
84
  * non-penetration (`vn → 0`) plus Coulomb-disk friction. No bias: depth
@@ -1 +1 @@
1
- {"version":3,"file":"solve_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/solver/solve_contacts.js"],"names":[],"mappings":"AA8ZA;;;;;;;;;;;;;GAaG;AACH,0FAHW,MAAM,GACJ,MAAM,CA4IlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,2FAuCC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wFA2DC;AAED;;;;;;;;GAQG;AACH,uFAFW,MAAM,QA2GhB;AAED;;;;;;;;;;;GAWG;AACH,yFA4DC;AAED;;;;;;;;;;;;;;GAcG;AACH,2FAFW,MAAM,QA4EhB;AAED;;;;;;;;;;;;;;;GAeG;AACH,oFAJW,MAAM,UACN,MAAM,cACN,MAAM,QAYhB;AAl7BD;;;;;GAKG;AACH,0CAFU,MAAM,CAEuB;AAEvC;;;GAGG;AACH,0CAFU,MAAM,CAEsB"}
1
+ {"version":3,"file":"solve_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/solver/solve_contacts.js"],"names":[],"mappings":"AAgcA;;;;;;;;;;;;;GAaG;AACH,0FAHW,MAAM,GACJ,MAAM,CA4JlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,2FAuCC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wFAgEC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,iGAmFC;AAED;;;;;;;;GAQG;AACH,uFAFW,MAAM,QA2GhB;AAED;;;;;;;;;;;GAWG;AACH,yFA4DC;AAED;;;;;;;;;;;;;;GAcG;AACH,2FAFW,MAAM,QA4EhB;AAED;;;;;;;;;;;;;;;GAeG;AACH,oFAJW,MAAM,UACN,MAAM,cACN,MAAM,QAahB;AAxlCD;;;;;GAKG;AACH,0CAFU,MAAM,CAEuB;AAEvC;;;GAGG;AACH,0CAFU,MAAM,CAEsB"}
@@ -5,6 +5,7 @@ import { RigidBodyFlags } from "../ecs/RigidBodyFlags.js";
5
5
  import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
6
6
  import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
7
7
  import { v3_quat3_apply_inverse } from "../../../core/geom/vec3/v3_quat3_apply_inverse.js";
8
+ import { redetect_pair_geometry } from "../narrowphase/narrowphase_step.js";
8
9
  import { friction_cone_clamp } from "./friction_cone.js";
9
10
 
10
11
  /**
@@ -18,7 +19,8 @@ import { friction_cone_clamp } from "./friction_cone.js";
18
19
  * prepare_contacts(manifolds, system, h) // once per outer step
19
20
  * for each substep:
20
21
  * (system integrates gravity by h)
21
- * refresh_contacts(manifolds, system) // re-derive geometry
22
+ * redetect_concave_contacts(manifolds, system) // concave: fresh narrowphase
23
+ * refresh_contacts(manifolds, system) // convex: analytic re-derive
22
24
  * warm_start_contacts(manifolds, system) // replay impulse — per substep!
23
25
  * solve_velocity(manifolds, system, iters) // non-penetration + friction
24
26
  * solve_position(manifolds, system, pos_iters)
@@ -177,6 +179,27 @@ let scratch_pos_jn = new Float64Array(64);
177
179
  */
178
180
  let scratch_max_jn = new Float64Array(64);
179
181
 
182
+ /**
183
+ * Per-contact flag: 1 if the contact's pair involves a concave body, else 0.
184
+ * Concave contacts take the per-substep re-detection path
185
+ * ({@link redetect_concave_contacts}) — their feature genuinely changes as
186
+ * the body rocks, so the analytic refresh that freezes it would pump energy.
187
+ * Convex contacts (flag 0) take the cheap analytic {@link refresh_contacts}.
188
+ * @type {Uint8Array}
189
+ */
190
+ let scratch_concave = new Uint8Array(64);
191
+
192
+ /**
193
+ * Distinct concave-involved manifold slots touched this step, with the body
194
+ * indices needed to fetch their collider lists for re-detection. Parallel
195
+ * arrays, `g_concave_slot_count` valid entries, filled by
196
+ * {@link prepare_contacts}.
197
+ */
198
+ let concave_slot = new Uint32Array(32);
199
+ let concave_slot_idxA = new Uint32Array(32);
200
+ let concave_slot_idxB = new Uint32Array(32);
201
+ let g_concave_slot_count = 0;
202
+
180
203
  /**
181
204
  * Shared cross-stage state, set by {@link prepare_contacts} and read by the
182
205
  * per-substep stages within the same outer step. Single-threaded, so plain
@@ -205,6 +228,17 @@ function ensure_capacity(n) {
205
228
  if (scratch_max_jn.length < n) {
206
229
  scratch_max_jn = new Float64Array(n * 2);
207
230
  }
231
+ if (scratch_concave.length < n) {
232
+ scratch_concave = new Uint8Array(n * 2);
233
+ }
234
+ }
235
+
236
+ function ensure_concave_slot_capacity(n) {
237
+ if (concave_slot.length < n) {
238
+ concave_slot = new Uint32Array(n * 2);
239
+ concave_slot_idxA = new Uint32Array(n * 2);
240
+ concave_slot_idxB = new Uint32Array(n * 2);
241
+ }
208
242
  }
209
243
 
210
244
  /**
@@ -452,6 +486,8 @@ export function prepare_contacts(manifolds, system, dt_sub) {
452
486
  const mus = scratch_mu;
453
487
  const pos_jn = scratch_pos_jn;
454
488
 
489
+ g_concave_slot_count = 0;
490
+
455
491
  let c = 0;
456
492
  for (let i = 0; i < total_slots; i++) {
457
493
  const slot = slot_list[i];
@@ -466,6 +502,19 @@ export function prepare_contacts(manifolds, system, dt_sub) {
466
502
  if (colA === null || colB === null) continue;
467
503
  if (pair_is_sensor(rbA, colA, rbB, colB)) continue;
468
504
 
505
+ // A pair is "concave" when either side's shape is non-convex. Those
506
+ // contacts take the per-substep re-detection path (the contact
507
+ // feature moves as the body rocks); convex pairs keep the cheap
508
+ // analytic refresh. Recorded once per slot for redetect_concave_contacts.
509
+ const slot_concave = (colA.shape.is_convex === false) || (colB.shape.is_convex === false);
510
+ if (slot_concave) {
511
+ ensure_concave_slot_capacity(g_concave_slot_count + 1);
512
+ concave_slot[g_concave_slot_count] = slot;
513
+ concave_slot_idxA[g_concave_slot_count] = idxA;
514
+ concave_slot_idxB[g_concave_slot_count] = idxB;
515
+ g_concave_slot_count++;
516
+ }
517
+
469
518
  const invMA = inv_mass_of(rbA);
470
519
  const invMB = inv_mass_of(rbB);
471
520
 
@@ -545,6 +594,7 @@ export function prepare_contacts(manifolds, system, dt_sub) {
545
594
  mus[c] = friction_combined;
546
595
  pos_jn[c] = 0;
547
596
  scratch_max_jn[c] = 0;
597
+ scratch_concave[c] = slot_concave ? 1 : 0;
548
598
 
549
599
  idx[c * INDEX_STRIDE] = slot;
550
600
  idx[c * INDEX_STRIDE + 1] = k;
@@ -654,6 +704,11 @@ export function refresh_contacts(manifolds, system) {
654
704
  const spook_a = g_spook_a;
655
705
 
656
706
  for (let ci = 0; ci < count; ci++) {
707
+ // Concave contacts are refreshed by redetect_concave_contacts (fresh
708
+ // narrowphase geometry each substep), not by the analytic rotation of
709
+ // frozen anchors — their feature moves as the body rocks.
710
+ if (scratch_concave[ci] === 1) continue;
711
+
657
712
  const slot = idx[ci * INDEX_STRIDE];
658
713
  const cidx = idx[ci * INDEX_STRIDE + 1];
659
714
  const idxA = idx[ci * INDEX_STRIDE + 2];
@@ -704,6 +759,118 @@ export function refresh_contacts(manifolds, system) {
704
759
  }
705
760
  }
706
761
 
762
+ /**
763
+ * Stage 2b (per substep) — the concave counterpart of {@link refresh_contacts}.
764
+ *
765
+ * For each concave-involved slot, re-runs the narrowphase geometry at the
766
+ * current substep pose ({@link redetect_pair_geometry}, which rewrites the
767
+ * manifold's witness / normal / depth in place), then re-derives the solver
768
+ * scratch (lever arms, tangent basis, effective masses, position bias) for
769
+ * that slot's contacts from the fresh geometry. This is the whole point of
770
+ * the concave path: a body rocking on a mesh changes which triangle (and
771
+ * normal) it rests on within a single outer step, and freezing that — as the
772
+ * convex analytic refresh does — pumps energy in. Re-detecting gives a
773
+ * correct per-substep normal so the body settles.
774
+ *
775
+ * Cost is ~one narrowphase dispatch per concave slot per substep — acceptable
776
+ * because concave-involved pairs are rare (the common concave case, a convex
777
+ * body on static terrain, is convex on the moving side and never lands here).
778
+ * The contact count is held fixed (redetect updates geometry only), so the
779
+ * scratch stays aligned with prepare.
780
+ *
781
+ * Must run before {@link refresh_contacts} / {@link warm_start_contacts} each
782
+ * substep. `rest_bias` (captured at prepare) and the `scratch_max_jn`
783
+ * restitution gate are preserved.
784
+ *
785
+ * @param {ManifoldStore} manifolds
786
+ * @param {PhysicsSystem} system reads `__body_collider_lists`, `__bodies`,
787
+ * `__transforms`.
788
+ */
789
+ export function redetect_concave_contacts(manifolds, system) {
790
+ const ns = g_concave_slot_count;
791
+ if (ns === 0) return;
792
+
793
+ const lists = system.__body_collider_lists;
794
+
795
+ // 1. Re-detect fresh geometry into the manifold for each concave slot.
796
+ for (let s = 0; s < ns; s++) {
797
+ redetect_pair_geometry(manifolds, concave_slot[s], lists[concave_slot_idxA[s]], lists[concave_slot_idxB[s]]);
798
+ }
799
+
800
+ // 2. Re-derive the solver scratch for every concave contact from the
801
+ // fresh manifold geometry (lever arms, tangents, effective masses,
802
+ // position bias). Mirrors prepare's per-contact setup but reads the
803
+ // just-updated witness / normal / depth instead of the prepare-time
804
+ // values; no local-frame anchors are needed since we re-detect rather
805
+ // than rotate frozen anchors.
806
+ const count = g_contact_count;
807
+ const data = manifolds.data_buffer;
808
+ const pre = scratch_pre;
809
+ const idx = scratch_idx;
810
+ const spook_a = g_spook_a;
811
+ const spook_eps = g_spook_eps;
812
+
813
+ for (let ci = 0; ci < count; ci++) {
814
+ if (scratch_concave[ci] === 0) continue;
815
+
816
+ const slot = idx[ci * INDEX_STRIDE];
817
+ const cidx = idx[ci * INDEX_STRIDE + 1];
818
+ const idxA = idx[ci * INDEX_STRIDE + 2];
819
+ const idxB = idx[ci * INDEX_STRIDE + 3];
820
+ const rbA = system.__bodies[idxA];
821
+ const rbB = system.__bodies[idxB];
822
+ const trA = system.__transforms[idxA];
823
+ const trB = system.__transforms[idxB];
824
+ const invMA = inv_mass_of(rbA);
825
+ const invMB = inv_mass_of(rbB);
826
+
827
+ const slot_off = manifolds.slot_data_offset(slot);
828
+ const off = slot_off + cidx * CONTACT_STRIDE;
829
+
830
+ const wax = data[off], way = data[off + 1], waz = data[off + 2];
831
+ const wbx = data[off + 3], wby = data[off + 4], wbz = data[off + 5];
832
+ const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];
833
+ const depth = data[off + 9];
834
+
835
+ const px = (wax + wbx) * 0.5;
836
+ const py = (way + wby) * 0.5;
837
+ const pz = (waz + wbz) * 0.5;
838
+
839
+ const rax = px - trA.position.x, ray = py - trA.position.y, raz = pz - trA.position.z;
840
+ const rbx = px - trB.position.x, rby = py - trB.position.y, rbz = pz - trB.position.z;
841
+
842
+ const pre_off = ci * PRE_STRIDE;
843
+ pre[pre_off + 6] = rax; pre[pre_off + 7] = ray; pre[pre_off + 8] = raz;
844
+ pre[pre_off + 9] = rbx; pre[pre_off + 10] = rby; pre[pre_off + 11] = rbz;
845
+
846
+ build_tangents(pre, pre_off + 12, nx, ny, nz);
847
+ const t1x = pre[pre_off + 12], t1y = pre[pre_off + 13], t1z = pre[pre_off + 14];
848
+ const t2x = pre[pre_off + 15], t2y = pre[pre_off + 16], t2z = pre[pre_off + 17];
849
+
850
+ const k_n = invMA + invMB
851
+ + angular_jacobian_contribution(rbA, trA, rax, ray, raz, nx, ny, nz, scratch_inertia_a)
852
+ + angular_jacobian_contribution(rbB, trB, rbx, rby, rbz, nx, ny, nz, scratch_inertia_b);
853
+ const k_t1 = invMA + invMB
854
+ + angular_jacobian_contribution(rbA, trA, rax, ray, raz, t1x, t1y, t1z, scratch_inertia_a)
855
+ + angular_jacobian_contribution(rbB, trB, rbx, rby, rbz, t1x, t1y, t1z, scratch_inertia_b);
856
+ const k_t2 = invMA + invMB
857
+ + angular_jacobian_contribution(rbA, trA, rax, ray, raz, t2x, t2y, t2z, scratch_inertia_a)
858
+ + angular_jacobian_contribution(rbB, trB, rbx, rby, rbz, t2x, t2y, t2z, scratch_inertia_b);
859
+
860
+ const k_n_eff = k_n + spook_eps;
861
+ pre[pre_off + 18] = k_n_eff > 0 ? 1 / k_n_eff : 0;
862
+ pre[pre_off + 19] = k_t1 > 0 ? 1 / k_t1 : 0;
863
+ pre[pre_off + 20] = k_t2 > 0 ? 1 / k_t2 : 0;
864
+
865
+ let bias_position = 0;
866
+ if (depth > PENETRATION_SLOP) {
867
+ bias_position = -spook_a * (depth - PENETRATION_SLOP);
868
+ if (bias_position < -MAX_POSITION_BIAS) bias_position = -MAX_POSITION_BIAS;
869
+ }
870
+ pre[pre_off + 22] = bias_position;
871
+ }
872
+ }
873
+
707
874
  /**
708
875
  * Stage 3 (per substep) — velocity iterations enforcing pure
709
876
  * non-penetration (`vn → 0`) plus Coulomb-disk friction. No bias: depth
@@ -1006,6 +1173,7 @@ export function solve_contacts(manifolds, system, dt,
1006
1173
  pos_iters = DEFAULT_POSITION_ITERATIONS) {
1007
1174
  if (dt <= 0) return;
1008
1175
  if (prepare_contacts(manifolds, system, dt) === 0) return;
1176
+ redetect_concave_contacts(manifolds, system);
1009
1177
  refresh_contacts(manifolds, system);
1010
1178
  warm_start_contacts(manifolds, system);
1011
1179
  solve_velocity(manifolds, system, iters);