@woosh/meep-engine 2.143.0 → 2.145.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 (56) hide show
  1. package/package.json +1 -1
  2. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  3. package/src/core/bvh2/bvh3/BVH.js +158 -4
  4. package/src/core/geom/3d/shape/CylinderShape3D.d.ts +56 -0
  5. package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -0
  6. package/src/core/geom/3d/shape/CylinderShape3D.js +223 -0
  7. package/src/core/geom/3d/shape/PointShape3D.d.ts +1 -0
  8. package/src/core/geom/3d/shape/PointShape3D.d.ts.map +1 -1
  9. package/src/core/geom/3d/shape/PointShape3D.js +11 -0
  10. package/src/core/geom/3d/shape/SphereShape3D.d.ts +1 -0
  11. package/src/core/geom/3d/shape/SphereShape3D.d.ts.map +1 -1
  12. package/src/core/geom/3d/shape/SphereShape3D.js +4 -0
  13. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  14. package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
  15. package/src/core/geom/3d/shape/json/type_adapters.d.ts +15 -0
  16. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  17. package/src/core/geom/3d/shape/json/type_adapters.js +16 -0
  18. package/src/engine/control/first-person/DESIGN_COLLISION.md +314 -217
  19. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +104 -58
  20. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  21. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1828 -1789
  22. package/src/engine/control/first-person/TODO.md +17 -32
  23. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  24. package/src/engine/control/first-person/abilities/WallRun.js +18 -35
  25. package/src/engine/control/first-person/collision/KinematicMover.d.ts +206 -0
  26. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -0
  27. package/src/engine/control/first-person/collision/KinematicMover.js +592 -0
  28. package/src/engine/control/first-person/prototype_first_person_controller.js +65 -0
  29. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +18 -9
  30. package/src/engine/physics/PLAN.md +145 -41
  31. package/src/engine/physics/contact/ManifoldStore.d.ts +28 -2
  32. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  33. package/src/engine/physics/contact/ManifoldStore.js +37 -3
  34. package/src/engine/physics/contact/combine_material.d.ts +30 -0
  35. package/src/engine/physics/contact/combine_material.d.ts.map +1 -0
  36. package/src/engine/physics/contact/combine_material.js +35 -0
  37. package/src/engine/physics/ecs/Collider.d.ts +15 -0
  38. package/src/engine/physics/ecs/Collider.d.ts.map +1 -1
  39. package/src/engine/physics/ecs/Collider.js +34 -0
  40. package/src/engine/physics/ecs/Joint.d.ts +18 -0
  41. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  42. package/src/engine/physics/ecs/Joint.js +70 -0
  43. package/src/engine/physics/ecs/PhysicsSystem.d.ts +9 -4
  44. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  45. package/src/engine/physics/ecs/PhysicsSystem.js +9 -4
  46. package/src/engine/physics/ecs/RigidBody.d.ts +15 -0
  47. package/src/engine/physics/ecs/RigidBody.d.ts.map +1 -1
  48. package/src/engine/physics/ecs/RigidBody.js +46 -0
  49. package/src/engine/physics/narrowphase/compute_penetration.d.ts +41 -41
  50. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
  51. package/src/engine/physics/narrowphase/compute_penetration.js +96 -169
  52. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +52 -0
  53. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  54. package/src/engine/physics/narrowphase/narrowphase_step.js +130 -3
  55. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  56. package/src/engine/physics/solver/solve_contacts.js +10 -21
@@ -199,8 +199,6 @@ const SAOShader = {
199
199
  // Roberts' R3 low-discrepancy additive recurrence: ( 1/g, 1/g^2, 1/g^3 ), g^4 = g + 1
200
200
  const vec3 R3 = vec3( 0.8191725134, 0.6710436067, 0.5497004779 );
201
201
 
202
- const float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );
203
-
204
202
  // normal-bias as a fraction of kernelRadius: lifts the sample origin off the surface to avoid
205
203
  // self-occlusion. Larger removes acne but lets near-surface contact occlusion leak.
206
204
  const float SURFACE_BIAS = 0.05;
