reze-engine 0.34.0 → 0.35.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.
package/README.md CHANGED
@@ -86,10 +86,10 @@ engine/src/
86
86
  slots.ts per-render-class fs() shell (stencil/alpha) around the graph body
87
87
  presets/ the 9 built-in shader graphs (hair, face, eye, cloth, …)
88
88
 
89
- physics/ in-house rigid-body solver (~2.5k lines)
89
+ physics/ in-house rigid-body solver (~4.2k lines)
90
90
  physics.ts RezePhysics: bone↔body sync, fixed-step accumulator + interpolation
91
91
  solver.ts sequential-impulse PGS (joint + contact rows)
92
- contact.ts narrowphase (analytical sphere/box/capsule pairs) + contact pool
92
+ contact.ts narrowphase (analytical sphere/box/capsule pairs, incl. box-box) + contact pool
93
93
  constraint.ts 6DOF spring joints · world.ts step, gravity + wind
94
94
  body.ts SoA rigid-body store · types.ts
95
95
 
@@ -325,15 +325,16 @@ Validation catches material conflicts, type mismatches, cycles, and bad links wi
325
325
 
326
326
  ## Physics
327
327
 
328
- In-house sequential-impulse rigid-body solver for PMX rigs (sphere / box / capsule colliders, 6DOF spring joints), ~2.5k lines of TypeScript, no external dependency, at quality comparable to Bullet's defaults. A fixed-timestep accumulator runs at a constant **60 Hz** (≤6 substeps/frame) so spring impulse, damping, and integration stay deterministic; dynamic bodies are **render-interpolated** between substeps to stay smooth when the display rate ≠ 60 Hz.
328
+ In-house sequential-impulse rigid-body solver for PMX rigs (sphere / box / capsule colliders, 6DOF spring joints), ~4.2k lines of TypeScript, no external dependency, at quality comparable to Bullet's defaults. A fixed-timestep accumulator runs at a constant **60 Hz** (≤6 substeps/frame) so spring impulse, damping, and integration stay deterministic; dynamic bodies are **render-interpolated** between substeps to stay smooth when the display rate ≠ 60 Hz.
329
329
 
330
330
  **Per substep:** `predict velocities → broad + narrowphase → solve constraints (10 iters) → split-impulse position correction → integrate`.
331
331
 
332
332
  - **Solver** — projected Gauss-Seidel, joint rows + contact rows in one loop. Joints are 6DOF spring constraints (3 linear + 3 angular) with stop-ERP limit correction. Linear rows pivot on each body's own joint-frame origin (Spring2-style, not Bullet 2.7x's shared mid-anchor), so a violated joint pulls itself back together instead of degenerating into torque and "breaking".
333
333
  - **Implicit spring-dampers** — each sprung axis solves the backward-Euler soft constraint `relVel⁺ + (k/γ)·err + s·λ = 0` (`γ = c + h·k`, `s = 1/(h·γ)`), unconditionally stable for any authored stiffness, with damping `c = 2ζ√(k·m_eff)` intrinsic to the row. Resting cloth genuinely settles: the previous clamped velocity-drive could inject energy far from equilibrium but not absorb it near it, so static dresses slowly "boiled" — measured 15× lower resting velocity after the change, with authored stiffness now delivered in full (no deadbeat clamp).
334
334
  - **Angular limits** — hybrid: small violations (< 0.5 rad, the resting-cloth regime) use per-axis Euler stop rows, which converge cleanly and keep resting cloth still; larger violations switch to a single geodesic row toward the Euler-clamped target rotation, because per-axis Euler rows chase phantom errors near the ±90° singularity and pump energy. Ranged stops are unilateral (accumulated impulse clamped to the corrective sign) so a limit pushes back into range but never brakes natural recovery; locked axes stay bilateral equality joints. Spring rows stay per-axis, with stiffness clamped to the `k·dt² ≤ ¼` stability bound.
335
- - **Narrowphase** — analytical sphere-sphere / -capsule / -box and capsule-capsule / -box. Capsule-capsule emits multiple contacts along near-parallel axes so cloth can't pivot around a single closest point.
336
- - **Speculative contacts** (`margin 0.04`) fire at near-touch with a push-only clamp inert until real overlap, but they stop fast bodies tunneling thin surfaces in one substep.
335
+ - **Narrowphase** — analytical sphere-sphere / -capsule / -box, capsule-capsule / -box, and box-box (SAT + face clipping, up to 4 points per manifold). MMD dress rigs are built from box panels, and box-box is the majority of collidable pairs on those models — without it cloth passes through cloth. Capsule-capsule emits multiple contacts along near-parallel axes so cloth can't pivot around a single closest point.
336
+ - **Speculative contacts** (`margin 0.04`) fire at near-touch and cancel only the approach velocity in excess of what closes the remaining gap in one substep (`gap/dt`), so they stop fast bodies tunneling thin surfaces while staying inert until the body would actually arrive. The push-only clamp does not achieve that on its own — it forbids a pulling impulse, not a large pushing one on a body still in mid-air.
337
+ - **Contact relaxation** — each contact row's gain is `1/max(rows on A, rows on B)` over dynamic bodies (floor 0.12). Every row drives the relative velocity at its own point to zero, so a dress panel carrying dozens at once is over-braked, differently each substep; a body on a lone contact keeps full gain and still settles exactly. A flat factor damps the well-conditioned rows too and leaves single-contact bodies creeping instead of settling.
337
338
  - **Split-impulse correction** resolves penetration by a mass-weighted translation _outside_ the velocity solver, so joint pulls can't fight separation.