@@ -246,6 +244,7 @@ const SAOShader = {
246
244
  float falloffAdd = falloffFrom / falloffRange + 1.0;
247
245
 
248
246
  float occlusion = 0.0;
247
+ float weightSum = 0.0;
249
248
 
250
249
  for ( int i = 0; i < NUM_SAMPLES; i ++ ) {
251
250
  // fixed R3 low-discrepancy point; per-pixel variation comes from the rotated frame
@@ -260,6 +259,10 @@ const SAOShader = {
260
259
  // sampleDistributionPower) so the near occluders that dominate AO are favoured
261
260
  float radius = pow( fract( q.z + noise.y ), sampleDistributionPower );
262
261
 
262
+ // falloff weight from the probe's own distance -- defined for every sample, so the
263
+ // weighted normaliser is well posed; distant probes are less reliable (GTAO)
264
+ float weight = saturate( radius * kernelRadius * falloffMul + falloffAdd );
265
+
263
266
  vec3 samplePositionVS = sampleOrigin + dir * ( radius * kernelRadius );
264
267
 
265
268
  // project the sample into screen space
@@ -283,6 +286,9 @@ const SAOShader = {
283
286
  continue;
284
287
  }
285
288
 
289
+ // a valid (on-screen) sample contributes its weight to the normaliser, occluded or not
290
+ weightSum += weight;
291
+
286
292
  float sampleDepth = getDepth( sampleUv );
287
293
 
288
294
  // skip the sky / far plane
@@ -302,17 +308,16 @@ const SAOShader = {
302
308
  // ...and the tapped surface must actually be within the sampling radius, otherwise we
303
309
  // have hit distant geometry through the depth buffer that does not occlude this point
304
310
  vec3 occluderVS = getViewPosition( sampleUv, sampleDepth, sceneViewZ );
305
- float sampleDist = distance( centerViewPosition, occluderVS );
306
- if ( sampleDist > kernelRadius ) {
311
+ if ( distance( centerViewPosition, occluderVS ) > kernelRadius ) {
307
312
  continue;
308
313
  }
309
314
 
310
- // distant samples contribute less: linear fade from full weight at falloffFrom to zero
311
- // at the radius (GTAO)
312
- occlusion += saturate( sampleDist * falloffMul + falloffAdd );
315
+ // occluded: add this sample's weight to the numerator
316
+ occlusion += weight;
313
317
  }
314
318
 
315
- return occlusion * INV_NUM_SAMPLES;
319
+ // weighted occlusion fraction; off-screen samples were excluded so screen borders are unbiased
320
+ return weightSum > 0.0 ? occlusion / weightSum : 0.0;
316
321
  }
317
322
 
318
323
  void main() {
@@ -323,7 +328,11 @@ const SAOShader = {
323
328
 
324
329
  float centerDepth = getDepth( centerUv );
325
330
  if( centerDepth >= ( 1.0 - EPSILON ) ) {
326
- discard;
331
+ // background / far plane is fully visible. Write white rather than discarding, so the
332
+ // half-res buffer never leaves the clear value for the upscale to read back as occlusion
333
+ // (that is the foreground-edge halo over background).
334
+ out_occlusion = 1.0;
335
+ return;
327
336
  }
328
337
 
329
338
  float centerViewZ = getViewZ( centerDepth );
@@ -169,21 +169,35 @@ Architectural references for design choices:
169
169
  kinematic bodies (character controllers, AOE selection).
170
170
 
171
171
  ### Standalone narrowphase utilities
172
+ - `deepest_pair_penetration(out_normal, shapeA, posA, rotA, shapeB, posB,
173
+ rotB)` (exported from `narrowphase_step.js`) — runs the **same**
174
+ `dispatch_pair` the contact solver consumes for one posed shape pair and
175
+ returns the DEEPEST contact's depth + world normal (B → A). The single
176
+ source of truth for "minimum-translation between two posed shapes", reused by
177
+ `compute_penetration` (and available to any other query).
172
178
  - `compute_penetration(out_direction, shape_a, pos_a, rot_a, shape_b,
173
179
  pos_b, rot_b)` — non-system geometry primitive: positive penetration
174
180
  depth + outward direction (B → A convention) on overlap, 0 otherwise.
175
- Convex × convex uses GJK + EPA. Convex × concave uses per-triangle
176
- half-space test (`convex.support(-face_normal)` projected onto each
177
- triangle's plane), aggregated deepest-wins. The half-space approach
178
- sidesteps `Triangle3D`'s degenerate support along face-normal axes
179
- (the same issue that makes per-triangle GJK return false positives
180
- on clearly non-overlapping sphere-above-flat configurations).
181
- Concave × concave throws (M×N triangle pairs is out of scope).
182
- Naturally handles "body inside the concave solid" — reports the depth
183
- needed to push back through the nearest face. Documented limitation:
184
- closed meshes can over-report on side faces whose 2D extent the
185
- convex shape's flank crosses; a future closed-form triangle-vs-X
186
- solver fixes this.
181
+ **Hardened** delegates to `deepest_pair_penetration`, so it is correct
182
+ (not "correct sometimes") for every shape pair the engine can build:
183
+ - sphere / box / capsule pairs → exact closed-form (box-box via SAT, so a
184
+ small body resting on a large floor reports the few-cm near-face overlap,
185
+ NOT the metres-deep "exit through the far side" that MPR's centroid-seeded
186
+ portal used to return);
187
+ - general convex pairs → GJK + EPA (exact for polytopes; curved shapes never
188
+ reach it they have closed forms);
189
+ - convex × concave triangle decomposition + the closed-form per-triangle
190
+ solvers, bounded to each triangle's true 2-D extent (the old closed-mesh
191
+ side-face over-report is gone).
192
+ The previous per-triangle half-space test is retained ONLY as a recovery
193
+ fallback for the one case the one-sided closed forms can't resolve: a convex
194
+ shape that has fully tunnelled to the *inner* side of a concave surface (a
195
+ depenetration query must still push it back out — exact for heightmap terrain,
196
+ a valid outward push for closed meshes). Concave × concave throws (M×N
197
+ triangle pairs out of scope). The spec asserts an "applying out_direction ×
198
+ depth separates the shapes" invariant across every convex+convex pair type and
199
+ convex+concave, plus exact per-type depths and the small-box-on-huge-floor
200
+ regression (3 m → 0.05 m).
187
201
 
188
202
  ### Determinism
189
203
  - Direct typed-array writes on hot paths (bypassing `Vector3#set`'s observer
@@ -224,11 +238,14 @@ Architectural references for design choices:
224
238
 
225
239
  ## Limitations / Known caveats
226
240
 
227
- - **Multi-collider material precision**: solver reads friction/restitution
228
- from the first-attached collider of each body. Mixed-material compound
229
- bodies lose accuracy here. The contact-filter callback's `colliderA/B`
230
- arguments are similarly the body's primary collider, not the specific
231
- collider in contact.
241
+ - **Multi-collider material precision** *resolved for contact materials.* The
242
+ narrowphase now combines the specific source-collider pair's friction /
243
+ restitution per contact and stores them in the manifold (CONTACT_STRIDE
244
+ offsets 14/15); the solver reads them per contact, so mixed-material compound
245
+ bodies are accurate (regression test: an asymmetric-friction body yaws when
246
+ shoved). Still primary-collider only: the contact-filter callback's
247
+ `colliderA/B` arguments and the body-level sensor / concave-dispatch flags —
248
+ a smaller follow-up.
232
249
  - **EPA on smooth shapes**: degenerates (no flat face to converge on).
233
250
  Mitigated by closed-form paths for sphere/cube/capsule pairs and by the
234
251
  **MPR fallback** on EPA non-convergence; exotic convex shapes vs spheres can
@@ -239,12 +256,20 @@ Architectural references for design choices:
239
256
  for those primitives, so a sphere/box/capsule on a heightmap or mesh decelerates
240
257
  and settles correctly; the `narrowphase_concave.spec.js` "drop and settle"
241
258
  cases and the mesh torus-knot settle test are **un-skipped**. Per-triangle
242
- GJK+EPA remains only as the fallback for *other* convex shapes vs triangles
243
- (and `compute_penetration` still uses the half-space pre-test there).
259
+ GJK+EPA remains only as the fallback for *other* convex shapes vs triangles.
260
+ (`compute_penetration` now routes through that same dispatch via
261
+ `deepest_pair_penetration` — see *Standalone narrowphase utilities* — instead
262
+ of its old half-space pre-test; the half-space test survives only as a
263
+ tunnel-recovery fallback.)
244
264
  - **Box-box edge-edge contact**: a single point at the true closest-pair of the
245
265
  two edges (P3.2), not the old body-centre midpoint. This is geometrically
246
- correct for a true edge-edge crossing; **multi-point** edge contacts (for
247
- near-parallel edges) remain a backlog refinement.
266
+ correct and an empirical SAT-source sweep confirms the edge-cross branch
267
+ *only* fires for **transverse** edge crossings (inter-edge angle ≈ 83-90°),
268
+ where two skew lines meet at a unique point. Near-parallel edge contacts
269
+ cannot reach this branch (a near-parallel `edgeA × edgeB` never wins the SAT
270
+ minimum) — they resolve through the multi-point face-clipping path. So the
271
+ once-planned "multi-point edge contact for near-parallel edges" refinement is
272
+ **moot**; see the resolved Stability backlog entry.
248
273
  - **CCD floor only**: speculative margin via the fattened AABB prevents
249
274
  most tunnelling. No per-body swept shape-cast for very fast objects.
250
275
  - **Cross-runtime determinism is not guaranteed**: `Math.sin/cos/exp/log`
@@ -530,28 +555,93 @@ scaffolding is in place.
530
555
  those primitives. Un-skipped the `narrowphase_concave.spec.js` ball-on-
531
556
  heightmap / mesh-cube settle tests and the `PhysicsSystem.spec.js`
532
557
  torus-knot test. Per-triangle GJK+EPA remains only as the fallback for
533
- *other* convex shapes vs triangles; `compute_penetration` keeps its
534
- half-space pre-test there (its closed-mesh over-report caveat stands for
535
- that fallback path).
536
- - [ ] **Edge-edge multi-point manifold** for near-parallel box edge contacts
537
- (the single closest-pair point from P3.2 is correct for a true edge-edge
538
- crossing; this is the multi-point refinement).
539
- - [ ] **Per-contact source-collider tracking** so multi-material compound
540
- bodies get accurate per-contact friction/restitution. Requires
541
- stashing the collider identity in the manifold contact stride.
558
+ *other* convex shapes vs triangles. `compute_penetration` now routes
559
+ through the shared narrowphase dispatch (`deepest_pair_penetration`), so it
560
+ uses the closed-form per-triangle solvers too — the old closed-mesh
561
+ over-report is gone; the half-space test is retained only as a
562
+ tunnel-recovery fallback.
563
+ - [x] **Edge-edge multi-point manifold** — *resolved by design (no code
564
+ change needed).* An empirical SAT-source sweep over a wide range of
565
+ box-box orientations shows the single-point edge-cross branch only ever
566
+ wins for **transverse** edge crossings (inter-edge angle 83-90°), where
567
+ a single closest-pair point is geometrically exact. A near-parallel edge
568
+ pair gives a near-degenerate `edgeA × edgeB` that never becomes the SAT
569
+ minimum, so near-parallel ("line") edge contacts resolve through the
570
+ multi-point **face-clipping** path instead — confirmed by regression
571
+ tests in `box_box_manifold.spec.js` (near-parallel tilted boxes → ≥ 2
572
+ points; transverse crossing → exactly 1 exact point). The originally
573
+ planned refinement targeted a case the geometry can't produce, so it is
574
+ closed rather than implemented.
575
+ - [x] **Per-contact source-collider tracking (materials)** — multi-material
576
+ compound bodies now get accurate per-contact friction / restitution. The
577
+ narrowphase combines the specific (colliderA, colliderB) pair's
578
+ coefficients at dispatch time (the only place that knows the source
579
+ collider on each side — `contact/combine_material.js`) and stamps them
580
+ into the manifold (CONTACT_STRIDE grown 14 → 16, offsets 14/15); the
581
+ solver reads them per contact instead of from the body's primary collider.
582
+ Regression test: an asymmetric-friction compound body yaws when shoved
583
+ (the grippy collider drags), and a symmetric control does not. Still
584
+ primary-collider-only: the contact-filter callback's collider args and the
585
+ body-level sensor / concave flags (smaller follow-up).
586
+ - [ ] **Joint-aware island sleep (ragdoll settle quality).** A draped,
587
+ self-colliding 10-joint ragdoll does not fully sleep in 10 s — surfaced by
588
+ a 1000-seed Monte-Carlo sweep (`PhysicsSystem.ragdoll.spec.js`, `.skip`):
589
+ for unlucky seeds a distal limb sustains a settled limit cycle (settled
590
+ finite-difference accel up to ~1094 m/s² / ~1479 rad/s² at a limb end vs a
591
+ ~55 m/s² median — bounded, non-growing, penetration-free, so a quality gap
592
+ not a divergence). The sleep test today is per-body `|v|²+|ω|²`; an island
593
+ over-constrained by cone-twist limits + self-contacts keeps small residual
594
+ jiggle above the per-body threshold so it never crosses into sleep.
595
+ Candidate fixes: sleep a jointed/contacting island on its AGGREGATE motion
596
+ rather than the per-body minimum, and/or a settled-regime relaxation (zero
597
+ restitution + extra position iterations) once an island's energy is low.
598
+ The sweep flags the worst seeds for replay. (Test infra also adds
599
+ per-point kinematics tracking — joint anchors + limb ends, with
600
+ displacement→velocity→acceleration and the angular equivalents.)
542
601
 
543
602
  ### Performance / Scale
544
603
  - [ ] **Per-body linear CCD shape-cast**: optional opt-in for fast-moving
545
604
  bodies where speculative margin isn't enough. The bench's falling
546
605
  tower (1km drop onto a 1cm floor) is the concrete reproducer —
547
606
  180 / 1000 bodies tunnel.
548
- - [ ] **Broadphase BVH balance / raycast traversal cost**: the raycast bench
549
- (`queries/raycast.bench.spec.js`) shows ~linear-in-N per-ray cost
550
- (~50 µs/ray at 500 bodies), i.e. a ray walks most of the tree — the static
551
- BVH built by sequential `link` inserts is poorly balanced. Orthogonal to
552
- raycast narrowphase (which adds only per-crossed-leaf refine, <1% here);
553
- affects every BVH query. Needs a balanced/refitting build (SAH or
554
- incremental rotation) on the static tree.
607
+ - [x] **Broadphase BVH balance SAH rotation.** The dynamic AABB tree
608
+ (`core/bvh2/bvh3/BVH.js`, a Box2D port) used SAH-cost insertion but a
609
+ *height-only* AVL rotation (`balance_height`): height-balanced yet not
610
+ SAH-balanced, so queries walked more nodes than needed. Replaced the
611
+ rotation in `bubble_up_update` with `balance_rotate` the Box2D-v3 /
612
+ Kensler SAH-reducing rotation (for node A with children B, C, evaluate the
613
+ four child↔grandchild swaps and apply the one that most reduces the
614
+ surface-area cost). Deterministic; identical pair set.
615
+ - Measured (same-session A/B, heavy benches): raycast **−9%**
616
+ (28.2→25.6 µs/ray), falling-tower median **−10%**, settling-grid
617
+ median **−12%**, and the **990/1000-churn stress −27%**
618
+ (63.95→46.68 ms mean over 10k ticks) — biggest where the tree churns
619
+ hardest. Determinism (8-trial bit-identical) holds.
620
+ - **Insertion cost (measured):** `balance_rotate` does 4 surface-area
621
+ evaluations per bubble-up level vs `balance_height`'s single height
622
+ compare, so *pure bulk insertion* is **~1.4–1.5× slower** — the 100k
623
+ synthetic insert bench (`BVH.spec.js`, drift-controlled interleaved
624
+ A/B) drops from **~37k → ~25k inserts/sec** (~27→~40 µs/insert). This
625
+ is the balancer's worst case (insert-only, zero queries/refits to
626
+ amortise against). It does not show up end-to-end: static trees are
627
+ built once then queried forever, dynamic bodies insert once then
628
+ refit/query every frame, and even the 990/1000-swap stress test — the
629
+ maximal insert-churn workload — is net **−27%**. Accepted.
630
+ - **Tradeoff (documented):** the contact solver's Gauss-Seidel order
631
+ follows broadphase traversal order (see `generate_pairs`), so the
632
+ different tree shape shifts convergence on near-aligned stacks — the
633
+ synthetic 128-cube wall now sleeps at ~10 s (was ~6.9 s). It still
634
+ settles, doesn't creep / topple (all bug-guard assertions hold); only
635
+ the sleep *time* moved (that test's budget was bumped 9→11 s with a
636
+ note). Random-shape scenes (falling tower) were faster *and* settled
637
+ fine.
638
+ - **Follow-up:** decouple the solve order from tree shape — sort the
639
+ broadphase pair list by `(idA, idB)` before narrowphase so contact
640
+ order is body-id-deterministic regardless of tree shape. Then no tree
641
+ change can affect convergence (and the stack settles identically under
642
+ either balancer). Has a per-step sort cost + wide test re-baseline, so
643
+ it's its own task. `balance_height` is retained for comparison /
644
+ fallback.
555
645
  - [ ] **Per-island parallel solve**: today's island data layout would
556
646
  allow worker-based solving once `SharedArrayBuffer` is available.
557
647
  Out-of-scope unless / until SAB is universally usable.
@@ -578,8 +668,19 @@ scaffolding is in place.
578
668
  - [ ] **Convex hull shape** with eigen-based principal-axes inertia
579
669
  derivation. Hooks `matrix_eigenvalues_in_place` from the existing
580
670
  linalg layer.
581
- - [ ] **Cylinder / cone shapes** (closed-form pairs against the existing
582
- family + GJK+EPA fallback for general convex).
671
+ - [~] **Cylinder / cone shapes.**
672
+ - [x] **`CylinderShape3D`** Y-aligned solid cylinder (radius + full
673
+ height, flat caps; the capsule's flat-cap sibling). Exact `support`,
674
+ capped-cylinder SDF, bounds, `contains` / `nearest_point` /
675
+ volume-sampling, equals/hash, `'cylinder'` JSON tag, `isCylinderShape3D`
676
+ marker. Convex → routes through the narrowphase **GJK + EPA** fallback
677
+ (no marker dispatch needed); spec asserts overlap-detected +
678
+ MTV-separates vs sphere/box. Closed-form cylinder-vs-X contact pairs
679
+ are a future refinement (the curved side is the usual smooth-support
680
+ EPA case — same status as pre-closed-form sphere/capsule).
681
+ - [ ] Closed-form cylinder contact pairs (cylinder × box / sphere / capsule
682
+ / plane) for multi-point cap manifolds + stable resting.
683
+ - [ ] **Cone shape** (+ closed-form / GJK fallback).
583
684
 
584
685
  ### API polish
585
686
  - [x] **`overlap(shape, position, rotation, output, output_offset,
@@ -598,8 +699,11 @@ scaffolding is in place.
598
699
  - [x] **`compute_penetration(out_direction, shape_a, pos_a, rot_a,
599
700
  shape_b, pos_b, rot_b)`** — standalone geometry primitive (no
600
701
  PhysicsSystem) for resolving overlap between two shapes at given
601
- poses. Returns depth + outward direction. Convex × convex via
602
- GJK + EPA; convex × concave via per-triangle half-space test.
702
+ poses. Returns depth + outward direction. **Hardened** to route through
703
+ the shared narrowphase dispatch (`deepest_pair_penetration`): exact
704
+ closed-form for sphere/box/capsule pairs (box-box via SAT), GJK+EPA for
705
+ general convex, closed-form per-triangle for convex × concave; the
706
+ half-space test is retained only for tunnel recovery.
603
707
 
604
708
  ### Raycast narrowphase (done)
605
709
 
@@ -19,11 +19,19 @@ export const MAX_CONTACTS_PER_MANIFOLD: number;
19
19
  * 13 : feature_id (uint32 packed as f64 — stable cross-frame ID of the
20
20
  * geometric feature pair that produced this contact;
21
21
  * 0 means "no info, fall back to position matching")
22
+ * 14 : friction (already-combined coefficient for THIS contact's
23
+ * specific source-collider pair — see combine_material)
24
+ * 15 : restitution(already-combined coefficient for this contact)
22
25
  *
23
26
  * Solver uses `(world_a + world_b) * 0.5` as the application point; storing
24
27
  * both surface points enables the per-frame warm-start matcher in the
25
28
  * narrowphase to compare contact-point positions when feature_id is 0.
26
29
  *
30
+ * Friction / restitution are stored **per contact** (not per body pair) so a
31
+ * compound body whose colliders have different materials gets the right
32
+ * coefficient at each contact: the narrowphase knows the exact source collider
33
+ * on each side of every contact and combines there.
34
+ *
27
35
  * @type {number}
28
36
  */
29
37
  export const CONTACT_STRIDE: number;
@@ -166,7 +174,8 @@ export class ManifoldStore {
166
174
  * Write a contact point into a slot. Increments contact_count if `idx`
167
175
  * exceeds the current count.
168
176
  *
169
- * Writes geometry (offsets 0..9) and the feature_id at offset 13.
177
+ * Writes geometry (offsets 0..9), the feature_id at offset 13, and the
178
+ * per-contact combined friction / restitution at offsets 14 / 15.
170
179
  * Deliberately does NOT touch the warm-start impulse fields (offsets
171
180
  * 10..12) — the narrowphase match-and-merge pass uses this to refill
172
181
  * a slot while preserving the cached impulses that the solver's
@@ -187,8 +196,25 @@ export class ManifoldStore {
187
196
  * @param {number} feature_id stable cross-frame identifier of the
188
197
  * geometric feature pair that produced this contact; 0 means
189
198
  * "no info, fall back to position matching"
199
+ * @param {number} friction combined friction for this contact's specific
200
+ * source-collider pair
201
+ * @param {number} restitution combined restitution for this contact
202
+ */
203
+ set_contact(slot: number, idx: number, lax: number, lay: number, laz: number, lbx: number, lby: number, lbz: number, nx: number, ny: number, nz: number, depth: number, feature_id?: number, friction?: number, restitution?: number): void;
204
+ /**
205
+ * Read the per-contact combined friction stored at one contact.
206
+ * @param {number} slot
207
+ * @param {number} idx
208
+ * @returns {number}
209
+ */
210
+ friction_of(slot: number, idx: number): number;
211
+ /**
212
+ * Read the per-contact combined restitution stored at one contact.
213
+ * @param {number} slot
214
+ * @param {number} idx
215
+ * @returns {number}
190
216
  */
191
- set_contact(slot: number, idx: number, lax: number, lay: number, laz: number, lbx: number, lby: number, lbz: number, nx: number, ny: number, nz: number, depth: number, feature_id?: number): void;
217
+ restitution_of(slot: number, idx: number): number;
192
218
  /**
193
219
  * Zero the warm-start impulse fields (j_n, j_t1, j_t2) for one contact
194
220
  * index inside a slot. Called by the narrowphase match-and-merge pass
@@ -1 +1 @@
1
- {"version":3,"file":"ManifoldStore.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/contact/ManifoldStore.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wCAFU,MAAM,CAE2B;AAE3C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,6BAFU,MAAM,CAEiB;AAEjC;;;;GAIG;AACH,wCAFU,MAAM,CAE4B;AAE5C;;;GAGG;AACH,+BAFU,MAAM,CAE2D;AAyB3E;;;GAGG;AACH,mCAFU,MAAM,CAEuB;AAIvC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IAEI;;OAEG;IACH,+BAFW,MAAM,EA6BhB;IAzBG,mBAA+C;IAC/C,qBAAqB;IAErB,qBAAkE;IAClE,oBAAiE;IAQjE,0BAAwD;IAExD,4BAA0D;IAG1D,0BAAoD;IACpD,uBAAiD;IAEjD,qBAAqB;IAGrB,yBAAmD;IACnD,qBAAqB;IAGzB;;OAEG;IACH,oBAEC;IAED;;OAEG;IACH,uBAEC;IAED;;;;;;;OAOG;IACH,UAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAKlB;IAED;;;;;;;;OAQG;IACH,aAJW,MAAM,OACN,MAAM,GACJ,MAAM,CA+BlB;IAED;;;OAGG;IACH,YAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,YAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,oBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,iBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;OAGG;IACH,uBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;OAKG;IACH,WAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;OAKG;IACH,qBAFW,MAAM,QAQhB;IAED;;;;;;;;;;;;;OAaG;IACH,mBAFW,MAAM,QAKhB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,kBAhBW,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,SACN,MAAM,eACN,MAAM,QAwBhB;IAED;;;;;;;;;;;;;;;OAeG;IACH,qBAHW,MAAM,OACN,MAAM,QAOhB;IAED;;;;;OAKG;IACH,oBAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;OAMG;IACH,kBALW,MAAM,OACN,MAAM,OACN,MAAM,EAAE,GAAC,YAAY,cACrB,MAAM,QAOhB;IAED;;;;;;OAMG;IACH,uBAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED,iEAAiE;IACjE,uBADY,MAAM,OAAe,MAAM,GAAgB,MAAM,CAG5D;IAED,iEAAiE;IACjE,uBADY,MAAM,OAAe,MAAM,GAAgB,MAAM,CAG5D;IAED,iEAAiE;IACjE,oBADY,MAAM,OAAe,MAAM,GAAgB,MAAM,CAG5D;IAED;;;;;OAKG;IACH,gCAEC;IAED;;;;;;OAMG;IACH,qCAEC;IAED;;;;;OAKG;IACH,uBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;OAMG;IACH,uBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,sBA6BC;IAED;;;OAGG;IACH,wBAcC;IAED;;;OAGG;IACH,uBAkBC;IAED;;OAEG;IACH,eA+BC;IAED;;;OAGG;IACH,oBAaC;IAED;;;OAGG;IACH,mBAqBC;CACJ;8BA9lBqD,2CAA2C"}
1
+ {"version":3,"file":"ManifoldStore.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/contact/ManifoldStore.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wCAFU,MAAM,CAE2B;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,6BAFU,MAAM,CAEiB;AAEjC;;;;GAIG;AACH,wCAFU,MAAM,CAE4B;AAE5C;;;GAGG;AACH,+BAFU,MAAM,CAE2D;AAyB3E;;;GAGG;AACH,mCAFU,MAAM,CAEuB;AAIvC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IAEI;;OAEG;IACH,+BAFW,MAAM,EA6BhB;IAzBG,mBAA+C;IAC/C,qBAAqB;IAErB,qBAAkE;IAClE,oBAAiE;IAQjE,0BAAwD;IAExD,4BAA0D;IAG1D,0BAAoD;IACpD,uBAAiD;IAEjD,qBAAqB;IAGrB,yBAAmD;IACnD,qBAAqB;IAGzB;;OAEG;IACH,oBAEC;IAED;;OAEG;IACH,uBAEC;IAED;;;;;;;OAOG;IACH,UAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAKlB;IAED;;;;;;;;OAQG;IACH,aAJW,MAAM,OACN,MAAM,GACJ,MAAM,CA+BlB;IAED;;;OAGG;IACH,YAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,YAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,oBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,iBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;OAGG;IACH,uBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;OAKG;IACH,WAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;OAKG;IACH,qBAFW,MAAM,QAQhB;IAED;;;;;;;;;;;;;OAaG;IACH,mBAFW,MAAM,QAKhB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,kBAnBW,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,SACN,MAAM,eACN,MAAM,aAGN,MAAM,gBAEN,MAAM,QAwBhB;IAED;;;;;OAKG;IACH,kBAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;OAKG;IACH,qBAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;;;;;;;;;;OAeG;IACH,qBAHW,MAAM,OACN,MAAM,QAOhB;IAED;;;;;OAKG;IACH,oBAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;OAMG;IACH,kBALW,MAAM,OACN,MAAM,OACN,MAAM,EAAE,GAAC,YAAY,cACrB,MAAM,QAOhB;IAED;;;;;;OAMG;IACH,uBAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED,iEAAiE;IACjE,uBADY,MAAM,OAAe,MAAM,GAAgB,MAAM,CAG5D;IAED,iEAAiE;IACjE,uBADY,MAAM,OAAe,MAAM,GAAgB,MAAM,CAG5D;IAED,iEAAiE;IACjE,oBADY,MAAM,OAAe,MAAM,GAAgB,MAAM,CAG5D;IAED;;;;;OAKG;IACH,gCAEC;IAED;;;;;;OAMG;IACH,qCAEC;IAED;;;;;OAKG;IACH,uBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;OAMG;IACH,uBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,sBA6BC;IAED;;;OAGG;IACH,wBAcC;IAED;;;OAGG;IACH,uBAkBC;IAED;;OAEG;IACH,eA+BC;IAED;;;OAGG;IACH,oBAaC;IAED;;;OAGG;IACH,mBAqBC;CACJ;8BAhoBqD,2CAA2C"}
@@ -23,14 +23,22 @@ export const MAX_CONTACTS_PER_MANIFOLD = 4;
23
23
  * 13 : feature_id (uint32 packed as f64 — stable cross-frame ID of the
24
24
  * geometric feature pair that produced this contact;
25
25
  * 0 means "no info, fall back to position matching")
26
+ * 14 : friction (already-combined coefficient for THIS contact's
27
+ * specific source-collider pair — see combine_material)
28
+ * 15 : restitution(already-combined coefficient for this contact)
26
29
  *
27
30
  * Solver uses `(world_a + world_b) * 0.5` as the application point; storing
28
31
  * both surface points enables the per-frame warm-start matcher in the
29
32
  * narrowphase to compare contact-point positions when feature_id is 0.
30
33
  *
34
+ * Friction / restitution are stored **per contact** (not per body pair) so a
35
+ * compound body whose colliders have different materials gets the right
36
+ * coefficient at each contact: the narrowphase knows the exact source collider
37
+ * on each side of every contact and combines there.
38
+ *
31
39
  * @type {number}
32
40
  */
33
- export const CONTACT_STRIDE = 14;
41
+ export const CONTACT_STRIDE = 16;
34
42
 
35
43
  /**
36
44
  * Per-contact feature_id offset within {@link CONTACT_STRIDE}.
@@ -286,7 +294,8 @@ export class ManifoldStore {
286
294
  * Write a contact point into a slot. Increments contact_count if `idx`
287
295
  * exceeds the current count.
288
296
  *
289
- * Writes geometry (offsets 0..9) and the feature_id at offset 13.
297
+ * Writes geometry (offsets 0..9), the feature_id at offset 13, and the
298
+ * per-contact combined friction / restitution at offsets 14 / 15.
290
299
  * Deliberately does NOT touch the warm-start impulse fields (offsets
291
300
  * 10..12) — the narrowphase match-and-merge pass uses this to refill
292
301
  * a slot while preserving the cached impulses that the solver's
@@ -307,8 +316,11 @@ export class ManifoldStore {
307
316
  * @param {number} feature_id stable cross-frame identifier of the
308
317
  * geometric feature pair that produced this contact; 0 means
309
318
  * "no info, fall back to position matching"
319
+ * @param {number} friction combined friction for this contact's specific
320
+ * source-collider pair
321
+ * @param {number} restitution combined restitution for this contact
310
322
  */
311
- set_contact(slot, idx, lax, lay, laz, lbx, lby, lbz, nx, ny, nz, depth, feature_id = 0) {
323
+ set_contact(slot, idx, lax, lay, laz, lbx, lby, lbz, nx, ny, nz, depth, feature_id = 0, friction = 0, restitution = 0) {
312
324
  const off = slot * SLOT_DATA_STRIDE + idx * CONTACT_STRIDE;
313
325
  this.__data[off] = lax;
314
326
  this.__data[off + 1] = lay;
@@ -322,6 +334,8 @@ export class ManifoldStore {
322
334
  this.__data[off + 9] = depth;
323
335
  // j_n, j_t1, j_t2 (offsets 10..12) are warm-start; preserved across calls.
324
336
  this.__data[off + 13] = feature_id;
337
+ this.__data[off + 14] = friction;
338
+ this.__data[off + 15] = restitution;
325
339
 
326
340
  const meta_off = slot * SLOT_META_STRIDE;
327
341
  const count_now = this.__meta[meta_off + 2] & COUNT_MASK;
@@ -330,6 +344,26 @@ export class ManifoldStore {
330
344
  }
331
345
  }
332
346
 
347
+ /**
348
+ * Read the per-contact combined friction stored at one contact.
349
+ * @param {number} slot
350
+ * @param {number} idx
351
+ * @returns {number}
352
+ */
353
+ friction_of(slot, idx) {
354
+ return this.__data[slot * SLOT_DATA_STRIDE + idx * CONTACT_STRIDE + 14];
355
+ }
356
+
357
+ /**
358
+ * Read the per-contact combined restitution stored at one contact.
359
+ * @param {number} slot
360
+ * @param {number} idx
361
+ * @returns {number}
362
+ */
363
+ restitution_of(slot, idx) {
364
+ return this.__data[slot * SLOT_DATA_STRIDE + idx * CONTACT_STRIDE + 15];
365
+ }
366
+
333
367
  /**
334
368
  * Zero the warm-start impulse fields (j_n, j_t1, j_t2) for one contact
335
369
  * index inside a slot. Called by the narrowphase match-and-merge pass
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Surface-material combine rules — how the friction / restitution of the two
3
+ * colliders in contact are merged into the single coefficient the solver uses
4
+ * for that contact.
5
+ *
6
+ * These live in a shared leaf module because the combine happens in the
7
+ * **narrowphase** (which knows the specific source collider on each side of
8
+ * every contact, so a compound body's per-collider materials are honoured
9
+ * per-contact) and the result is stored in the manifold and read by the
10
+ * **solver**. Both sides import from here, keeping one source of truth for the
11
+ * policy.
12
+ *
13
+ * @author Alex Goldring
14
+ * @copyright Company Named Limited (c) 2026
15
+ */
16
+ /**
17
+ * Combine two friction coefficients — geometric mean (Bullet / PhysX default).
18
+ * @param {number} a
19
+ * @param {number} b
20
+ * @returns {number}
21
+ */
22
+ export function combine_friction(a: number, b: number): number;
23
+ /**
24
+ * Combine two restitution coefficients — maximum (Unity / common default).
25
+ * @param {number} a
26
+ * @param {number} b
27
+ * @returns {number}
28
+ */
29
+ export function combine_restitution(a: number, b: number): number;
30
+ //# sourceMappingURL=combine_material.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"combine_material.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/contact/combine_material.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AACH,oCAJW,MAAM,KACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;;GAKG;AACH,uCAJW,MAAM,KACN,MAAM,GACJ,MAAM,CAIlB"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Surface-material combine rules — how the friction / restitution of the two
3
+ * colliders in contact are merged into the single coefficient the solver uses
4
+ * for that contact.
5
+ *
6
+ * These live in a shared leaf module because the combine happens in the
7
+ * **narrowphase** (which knows the specific source collider on each side of
8
+ * every contact, so a compound body's per-collider materials are honoured
9
+ * per-contact) and the result is stored in the manifold and read by the
10
+ * **solver**. Both sides import from here, keeping one source of truth for the
11
+ * policy.
12
+ *
13
+ * @author Alex Goldring
14
+ * @copyright Company Named Limited (c) 2026
15
+ */
16
+
17
+ /**
18
+ * Combine two friction coefficients — geometric mean (Bullet / PhysX default).
19
+ * @param {number} a
20
+ * @param {number} b
21
+ * @returns {number}
22
+ */
23
+ export function combine_friction(a, b) {
24
+ return Math.sqrt(a * b);
25
+ }
26
+
27
+ /**
28
+ * Combine two restitution coefficients — maximum (Unity / common default).
29
+ * @param {number} a
30
+ * @param {number} b
31
+ * @returns {number}
32
+ */
33
+ export function combine_restitution(a, b) {
34
+ return a > b ? a : b;
35
+ }
@@ -84,6 +84,21 @@ export class Collider {
84
84
  flags: number;
85
85
  };
86
86
  fromJSON(json: any): void;
87
+ /**
88
+ * Value equality over the shape and the surface material. System-private
89
+ * fields (`_bvhNode`, `_bodyId`) are excluded. Shapes are compared by value
90
+ * (`shape.equals`), so two colliders sharing one shape instance or holding
91
+ * equal shapes both compare equal.
92
+ *
93
+ * @param {Collider} other
94
+ * @returns {boolean}
95
+ */
96
+ equals(other: Collider): boolean;
97
+ /**
98
+ * Hash over the same state as {@link equals}. Equal colliders hash equal.
99
+ * @returns {number}
100
+ */
101
+ hash(): number;
87
102
  /**
88
103
  * @readonly
89
104
  * @type {boolean}
@@ -1 +1 @@
1
- {"version":3,"file":"Collider.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/Collider.js"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,+BAFU,MAAM,CAEmB;AAEnC;;;;;;;;;;GAUG;AACH;IAEI;;;OAGG;IACH,OAFU,eAAe,CAEU;IAEnC;;;;OAIG;IACH,UAFU,MAAM,CAED;IAEf;;;;OAIG;IACH,aAFU,MAAM,CAEA;IAEhB;;;;OAIG;IACH,SAFU,MAAM,CAEJ;IAEZ;;OAEG;IACH,OAFU,aAAa,GAAC,MAAM,CAEH;IAE3B;;;OAGG;IACH,UAFU,MAAM,CAEY;IAE5B;;;;OAIG;IACH,SAFU,MAAM,CAEH;IAEb;;;OAGG;IACH,cAFW,MAAM,GAAC,aAAa,QAI9B;IAED;;;OAGG;IACH,gBAFW,MAAM,GAAC,aAAa,QAI9B;IAED;;;;OAIG;IACH,cAHW,MAAM,GAAC,aAAa,GAClB,OAAO,CAInB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GAAC,aAAa,SACpB,OAAO,QAQjB;IAED;;;;;MAOC;IAED,0BAKC;IASL;;;OAGG;IACH,qBAFU,OAAO,CAEY;CAZ5B;;kBAIS,MAAM;;gCA/HgB,gDAAgD;8BAElD,oBAAoB"}
1
+ {"version":3,"file":"Collider.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/Collider.js"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,+BAFU,MAAM,CAEmB;AAEnC;;;;;;;;;;GAUG;AACH;IAEI;;;OAGG;IACH,OAFU,eAAe,CAEU;IAEnC;;;;OAIG;IACH,UAFU,MAAM,CAED;IAEf;;;;OAIG;IACH,aAFU,MAAM,CAEA;IAEhB;;;;OAIG;IACH,SAFU,MAAM,CAEJ;IAEZ;;OAEG;IACH,OAFU,aAAa,GAAC,MAAM,CAEH;IAE3B;;;OAGG;IACH,UAFU,MAAM,CAEY;IAE5B;;;;OAIG;IACH,SAFU,MAAM,CAEH;IAEb;;;OAGG;IACH,cAFW,MAAM,GAAC,aAAa,QAI9B;IAED;;;OAGG;IACH,gBAFW,MAAM,GAAC,aAAa,QAI9B;IAED;;;;OAIG;IACH,cAHW,MAAM,GAAC,aAAa,GAClB,OAAO,CAInB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GAAC,aAAa,SACpB,OAAO,QAQjB;IAED;;;;;MAOC;IAED,0BAKC;IAED;;;;;;;;OAQG;IACH,cAHW,QAAQ,GACN,OAAO,CAWnB;IAED;;;OAGG;IACH,QAFa,MAAM,CASlB;IASL;;;OAGG;IACH,qBAFU,OAAO,CAEY;CAZ5B;;kBAIS,MAAM;;gCAhKgB,gDAAgD;8BAElD,oBAAoB"}
@@ -1,3 +1,4 @@
1
+ import { computeHashFloat } from "../../../core/primitives/numbers/computeHashFloat.js";
1
2
  import { AbstractShape3D } from "../../../core/geom/3d/shape/AbstractShape3D.js";
2
3
  import { UnitSphereShape3D } from "../../../core/geom/3d/shape/UnitSphereShape3D.js";
3
4
  import { ColliderFlags } from "./ColliderFlags.js";
@@ -121,6 +122,39 @@ export class Collider {
121
122
  if (json.density !== undefined) this.density = json.density;
122
123
  if (json.flags !== undefined) this.flags = json.flags;
123
124
  }
125
+
126
+ /**
127
+ * Value equality over the shape and the surface material. System-private
128
+ * fields (`_bvhNode`, `_bodyId`) are excluded. Shapes are compared by value
129
+ * (`shape.equals`), so two colliders sharing one shape instance or holding
130
+ * equal shapes both compare equal.
131
+ *
132
+ * @param {Collider} other
133
+ * @returns {boolean}
134
+ */
135
+ equals(other) {
136
+ if (other === this) return true;
137
+ if (other === null || other === undefined || other.isCollider !== true) return false;
138
+
139
+ return this.friction === other.friction
140
+ && this.restitution === other.restitution
141
+ && this.density === other.density
142
+ && this.flags === other.flags
143
+ && this.shape.equals(other.shape);
144
+ }
145
+
146
+ /**
147
+ * Hash over the same state as {@link equals}. Equal colliders hash equal.
148
+ * @returns {number}
149
+ */
150
+ hash() {
151
+ let h = this.shape.hash() | 0;
152
+ h = (h * 31 + computeHashFloat(this.friction)) | 0;
153
+ h = (h * 31 + computeHashFloat(this.restitution)) | 0;
154
+ h = (h * 31 + computeHashFloat(this.density)) | 0;
155
+ h = (h * 31 + (this.flags | 0)) | 0;
156
+ return h;
157
+ }
124
158
  }
125
159
 
126
160
  /**
@@ -280,6 +280,24 @@ export class Joint {
280
280
  * @returns {Joint} this
281
281
  */
282
282
  setAngularSpring(axis: number, stiffness: number, damping: number): Joint;
283
+ /**
284
+ * Value equality over the configurable / persistent state — the same field
285
+ * set that {@link JointSerializationAdapter} writes: the two anchor
286
+ * entities, the local anchor points and basis frames, the swing-twist flag,
287
+ * and the six per-DOF mode + limit / spring / motor arrays. Transient
288
+ * solver state (`dofImpulse`) and the system-resolved handles (`_bodyIdA`,
289
+ * `_bodyIdB`, `_jointId`) are excluded.
290
+ *
291
+ * @param {Joint} other
292
+ * @returns {boolean}
293
+ */
294
+ equals(other: Joint): boolean;
295
+ /**
296
+ * Hash over the same persistent state as {@link equals}. Equal joints hash
297
+ * equal.
298
+ * @returns {number}
299
+ */
300
+ hash(): number;
283
301
  /**
284
302
  * @readonly
285
303
  * @type {boolean}