338
339
  - **Kinematic advancement** — bone-driven bodies move toward the frame's bone pose incrementally per substep, with velocities derived over the fixed step, so the solver never sees more than one 60 Hz step of anchor motion regardless of render dt.
339
340
  - **Discontinuity guards** — a bone-pose jump beyond continuous motion (timeline scrub, long stall) rigidly carries each dynamic body along with its kinematic root's transform delta and zeroes momentum instead of dragging cloth across the gap; correction velocities are clamped (120 u/s linear, 30 rad/s angular), per-step travel is capped, and any body that still goes non-finite is restored to its previous substep pose.
@@ -26,6 +26,9 @@ export interface Contact {
26
26
  cBzN: number;
27
27
  jacInvN: number;
28
28
  bounceVel: number;
29
+ /** Per-contact relaxation gain, 1/max(rows on A, rows on B). See CONTACT_SOR_MIN. */
30
+ sorGain: number;
31
+ allowedApproachVel: number;
29
32
  t1x: number;
30
33
  t1y: number;
31
34
  t1z: number;
@@ -1 +1 @@
1
- {"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/physics/contact.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAO5C,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IAEb,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IAEX,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IAEnB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,uBAAuB,EAAE,MAAM,CAAA;IAI/B,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IAEjB,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAEhB,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;CACjB;AA8BD,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAgB;IAC5B,KAAK,SAAI;IAET,OAAO,IAAI,OAAO;IAelB,KAAK,IAAI,IAAI;IAGb,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;CAGxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAahF;AAkxBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA8CrG;AA4BD,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAsB3E"}
1
+ {"version":3,"file":"contact.d.ts","sourceRoot":"","sources":["../../src/physics/contact.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAa5C,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IAEb,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IAEX,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IAEnB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,CAAA;IAC/B,uBAAuB,EAAE,MAAM,CAAA;IAI/B,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAA;IAGf,kBAAkB,EAAE,MAAM,CAAA;IAE1B,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAEhB,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;IACrC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC3C,QAAQ,EAAE,MAAM,CAAA;CACjB;AAgCD,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAgB;IAC5B,KAAK,SAAI;IAET,OAAO,IAAI,OAAO;IAelB,KAAK,IAAI,IAAI;IAGb,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;CAGxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAahF;AAwoCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAgDrG;AAiCD,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAsB3E"}
@@ -4,13 +4,20 @@
4
4
  // positive normal impulse pushes B away from A. `rA` / `rB` are world-space
5
5
  // lever arms from each CG to the contact point. Depth is positive when
6
6
  // shapes overlap, ≤ 0 for speculative contacts inside the margin band.
7
- // Box-box not implemented (PMX rigs rarely use it and it needs SAT + clipping).
7
+ // Box-box is SAT + face clipping (see detectBoxBox) MMD dress rigs are built
8
+ // from box panels, and it is the majority of collidable pairs on those models.
8
9
  import { RigidbodyShape } from "./types";
9
10
  // Speculative contact range. Depth is reported relative to the un-inflated
10
11
  // surface, so values 0 ≥ depth ≥ −CONTACT_MARGIN cover the "near touch but
11
- // not overlapping yet" case. The push-only impulse clamp keeps these inert
12
- // until actual overlap, but they prevent fast bodies from crossing a thin
12
+ // not overlapping yet" case. They exist so a fast body cannot cross a thin
13
13
  // surface in one substep without ever generating a contact.
14
+ //
15
+ // What keeps them inert until the body would actually arrive is the solver's
16
+ // `allowedApproachVel` (gap / dt), NOT the push-only impulse clamp — the clamp
17
+ // only forbids a negative (pulling) impulse and does nothing to stop a large
18
+ // positive one from stopping a body dead in mid-air. This comment used to
19
+ // claim otherwise, and the bug it hid was worth 88% of speculative rows firing
20
+ // on a dress rig. See setupContactRow.
14
21
  export const CONTACT_MARGIN = 0.04;
15
22
  function makeContact() {
16
23
  return {
@@ -28,6 +35,8 @@ function makeContact() {
28
35
  cBxN: 0, cByN: 0, cBzN: 0,
29
36
  jacInvN: 0,
30
37
  bounceVel: 0,
38
+ sorGain: 1,
39
+ allowedApproachVel: 0,
31
40
  t1x: 0, t1y: 0, t1z: 0,
32
41
  cAxT1: 0, cAyT1: 0, cAzT1: 0,
33
42
  cBxT1: 0, cByT1: 0, cBzT1: 0,
@@ -483,9 +492,17 @@ function detectSphereBox(store, a, b, pool) {
483
492
  }
484
493
  // --- Capsule–box -----------------------------------------------------------
485
494
  // Walk the capsule's segment (in box-local space) toward the box, sample
486
- // sphere-box at the converged parameter plus both endpoints. Endpoint
487
- // samples catch caps grazing a face when the closest-point parameter sits
488
- // at one end of the segment.
495
+ // sphere-box at the converged parameter plus both endpoints, and keep the
496
+ // DEEPEST sample. Endpoint samples catch caps grazing a face when the
497
+ // closest-point parameter sits at one end of the segment.
498
+ //
499
+ // Deliberately one contact, not a manifold along the touching segment. A line
500
+ // of contacts here does stop a panel pivoting into a leg, and it was tried:
501
+ // penetration fell, but idle jitter on a 688-body rig rose because every extra
502
+ // contact is another shove from the position-correction pass, which the joint
503
+ // springs hand straight back. Cloth that buzzes reads worse than cloth that
504
+ // clips, so this keeps a little 穿模 in exchange for stillness. Box-box is a
505
+ // different case — those panels had NO collision at all, so it is pure gain.
489
506
  function detectCapsuleBox(store, a, b, pool) {
490
507
  const pos = store.positions, sz = store.size;
491
508
  const ai = a * 3, bi = b * 3;
@@ -643,6 +660,353 @@ function detectCapsuleBox(store, a, b, pool) {
643
660
  c.rBz = bestRBZ;
644
661
  combineMaterials(store, a, b, c);
645
662
  }
663
+ // --- Box–box ---------------------------------------------------------------
664
+ // SAT over the 15 candidate axes, then face clipping for a multi-point
665
+ // manifold. Everything below happens in A's local frame: B's centre and axes
666
+ // are transformed in once, so the 15 tests and the clipping all read as plain
667
+ // vector maths instead of repeated quaternion work.
668
+ //
669
+ // Why this exists at all: MMD dress rigs are built from flat box PANELS, and
670
+ // panel-against-panel is the collision that keeps skirt layers out of each
671
+ // other. Measured across seven shipped models, box-box is 45–70% of every
672
+ // collidable pair on models whose riggers left skirt self-collision enabled —
673
+ // all of it silently dropped before this. MMD's own physics is Bullet, which
674
+ // dispatches box-box through btBoxBoxDetector (ODE's dBoxBox: this same
675
+ // 15-axis SAT plus face clipping), so rigs are authored assuming it works.
676
+ //
677
+ // The pair count is not the frame cost: the AABB pass upstream filters first,
678
+ // and in a rest pose only ~220 of 诗蔻蒂's 60k box-box candidates reach here.
679
+ // Scratch, module-level so a frame of narrowphase allocates nothing.
680
+ const _bbBax = new Float32Array(9); // B's axes in A's frame, row j = axis j
681
+ const _bbC = new Float32Array(3); // B's centre in A's frame
682
+ const _bbAxis = new Float32Array(3); // best separating axis, A's frame
683
+ const _bbClip = new Float32Array(24); // clip buffer: up to 8 points
684
+ const _bbClip2 = new Float32Array(24);
685
+ const _bbDepth = new Float32Array(8);
686
+ const _bbTmp = new Float32Array(3);
687
+ // A's axes in A's own frame are the identity; kept as a constant so the
688
+ // reference/incident selection can treat both boxes through one code path
689
+ // without allocating a basis per call.
690
+ const _bbIdent = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
691
+ const _bbRefH = new Float32Array(3);
692
+ const _bbIncH = new Float32Array(3);
693
+ const _bbPA = new Float32Array(3);
694
+ const _bbHA = new Float32Array(3);
695
+ const _bbHB = new Float32Array(3);
696
+ // A face axis has to lose by a real margin before an edge axis wins. Near
697
+ // ties are common between two flat panels lying against each other, and an
698
+ // edge axis there yields one point where a face yields four — the manifold
699
+ // would flicker between them frame to frame and the panel would rock.
700
+ const EDGE_AXIS_BIAS = 1.05;
701
+ function detectBoxBox(store, a, b, pool) {
702
+ const ai = a * 3, bi = b * 3;
703
+ const sz = store.size;
704
+ const hAx = sz[ai + 0], hAy = sz[ai + 1], hAz = sz[ai + 2];
705
+ const hBx = sz[bi + 0], hBy = sz[bi + 1], hBz = sz[bi + 2];
706
+ // B's centre, in A's frame. The body index here is A, not B — transforming
707
+ // B's own centre by B's own transform yields the origin every time, which
708
+ // makes every SAT distance zero and every pair maximally overlapping.
709
+ worldToBodyLocal(store, a, store.positions[bi + 0], store.positions[bi + 1], store.positions[bi + 2], _bbC);
710
+ const cx = _bbC[0], cy = _bbC[1], cz = _bbC[2];
711
+ // B's three axes, in A's frame: RAᵀ · RB. loadBodyRot writes one body at a
712
+ // time, so read B's columns out before loading A over the top of them.
713
+ loadBodyRot(store, b);
714
+ const b00 = _rot[0], b01 = _rot[1], b02 = _rot[2];
715
+ const b10 = _rot[3], b11 = _rot[4], b12 = _rot[5];
716
+ const b20 = _rot[6], b21 = _rot[7], b22 = _rot[8];
717
+ loadBodyRot(store, a);
718
+ const a00 = _rot[0], a01 = _rot[1], a02 = _rot[2];
719
+ const a10 = _rot[3], a11 = _rot[4], a12 = _rot[5];
720
+ const a20 = _rot[6], a21 = _rot[7], a22 = _rot[8];
721
+ // Column j of RB is B's axis j in world; RAᵀ · that is it in A's frame.
722
+ for (let j = 0; j < 3; j++) {
723
+ const wx = j === 0 ? b00 : j === 1 ? b01 : b02;
724
+ const wy = j === 0 ? b10 : j === 1 ? b11 : b12;
725
+ const wz = j === 0 ? b20 : j === 1 ? b21 : b22;
726
+ _bbBax[j * 3 + 0] = a00 * wx + a10 * wy + a20 * wz;
727
+ _bbBax[j * 3 + 1] = a01 * wx + a11 * wy + a21 * wz;
728
+ _bbBax[j * 3 + 2] = a02 * wx + a12 * wy + a22 * wz;
729
+ }
730
+ // --- SAT. Track the axis of MINIMUM overlap; that is the shallowest way out
731
+ // and therefore the contact normal.
732
+ let bestOverlap = Infinity;
733
+ let bestAxis = -1; // 0-2 = A's faces, 3-5 = B's faces, 6-14 = edge crosses
734
+ let bestNx = 0, bestNy = 0, bestNz = 0;
735
+ // `scale` lets the edge axes be judged slightly harder — see EDGE_AXIS_BIAS.
736
+ const test = (nx, ny, nz, id, scale) => {
737
+ const len2 = nx * nx + ny * ny + nz * nz;
738
+ // Degenerate cross product: the two edges are parallel, so this axis adds
739
+ // nothing that the face axes have not already covered.
740
+ if (len2 < 1e-12)
741
+ return true;
742
+ const inv = 1 / Math.sqrt(len2);
743
+ const ux = nx * inv, uy = ny * inv, uz = nz * inv;
744
+ const projA = hAx * Math.abs(ux) + hAy * Math.abs(uy) + hAz * Math.abs(uz);
745
+ const projB = hBx * Math.abs(ux * _bbBax[0] + uy * _bbBax[1] + uz * _bbBax[2]) +
746
+ hBy * Math.abs(ux * _bbBax[3] + uy * _bbBax[4] + uz * _bbBax[5]) +
747
+ hBz * Math.abs(ux * _bbBax[6] + uy * _bbBax[7] + uz * _bbBax[8]);
748
+ const dist = Math.abs(ux * cx + uy * cy + uz * cz);
749
+ const overlap = projA + projB - dist;
750
+ // A gap wider than the speculative band: no contact, and no need to test
751
+ // the rest — one separating axis is proof.
752
+ if (overlap < -CONTACT_MARGIN)
753
+ return false;
754
+ if (overlap * scale < bestOverlap) {
755
+ bestOverlap = overlap * scale;
756
+ bestAxis = id;
757
+ bestNx = ux;
758
+ bestNy = uy;
759
+ bestNz = uz;
760
+ }
761
+ return true;
762
+ };
763
+ if (!test(1, 0, 0, 0, 1))
764
+ return;
765
+ if (!test(0, 1, 0, 1, 1))
766
+ return;
767
+ if (!test(0, 0, 1, 2, 1))
768
+ return;
769
+ for (let j = 0; j < 3; j++) {
770
+ if (!test(_bbBax[j * 3 + 0], _bbBax[j * 3 + 1], _bbBax[j * 3 + 2], 3 + j, 1))
771
+ return;
772
+ }
773
+ for (let i = 0; i < 3; i++) {
774
+ const axi = i === 0 ? 1 : 0, ayi = i === 1 ? 1 : 0, azi = i === 2 ? 1 : 0;
775
+ for (let j = 0; j < 3; j++) {
776
+ const bx = _bbBax[j * 3 + 0], by = _bbBax[j * 3 + 1], bz = _bbBax[j * 3 + 2];
777
+ if (!test(ayi * bz - azi * by, azi * bx - axi * bz, axi * by - ayi * bx, 6 + i * 3 + j, EDGE_AXIS_BIAS))
778
+ return;
779
+ }
780
+ }
781
+ if (bestAxis < 0)
782
+ return;
783
+ // Orient the normal A → B, matching the contact convention.
784
+ if (bestNx * cx + bestNy * cy + bestNz * cz < 0) {
785
+ bestNx = -bestNx;
786
+ bestNy = -bestNy;
787
+ bestNz = -bestNz;
788
+ }
789
+ _bbAxis[0] = bestNx;
790
+ _bbAxis[1] = bestNy;
791
+ _bbAxis[2] = bestNz;
792
+ if (bestAxis >= 6) {
793
+ emitBoxEdgeContact(store, a, b, bestOverlap / EDGE_AXIS_BIAS, (bestAxis - 6) / 3 | 0, (bestAxis - 6) % 3, hAx, hAy, hAz, hBx, hBy, hBz, cx, cy, cz, pool);
794
+ return;
795
+ }
796
+ emitBoxFaceManifold(store, a, b, bestAxis, hAx, hAy, hAz, hBx, hBy, hBz, cx, cy, cz, pool);
797
+ }
798
+ // Write one contact from a point given in A's LOCAL frame, with the manifold's
799
+ // shared world normal. Both lever arms come from the same world point: the
800
+ // clipped point sits on the incident face, within CONTACT_MARGIN of the
801
+ // reference face, so splitting them would be false precision.
802
+ function emitBoxContact(store, a, b, lx, ly, lz, depth, pool) {
803
+ const ai = a * 3, bi = b * 3;
804
+ bodyLocalToWorldDir(store, a, lx, ly, lz, _bbTmp);
805
+ const wx = _bbTmp[0] + store.positions[ai + 0];
806
+ const wy = _bbTmp[1] + store.positions[ai + 1];
807
+ const wz = _bbTmp[2] + store.positions[ai + 2];
808
+ bodyLocalToWorldDir(store, a, _bbAxis[0], _bbAxis[1], _bbAxis[2], _bbTmp);
809
+ const c = pool.acquire();
810
+ c.bodyA = a;
811
+ c.bodyB = b;
812
+ c.nx = _bbTmp[0];
813
+ c.ny = _bbTmp[1];
814
+ c.nz = _bbTmp[2];
815
+ c.depth = depth;
816
+ c.rAx = wx - store.positions[ai + 0];
817
+ c.rAy = wy - store.positions[ai + 1];
818
+ c.rAz = wz - store.positions[ai + 2];
819
+ c.rBx = wx - store.positions[bi + 0];
820
+ c.rBy = wy - store.positions[bi + 1];
821
+ c.rBz = wz - store.positions[bi + 2];
822
+ combineMaterials(store, a, b, c);
823
+ }
824
+ // Clip a polygon against the plane dot(p, t) ≤ offset (Sutherland–Hodgman).
825
+ // Points are xyz triples packed into `src`; returns the new count.
826
+ function clipPolyByPlane(src, n, tx, ty, tz, offset, dst) {
827
+ let out = 0;
828
+ for (let i = 0; i < n; i++) {
829
+ const j = (i + 1) % n;
830
+ const px = src[i * 3], py = src[i * 3 + 1], pz = src[i * 3 + 2];
831
+ const qx = src[j * 3], qy = src[j * 3 + 1], qz = src[j * 3 + 2];
832
+ const dp = px * tx + py * ty + pz * tz - offset;
833
+ const dq = qx * tx + qy * ty + qz * tz - offset;
834
+ if (dp <= 0) {
835
+ dst[out * 3] = px;
836
+ dst[out * 3 + 1] = py;
837
+ dst[out * 3 + 2] = pz;
838
+ out++;
839
+ }
840
+ // Sign change: the edge crosses the plane, so the crossing point joins the
841
+ // polygon. Guard the divide — a denominator this small means the edge lies
842
+ // in the plane, and both endpoints are already handled by the tests above.
843
+ if ((dp < 0 && dq > 0) || (dp > 0 && dq < 0)) {
844
+ const den = dp - dq;
845
+ if (Math.abs(den) > 1e-12 && out < 8) {
846
+ const s = dp / den;
847
+ dst[out * 3] = px + (qx - px) * s;
848
+ dst[out * 3 + 1] = py + (qy - py) * s;
849
+ dst[out * 3 + 2] = pz + (qz - pz) * s;
850
+ out++;
851
+ }
852
+ }
853
+ if (out >= 8)
854
+ break;
855
+ }
856
+ return out;
857
+ }
858
+ // Face-vs-face: clip the incident face against the reference face's four side
859
+ // planes, then keep whatever is at or below the reference plane. This is what
860
+ // yields a multi-point manifold — the reason a flat panel resting on another
861
+ // stops pivoting about a single point.
862
+ function emitBoxFaceManifold(store, a, b, bestAxis, hAx, hAy, hAz, hBx, hBy, hBz, cx, cy, cz, pool) {
863
+ const refIsA = bestAxis < 3;
864
+ const refAxisIdx = refIsA ? bestAxis : bestAxis - 3;
865
+ // Reference basis, half extents and centre — all in A's frame. When A is the
866
+ // reference its axes ARE the frame, hence the identity rows.
867
+ const refAx = refIsA ? _bbIdent : _bbBax;
868
+ const incAx = refIsA ? _bbBax : _bbIdent;
869
+ const refH = _bbRefH, incH = _bbIncH;
870
+ refH[0] = refIsA ? hAx : hBx;
871
+ refH[1] = refIsA ? hAy : hBy;
872
+ refH[2] = refIsA ? hAz : hBz;
873
+ incH[0] = refIsA ? hBx : hAx;
874
+ incH[1] = refIsA ? hBy : hAy;
875
+ incH[2] = refIsA ? hBz : hAz;
876
+ const refCx = refIsA ? 0 : cx, refCy = refIsA ? 0 : cy, refCz = refIsA ? 0 : cz;
877
+ const incCx = refIsA ? cx : 0, incCy = refIsA ? cy : 0, incCz = refIsA ? cz : 0;
878
+ // Outward normal of the reference face, pointing at the incident box.
879
+ // _bbAxis runs A → B, so B-as-reference faces the other way.
880
+ const sgn = refIsA ? 1 : -1;
881
+ const nx = _bbAxis[0] * sgn, ny = _bbAxis[1] * sgn, nz = _bbAxis[2] * sgn;
882
+ // Incident face: the one whose outward normal is most opposed to n.
883
+ let incIdx = 0, incDot = Infinity, incSign = 1;
884
+ for (let k = 0; k < 3; k++) {
885
+ const d = incAx[k * 3] * nx + incAx[k * 3 + 1] * ny + incAx[k * 3 + 2] * nz;
886
+ const s = d > 0 ? -1 : 1;
887
+ const v = d * s;
888
+ if (v < incDot) {
889
+ incDot = v;
890
+ incIdx = k;
891
+ incSign = s;
892
+ }
893
+ }
894
+ // Its four corners, from the face centre along the two remaining axes.
895
+ const u = (incIdx + 1) % 3, v = (incIdx + 2) % 3;
896
+ const fx = incCx + incAx[incIdx * 3] * incSign * incH[incIdx];
897
+ const fy = incCy + incAx[incIdx * 3 + 1] * incSign * incH[incIdx];
898
+ const fz = incCz + incAx[incIdx * 3 + 2] * incSign * incH[incIdx];
899
+ let n0 = 0;
900
+ for (let iu = 0; iu < 2; iu++) {
901
+ const su = iu === 0 ? 1 : -1;
902
+ for (let iv = 0; iv < 2; iv++) {
903
+ const sv = iv === 0 ? 1 : -1;
904
+ // Wound consistently (++, +−, −−, −+) so the clip walks a real quad.
905
+ const s2 = su === 1 ? sv : -sv;
906
+ _bbClip[n0 * 3] = fx + incAx[u * 3] * incH[u] * su + incAx[v * 3] * incH[v] * s2;
907
+ _bbClip[n0 * 3 + 1] = fy + incAx[u * 3 + 1] * incH[u] * su + incAx[v * 3 + 1] * incH[v] * s2;
908
+ _bbClip[n0 * 3 + 2] = fz + incAx[u * 3 + 2] * incH[u] * su + incAx[v * 3 + 2] * incH[v] * s2;
909
+ n0++;
910
+ }
911
+ }
912
+ // Clip against the reference face's four side planes.
913
+ const ru = (refAxisIdx + 1) % 3, rv = (refAxisIdx + 2) % 3;
914
+ let src = _bbClip, dst = _bbClip2, cnt = n0;
915
+ for (let plane = 0; plane < 4; plane++) {
916
+ const ax = plane < 2 ? ru : rv;
917
+ const sgn2 = plane % 2 === 0 ? 1 : -1;
918
+ const tx = refAx[ax * 3] * sgn2, ty = refAx[ax * 3 + 1] * sgn2, tz = refAx[ax * 3 + 2] * sgn2;
919
+ const offset = refCx * tx + refCy * ty + refCz * tz + refH[ax];
920
+ cnt = clipPolyByPlane(src, cnt, tx, ty, tz, offset, dst);
921
+ const t = src;
922
+ src = dst;
923
+ dst = t;
924
+ if (cnt === 0)
925
+ return;
926
+ }
927
+ // Keep what is at or below the reference face plane.
928
+ const planeD = (refCx + nx * refH[refAxisIdx]) * nx + (refCy + ny * refH[refAxisIdx]) * ny +
929
+ (refCz + nz * refH[refAxisIdx]) * nz;
930
+ let kept = 0;
931
+ for (let i = 0; i < cnt; i++) {
932
+ const sep = src[i * 3] * nx + src[i * 3 + 1] * ny + src[i * 3 + 2] * nz - planeD;
933
+ if (sep > CONTACT_MARGIN)
934
+ continue;
935
+ src[kept * 3] = src[i * 3];
936
+ src[kept * 3 + 1] = src[i * 3 + 1];
937
+ src[kept * 3 + 2] = src[i * 3 + 2];
938
+ _bbDepth[kept] = -sep;
939
+ kept++;
940
+ }
941
+ if (kept === 0)
942
+ return;
943
+ // Cap the manifold at Bullet's four. Clipping a quad by four planes can
944
+ // reach eight points, and every extra one is another solver row for a
945
+ // patch the deepest four already describe. Deepest-first so the points
946
+ // that matter survive the cut.
947
+ if (kept > 4) {
948
+ for (let i = 1; i < kept; i++) {
949
+ const d = _bbDepth[i];
950
+ const px = src[i * 3], py = src[i * 3 + 1], pz = src[i * 3 + 2];
951
+ let j = i - 1;
952
+ while (j >= 0 && _bbDepth[j] < d) {
953
+ _bbDepth[j + 1] = _bbDepth[j];
954
+ src[(j + 1) * 3] = src[j * 3];
955
+ src[(j + 1) * 3 + 1] = src[j * 3 + 1];
956
+ src[(j + 1) * 3 + 2] = src[j * 3 + 2];
957
+ j--;
958
+ }
959
+ _bbDepth[j + 1] = d;
960
+ src[(j + 1) * 3] = px;
961
+ src[(j + 1) * 3 + 1] = py;
962
+ src[(j + 1) * 3 + 2] = pz;
963
+ }
964
+ kept = 4;
965
+ }
966
+ for (let i = 0; i < kept; i++) {
967
+ emitBoxContact(store, a, b, src[i * 3], src[i * 3 + 1], src[i * 3 + 2], _bbDepth[i], pool);
968
+ }
969
+ }
970
+ // Edge-vs-edge: one point, at the midpoint of the closest approach between the
971
+ // two supporting edges. Single-point is correct here — two crossed edges touch
972
+ // at a point, unlike two faces.
973
+ function emitBoxEdgeContact(store, a, b, depth, i, j, hAx, hAy, hAz, hBx, hBy, hBz, cx, cy, cz, pool) {
974
+ const hA = _bbHA, hB = _bbHB;
975
+ hA[0] = hAx;
976
+ hA[1] = hAy;
977
+ hA[2] = hAz;
978
+ hB[0] = hBx;
979
+ hB[1] = hBy;
980
+ hB[2] = hBz;
981
+ const nx = _bbAxis[0], ny = _bbAxis[1], nz = _bbAxis[2];
982
+ // A's supporting edge: offset along the two axes that are NOT the edge
983
+ // direction, each toward B.
984
+ const pA = _bbPA;
985
+ pA[0] = 0;
986
+ pA[1] = 0;
987
+ pA[2] = 0;
988
+ for (let k = 0; k < 3; k++) {
989
+ if (k === i)
990
+ continue;
991
+ const d = k === 0 ? nx : k === 1 ? ny : nz;
992
+ pA[k] = hA[k] * (d >= 0 ? 1 : -1);
993
+ }
994
+ const dAx = i === 0 ? 1 : 0, dAy = i === 1 ? 1 : 0, dAz = i === 2 ? 1 : 0;
995
+ // B's, offset the other way — its edge faces back toward A.
996
+ let pBx = cx, pBy = cy, pBz = cz;
997
+ for (let k = 0; k < 3; k++) {
998
+ if (k === j)
999
+ continue;
1000
+ const ax = _bbBax[k * 3], ay = _bbBax[k * 3 + 1], az = _bbBax[k * 3 + 2];
1001
+ const s = ax * nx + ay * ny + az * nz >= 0 ? -1 : 1;
1002
+ pBx += ax * hB[k] * s;
1003
+ pBy += ay * hB[k] * s;
1004
+ pBz += az * hB[k] * s;
1005
+ }
1006
+ const dBx = _bbBax[j * 3], dBy = _bbBax[j * 3 + 1], dBz = _bbBax[j * 3 + 2];
1007
+ closestPointsTwoSegments(pA[0] - dAx * hA[i], pA[1] - dAy * hA[i], pA[2] - dAz * hA[i], pA[0] + dAx * hA[i], pA[1] + dAy * hA[i], pA[2] + dAz * hA[i], pBx - dBx * hB[j], pBy - dBy * hB[j], pBz - dBz * hB[j], pBx + dBx * hB[j], pBy + dBy * hB[j], pBz + dBz * hB[j], _cpA, _cpB);
1008
+ emitBoxContact(store, a, b, (_cpA[0] + _cpB[0]) * 0.5, (_cpA[1] + _cpB[1]) * 0.5, (_cpA[2] + _cpB[2]) * 0.5, depth, pool);
1009
+ }
646
1010
  // Dispatch a pair to the matching narrowphase. Caller has already done
647
1011
  // broadphase + group/mask filtering. Some shape pairs (sphere-A capsule-B
648
1012
  // etc.) reuse a canonical implementation via swap + flipLastNormal.
@@ -664,8 +1028,7 @@ export function generateContacts(store, a, b, pool) {
664
1028
  // end of the pool — pushing those two bodies together instead of apart.
665
1029
  const before = pool.count;
666
1030
  detectSphereCapsule(store, b, a, pool);
667
- if (pool.count > before)
668
- flipLastNormal(pool);
1031
+ flipNormalsFrom(pool, before);
669
1032
  return;
670
1033
  }
671
1034
  if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Capsule) {
@@ -679,8 +1042,7 @@ export function generateContacts(store, a, b, pool) {
679
1042
  if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Sphere) {
680
1043
  const before = pool.count;
681
1044
  detectSphereBox(store, b, a, pool);
682
- if (pool.count > before)
683
- flipLastNormal(pool);
1045
+ flipNormalsFrom(pool, before);
684
1046
  return;
685
1047
  }
686
1048
  if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Box) {
@@ -690,18 +1052,23 @@ export function generateContacts(store, a, b, pool) {
690
1052
  if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Capsule) {
691
1053
  const before = pool.count;
692
1054
  detectCapsuleBox(store, b, a, pool);
693
- if (pool.count > before)
694
- flipLastNormal(pool);
1055
+ flipNormalsFrom(pool, before);
695
1056
  return;
696
1057
  }
697
- // Box-box left unimplemented.
1058
+ if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Box) {
1059
+ detectBoxBox(store, a, b, pool);
1060
+ }
698
1061
  }
699
- // After a swapped detect* call, the last contact's normal points the wrong
700
- // way and lever arms are mismatched. Flip and re-anchor.
701
- function flipLastNormal(pool) {
702
- if (pool.count === 0)
703
- return;
704
- const c = pool.get(pool.count - 1);
1062
+ // After a swapped detect* call, the produced contacts' normals point the wrong
1063
+ // way and lever arms are mismatched. Flip and re-anchor EVERY contact the call
1064
+ // emitted, not just the last: capsule-box now returns up to three, and flipping
1065
+ // one of them would leave the others pulling the pair together instead of
1066
+ // pushing it apart.
1067
+ function flipNormalsFrom(pool, from) {
1068
+ for (let i = from; i < pool.count; i++)
1069
+ flipOneNormal(pool.get(i));
1070
+ }
1071
+ function flipOneNormal(c) {
705
1072
  const ta = c.bodyA;
706
1073
  c.bodyA = c.bodyB;
707
1074
  c.bodyB = ta;
@@ -0,0 +1,13 @@
1
+ import type { ContactPool } from "./contact";
2
+ export declare class ImpulseCache {
3
+ private a;
4
+ private b;
5
+ /** Seed this substep's contacts with what the same rows converged to last
6
+ * substep. Contacts with no history start at zero, as new contacts should. */
7
+ seed(pool: ContactPool): void;
8
+ /** Store the converged impulses, then swap: this substep's results become the
9
+ * next one's history. Double-buffered so a pair that stops touching simply
10
+ * falls out rather than needing an expiry sweep. */
11
+ writeback(pool: ContactPool): void;
12
+ }
13
+ //# sourceMappingURL=manifold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifold.d.ts","sourceRoot":"","sources":["../../src/physics/manifold.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAK5C,qBAAa,YAAY;IACvB,OAAO,CAAC,CAAC,CAAkC;IAC3C,OAAO,CAAC,CAAC,CAAkC;IAE3C;mFAC+E;IAC/E,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAmB7B;;yDAEqD;IACrD,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;CAgBnC"}
@@ -0,0 +1,68 @@
1
+ // Contact impulse history — the cache that makes warm starting possible.
2
+ //
3
+ // Bullet keeps a btPersistentManifold per body pair and carries each point's
4
+ // m_appliedImpulse into the next substep at m_warmstartingFactor (0.85), so a
5
+ // resting contact starts from the answer it converged to last time instead of
6
+ // from zero. Without it a sustained contact is rediscovered every substep:
7
+ // it overshoots, releases, re-forms — which is chatter. Measured here, contacts
8
+ // account for ALL of the visible shake under animation (25 shaking bodies with
9
+ // contacts on, 0 with them off), so this is the mechanism that matters.
10
+ //
11
+ // Identity is by ORDINAL within the pair, not by proximity. Our narrowphase is
12
+ // deterministic — the same pair walks the same code path and emits its contacts
13
+ // in the same order every substep — so "the third contact of this pair" is a
14
+ // stabler key than "the cached point nearest this one", which needs a distance
15
+ // threshold and mis-pairs whenever two contacts fall inside it.
16
+ const MAX_PER_PAIR = 8;
17
+ const STRIDE = 3; // normal, friction1, friction2
18
+ export class ImpulseCache {
19
+ constructor() {
20
+ this.a = new Map();
21
+ this.b = new Map();
22
+ }
23
+ /** Seed this substep's contacts with what the same rows converged to last
24
+ * substep. Contacts with no history start at zero, as new contacts should. */
25
+ seed(pool) {
26
+ const ord = new Map();
27
+ for (let i = 0; i < pool.count; i++) {
28
+ const c = pool.get(i);
29
+ const key = c.bodyA * 65536 + c.bodyB;
30
+ const n = ord.get(key) ?? 0;
31
+ ord.set(key, n + 1);
32
+ c.manifoldKey = key;
33
+ c.manifoldSlot = n;
34
+ if (n >= MAX_PER_PAIR)
35
+ continue;
36
+ const prev = this.a.get(key);
37
+ if (!prev)
38
+ continue;
39
+ const o = n * STRIDE;
40
+ c.appliedNormalImpulse = prev[o];
41
+ c.appliedFrictionImpulse1 = prev[o + 1];
42
+ c.appliedFrictionImpulse2 = prev[o + 2];
43
+ }
44
+ }
45
+ /** Store the converged impulses, then swap: this substep's results become the
46
+ * next one's history. Double-buffered so a pair that stops touching simply
47
+ * falls out rather than needing an expiry sweep. */
48
+ writeback(pool) {
49
+ this.b.clear();
50
+ for (let i = 0; i < pool.count; i++) {
51
+ const c = pool.get(i);
52
+ if (c.manifoldSlot >= MAX_PER_PAIR)
53
+ continue;
54
+ let arr = this.b.get(c.manifoldKey);
55
+ if (!arr) {
56
+ arr = new Float32Array(MAX_PER_PAIR * STRIDE);
57
+ this.b.set(c.manifoldKey, arr);
58
+ }
59
+ const o = c.manifoldSlot * STRIDE;
60
+ arr[o] = c.appliedNormalImpulse;
61
+ arr[o + 1] = c.appliedFrictionImpulse1;
62
+ arr[o + 2] = c.appliedFrictionImpulse2;
63
+ }
64
+ const t = this.a;
65
+ this.a = this.b;
66
+ this.b = t;
67
+ }
68
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;AAuDrD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACjB,IAAI,CA4BN;AAyCD,qBAAa,WAAW;IACtB,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;IACxB,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAA;IACtB;sEACkE;IAClE,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;gBACZ,WAAW,EAAE,sBAAsB,EAAE;CAUlD"}
1
+ {"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;AAgFrD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACjB,IAAI,CA2CN;AAyCD,qBAAa,WAAW;IACtB,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;IACxB,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAA;IACtB;sEACkE;IAClE,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;gBACZ,WAAW,EAAE,sBAAsB,EAAE;CAUlD"}