Skip to main content

oxiphysics_core/
traits.rs

1#![allow(clippy::ptr_arg)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Core simulation traits for the OxiPhysics engine.
6//!
7//! This module defines the fundamental behavioural contracts used throughout
8//! the engine.  All physics objects implement one or more of these traits so
9//! that solvers, integrators, and broadphase structures can operate on them
10//! generically.
11
12#![allow(dead_code)]
13#![allow(clippy::too_many_arguments)]
14
15use crate::math::Real;
16use crate::types::{Aabb, TimeStep, Transform};
17
18// ---------------------------------------------------------------------------
19// Time integration
20// ---------------------------------------------------------------------------
21
22/// Trait for objects that can be stepped forward in time.
23pub trait Steppable {
24    /// Advance the simulation by one time step.
25    fn step(&mut self, time_step: &TimeStep);
26}
27
28// ---------------------------------------------------------------------------
29// Physics body
30// ---------------------------------------------------------------------------
31
32/// Trait for objects that represent a physics body.
33pub trait PhysicsBody {
34    /// Return the current transform of this body.
35    fn transform(&self) -> &Transform;
36
37    /// Return a mutable reference to the transform.
38    fn transform_mut(&mut self) -> &mut Transform;
39
40    /// Return the mass of this body (kg). Returns 0 for static/kinematic.
41    fn mass(&self) -> Real;
42
43    /// Return the inverse mass. Returns 0 for static/kinematic.
44    fn inv_mass(&self) -> Real {
45        if self.mass() > 0.0 {
46            1.0 / self.mass()
47        } else {
48            0.0
49        }
50    }
51
52    /// Whether this body participates in dynamics integration.
53    fn is_dynamic(&self) -> bool {
54        self.mass() > 0.0
55    }
56
57    /// Whether this body is treated as immovable.
58    fn is_static(&self) -> bool {
59        self.mass() == 0.0
60    }
61}
62
63// ---------------------------------------------------------------------------
64// Collision
65// ---------------------------------------------------------------------------
66
67/// Trait for objects that can participate in collision detection.
68pub trait Collidable {
69    /// Return the axis-aligned bounding box of this object.
70    fn bounding_box(&self) -> Aabb;
71
72    /// Whether this object can interact with another.
73    fn can_collide_with(&self, _other: &dyn Collidable) -> bool {
74        true
75    }
76}
77
78// ---------------------------------------------------------------------------
79// Force source
80// ---------------------------------------------------------------------------
81
82/// Trait for objects that apply forces to bodies (gravity fields, springs, etc.).
83pub trait ForceSource {
84    /// Compute the force (and optionally torque) acting on a body at the
85    /// given transform with the given velocity.
86    ///
87    /// Returns `(force, torque)` both as `[f64; 3]` in world space.
88    fn force_at(
89        &self,
90        transform: &Transform,
91        velocity: [Real; 3],
92        mass: Real,
93    ) -> ([Real; 3], [Real; 3]);
94}
95
96// ---------------------------------------------------------------------------
97// Constraint
98// ---------------------------------------------------------------------------
99
100/// Trait for position/velocity-level constraints between bodies.
101pub trait PhysicsConstraint {
102    /// Number of degrees of freedom constrained (1–6).
103    fn dof(&self) -> usize;
104
105    /// Evaluate the positional violation of this constraint.
106    /// Returns a slice of `dof()` scalar violations.
107    fn positional_error(&self) -> Vec<Real>;
108
109    /// Whether this constraint is currently active.
110    fn is_active(&self) -> bool;
111
112    /// Compliance of the constraint (XPBD softness). 0 = rigid.
113    fn compliance(&self) -> Real {
114        0.0
115    }
116}
117
118// ---------------------------------------------------------------------------
119// Integrator
120// ---------------------------------------------------------------------------
121
122/// Trait for numerical time integrators.
123pub trait Integrator {
124    /// Advance state `q` (positions) and `v` (velocities) by `dt` given
125    /// forces `f`.
126    ///
127    /// `dof` is the number of degrees of freedom.
128    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real);
129}
130
131/// Semi-implicit (symplectic) Euler integrator.
132///
133/// Updates velocity first, then position:
134/// ```text
135/// v_{n+1} = v_n + (f_n / m) * dt
136/// q_{n+1} = q_n + v_{n+1} * dt
137/// ```
138pub struct SemiImplicitEuler;
139
140impl Integrator for SemiImplicitEuler {
141    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
142        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
143        for i in 0..n {
144            v[i] += f[i] * inv_mass[i] * dt;
145            q[i] += v[i] * dt;
146        }
147    }
148}
149
150/// Classic explicit (forward) Euler integrator.
151///
152/// Updates position first using *old* velocity, then updates velocity:
153/// ```text
154/// q_{n+1} = q_n + v_n * dt
155/// v_{n+1} = v_n + (f_n / m) * dt
156/// ```
157pub struct ExplicitEuler;
158
159impl Integrator for ExplicitEuler {
160    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
161        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
162        for i in 0..n {
163            q[i] += v[i] * dt;
164            v[i] += f[i] * inv_mass[i] * dt;
165        }
166    }
167}
168
169/// 4th-order Runge-Kutta integrator (position only, given an acceleration field).
170///
171/// Uses `f` as the acceleration (force/mass) directly.
172pub struct RungeKutta4;
173
174impl RungeKutta4 {
175    /// Integrate positions `q` with velocities `v` under accelerations `a = f * inv_mass`,
176    /// using the classic RK4 scheme.  Both `q` and `v` are updated.
177    pub fn integrate_rk4(q: &mut [Real], v: &mut [Real], accel: &[Real], dt: Real) {
178        let n = q.len().min(v.len()).min(accel.len());
179        let h = dt;
180        let h2 = h / 2.0;
181        let h6 = h / 6.0;
182        // k1 = (v, a)
183        // k2 = (v + h/2*a, a)   (constant acceleration assumption)
184        // k3 = (v + h/2*a, a)
185        // k4 = (v + h*a, a)
186        for i in 0..n {
187            let a = accel[i];
188            // Velocity increments
189            let kv1 = a;
190            let kv2 = a; // constant force assumption
191            let kv3 = a;
192            let kv4 = a;
193            // Position increments
194            let kq1 = v[i];
195            let kq2 = v[i] + h2 * kv1;
196            let kq3 = v[i] + h2 * kv2;
197            let kq4 = v[i] + h * kv3;
198            v[i] += h6 * (kv1 + 2.0 * kv2 + 2.0 * kv3 + kv4);
199            q[i] += h6 * (kq1 + 2.0 * kq2 + 2.0 * kq3 + kq4);
200        }
201    }
202}
203
204impl Integrator for RungeKutta4 {
205    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
206        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
207        let accel: Vec<Real> = (0..n).map(|i| f[i] * inv_mass[i]).collect();
208        Self::integrate_rk4(q, v, &accel, dt);
209    }
210}
211
212// ---------------------------------------------------------------------------
213// Broadphase
214// ---------------------------------------------------------------------------
215
216/// Trait for broadphase collision detection structures.
217pub trait Broadphase {
218    /// Insert an object with the given id and AABB.
219    fn insert(&mut self, id: u64, aabb: Aabb);
220
221    /// Update the AABB for an existing object.
222    fn update(&mut self, id: u64, aabb: Aabb);
223
224    /// Remove an object.
225    fn remove(&mut self, id: u64);
226
227    /// Return all pairs of overlapping ids.
228    fn overlapping_pairs(&self) -> Vec<(u64, u64)>;
229
230    /// Return all ids whose AABB overlaps the query AABB.
231    fn query_aabb(&self, query: &Aabb) -> Vec<u64>;
232}
233
234// ---------------------------------------------------------------------------
235// Narrowphase
236// ---------------------------------------------------------------------------
237
238/// A contact point produced by narrowphase collision detection.
239#[derive(Debug, Clone)]
240pub struct ContactPoint {
241    /// Contact point in world space.
242    pub point: [Real; 3],
243    /// Contact normal pointing from body B into body A.
244    pub normal: [Real; 3],
245    /// Penetration depth (positive = penetrating).
246    pub depth: Real,
247}
248
249/// Trait for narrowphase collision queries.
250pub trait Narrowphase {
251    /// Test whether two convex shapes (described by their transforms) overlap.
252    fn test_overlap(&self, transform_a: &Transform, transform_b: &Transform) -> bool;
253
254    /// Compute contact points between two shapes.
255    fn contacts(&self, transform_a: &Transform, transform_b: &Transform) -> Vec<ContactPoint>;
256}
257
258// ---------------------------------------------------------------------------
259// Serialisable physics state
260// ---------------------------------------------------------------------------
261
262/// Trait for objects whose full physics state can be captured and restored.
263pub trait PhysicsStateful {
264    /// The type representing the saved state.
265    type State;
266
267    /// Capture the current state.
268    fn save_state(&self) -> Self::State;
269
270    /// Restore from a previously saved state.
271    fn restore_state(&mut self, state: Self::State);
272}
273
274// ---------------------------------------------------------------------------
275// Damping
276// ---------------------------------------------------------------------------
277
278/// Trait for objects that model energy dissipation.
279pub trait Damped {
280    /// Return the linear damping coefficient.
281    fn linear_damping(&self) -> Real;
282
283    /// Return the angular damping coefficient.
284    fn angular_damping(&self) -> Real;
285
286    /// Compute damped velocity components from current velocity over `dt`.
287    fn damp_velocity(&self, velocity: Real, dt: Real, is_angular: bool) -> Real {
288        let d = if is_angular {
289            self.angular_damping()
290        } else {
291            self.linear_damping()
292        };
293        velocity * (1.0 - d * dt).max(0.0)
294    }
295}
296
297// ---------------------------------------------------------------------------
298// Sleeping / activation
299// ---------------------------------------------------------------------------
300
301/// Trait for bodies that can sleep when below activity thresholds.
302pub trait Sleepable {
303    /// Whether the body is currently sleeping.
304    fn is_sleeping(&self) -> bool;
305
306    /// Wake the body from sleep.
307    fn wake(&mut self);
308
309    /// Put the body to sleep, zeroing velocities.
310    fn sleep(&mut self);
311
312    /// Test whether the body should fall asleep given its speed squared and
313    /// the world's thresholds.
314    fn should_sleep(
315        &self,
316        speed_sq: Real,
317        threshold_sq: Real,
318        accumulated_sleep_time: Real,
319        time_to_sleep: Real,
320    ) -> bool {
321        speed_sq < threshold_sq && accumulated_sleep_time >= time_to_sleep
322    }
323}
324
325// ---------------------------------------------------------------------------
326// Renderer output
327// ---------------------------------------------------------------------------
328
329/// Trait for physics objects that can provide render-ready data.
330pub trait Renderable {
331    /// Return the interpolated world-space transform for rendering at
332    /// sub-step fraction `alpha` ∈ \[0,1\].
333    fn render_transform(&self, alpha: Real) -> Transform;
334}
335
336// ---------------------------------------------------------------------------
337// Physics material
338// ---------------------------------------------------------------------------
339
340/// Physical surface properties.
341#[derive(Debug, Clone)]
342pub struct PhysicsMaterial {
343    /// Coefficient of static friction.
344    pub static_friction: Real,
345    /// Coefficient of dynamic friction.
346    pub dynamic_friction: Real,
347    /// Restitution (bounciness), 0 = perfectly inelastic, 1 = perfectly elastic.
348    pub restitution: Real,
349    /// Rolling friction coefficient (for spheres/cylinders).
350    pub rolling_friction: Real,
351}
352
353impl Default for PhysicsMaterial {
354    fn default() -> Self {
355        Self {
356            static_friction: 0.5,
357            dynamic_friction: 0.4,
358            restitution: 0.3,
359            rolling_friction: 0.01,
360        }
361    }
362}
363
364impl PhysicsMaterial {
365    /// Create a rubber-like material.
366    pub fn rubber() -> Self {
367        Self {
368            static_friction: 1.0,
369            dynamic_friction: 0.8,
370            restitution: 0.7,
371            rolling_friction: 0.02,
372        }
373    }
374
375    /// Create a steel-on-steel material.
376    pub fn steel() -> Self {
377        Self {
378            static_friction: 0.74,
379            dynamic_friction: 0.57,
380            restitution: 0.6,
381            rolling_friction: 0.001,
382        }
383    }
384
385    /// Create a frictionless ice-like material.
386    pub fn ice() -> Self {
387        Self {
388            static_friction: 0.03,
389            dynamic_friction: 0.02,
390            restitution: 0.05,
391            rolling_friction: 0.001,
392        }
393    }
394
395    /// Combine two materials using geometric mean for friction and min for restitution
396    /// (a common convention in game physics).
397    pub fn combine(&self, other: &PhysicsMaterial) -> PhysicsMaterial {
398        PhysicsMaterial {
399            static_friction: (self.static_friction * other.static_friction).sqrt(),
400            dynamic_friction: (self.dynamic_friction * other.dynamic_friction).sqrt(),
401            restitution: self.restitution.min(other.restitution),
402            rolling_friction: (self.rolling_friction * other.rolling_friction).sqrt(),
403        }
404    }
405}
406
407/// Trait for objects that expose a surface material.
408pub trait HasMaterial {
409    /// Return the physics material for this object's surface.
410    fn material(&self) -> &PhysicsMaterial;
411}
412
413// ---------------------------------------------------------------------------
414// Debug / diagnostics
415// ---------------------------------------------------------------------------
416
417/// Trait for physics objects that can emit diagnostic information.
418pub trait PhysicsDiagnostics {
419    /// Return a vector of (name, value) pairs for debug display.
420    fn diagnostics(&self) -> Vec<(&'static str, Real)>;
421}
422
423// ---------------------------------------------------------------------------
424// Tests
425// ---------------------------------------------------------------------------
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    use crate::VelocityVerlet;
432
433    use crate::fabrik_solve;
434
435    // ── SemiImplicitEuler ─────────────────────────────────────────────────────
436
437    #[test]
438    fn test_semi_implicit_euler_constant_force() {
439        let integrator = SemiImplicitEuler;
440        let mut q = [0.0f64];
441        let mut v = [0.0f64];
442        let f = [1.0f64]; // constant force
443        let inv_mass = [1.0f64]; // mass = 1 kg
444        integrator.integrate(&mut q, &mut v, &f, &inv_mass, 1.0);
445        // v = 0 + 1*1 = 1; q = 0 + 1*1 = 1
446        assert!((v[0] - 1.0).abs() < 1e-12, "v={}", v[0]);
447        assert!((q[0] - 1.0).abs() < 1e-12, "q={}", q[0]);
448    }
449
450    #[test]
451    fn test_semi_implicit_euler_zero_force() {
452        let integrator = SemiImplicitEuler;
453        let mut q = [5.0f64];
454        let mut v = [2.0f64];
455        let f = [0.0f64];
456        let inv_mass = [1.0f64];
457        integrator.integrate(&mut q, &mut v, &f, &inv_mass, 0.5);
458        // v unchanged = 2; q = 5 + 2*0.5 = 6
459        assert!((v[0] - 2.0).abs() < 1e-12);
460        assert!((q[0] - 6.0).abs() < 1e-12);
461    }
462
463    // ── ExplicitEuler ─────────────────────────────────────────────────────────
464
465    #[test]
466    fn test_explicit_euler_differs_from_semi_implicit() {
467        // With a non-zero force the two integrators give different results.
468        let f = [2.0f64];
469        let inv_mass = [1.0f64];
470        let dt = 1.0;
471
472        let mut q_si = [0.0f64];
473        let mut v_si = [0.0f64];
474        SemiImplicitEuler.integrate(&mut q_si, &mut v_si, &f, &inv_mass, dt);
475
476        let mut q_ex = [0.0f64];
477        let mut v_ex = [0.0f64];
478        ExplicitEuler.integrate(&mut q_ex, &mut v_ex, &f, &inv_mass, dt);
479
480        // Semi-implicit: v=2, q=2; Explicit: q=0 first, v=2
481        assert!((v_si[0] - 2.0).abs() < 1e-12);
482        assert!((q_si[0] - 2.0).abs() < 1e-12);
483        assert!((v_ex[0] - 2.0).abs() < 1e-12);
484        assert!((q_ex[0] - 0.0).abs() < 1e-12); // position integrated with OLD v=0
485    }
486
487    // ── RungeKutta4 ───────────────────────────────────────────────────────────
488
489    #[test]
490    fn test_rk4_constant_acceleration() {
491        // Under constant acceleration a=2, starting from rest:
492        // q(dt) = ½*a*dt², v(dt) = a*dt
493        let mut q = [0.0f64];
494        let mut v = [0.0f64];
495        let a = [2.0f64];
496        let inv_mass = [1.0f64];
497        RungeKutta4.integrate(&mut q, &mut v, &a, &inv_mass, 1.0);
498        assert!((v[0] - 2.0).abs() < 1e-10, "v={}", v[0]);
499        // RK4 with constant a gives q = v0*dt + a*dt²/2 (exact for const a)
500        assert!((q[0] - 1.0).abs() < 1e-10, "q={}", q[0]);
501    }
502
503    #[test]
504    fn test_rk4_with_initial_velocity() {
505        let mut q = [0.0f64];
506        let mut v = [3.0f64];
507        let a = [0.0f64]; // no force
508        let inv_mass = [1.0f64];
509        RungeKutta4.integrate(&mut q, &mut v, &a, &inv_mass, 2.0);
510        // q = 0 + 3*2 = 6, v = 3 unchanged
511        assert!((q[0] - 6.0).abs() < 1e-10, "q={}", q[0]);
512        assert!((v[0] - 3.0).abs() < 1e-10, "v={}", v[0]);
513    }
514
515    // ── PhysicsMaterial ───────────────────────────────────────────────────────
516
517    #[test]
518    fn test_material_combine_friction_geometric_mean() {
519        let a = PhysicsMaterial {
520            static_friction: 4.0,
521            dynamic_friction: 4.0,
522            restitution: 0.5,
523            rolling_friction: 0.01,
524        };
525        let b = PhysicsMaterial {
526            static_friction: 1.0,
527            dynamic_friction: 1.0,
528            restitution: 0.8,
529            rolling_friction: 0.01,
530        };
531        let c = a.combine(&b);
532        // geometric mean of 4 and 1 = 2
533        assert!((c.static_friction - 2.0).abs() < 1e-12);
534        assert!((c.dynamic_friction - 2.0).abs() < 1e-12);
535        // min restitution
536        assert!((c.restitution - 0.5).abs() < 1e-12);
537    }
538
539    #[test]
540    fn test_material_presets_valid() {
541        let rubber = PhysicsMaterial::rubber();
542        assert!(rubber.static_friction > 0.0);
543        assert!(rubber.restitution <= 1.0);
544
545        let steel = PhysicsMaterial::steel();
546        assert!(steel.dynamic_friction > 0.0);
547
548        let ice = PhysicsMaterial::ice();
549        assert!(ice.static_friction < 0.1);
550    }
551
552    // ── Damped via PhysicsMaterial-like manual impl ───────────────────────────
553
554    struct SimpleDamped {
555        lin: Real,
556        ang: Real,
557    }
558    impl Damped for SimpleDamped {
559        fn linear_damping(&self) -> Real {
560            self.lin
561        }
562        fn angular_damping(&self) -> Real {
563            self.ang
564        }
565    }
566
567    #[test]
568    fn test_damped_velocity() {
569        let d = SimpleDamped { lin: 0.5, ang: 0.2 };
570        let v_after = d.damp_velocity(10.0, 1.0, false);
571        // factor = 1 - 0.5*1 = 0.5; v = 10 * 0.5 = 5
572        assert!((v_after - 5.0).abs() < 1e-12, "v={}", v_after);
573    }
574
575    #[test]
576    fn test_damped_velocity_clamp_zero() {
577        let d = SimpleDamped { lin: 2.0, ang: 0.0 };
578        // factor = 1 - 2.0*1.0 = -1 → clamped to 0
579        let v_after = d.damp_velocity(10.0, 1.0, false);
580        assert_eq!(v_after, 0.0);
581    }
582
583    // ── Sleepable via manual impl ─────────────────────────────────────────────
584
585    #[test]
586    fn test_sleepable_should_sleep() {
587        struct DummySleepable;
588        impl Sleepable for DummySleepable {
589            fn is_sleeping(&self) -> bool {
590                false
591            }
592            fn wake(&mut self) {}
593            fn sleep(&mut self) {}
594        }
595        let s = DummySleepable;
596        assert!(s.should_sleep(0.0001, 0.01, 1.0, 0.5));
597        assert!(!s.should_sleep(0.1, 0.01, 1.0, 0.5)); // too fast
598        assert!(!s.should_sleep(0.0001, 0.01, 0.1, 0.5)); // not long enough
599    }
600
601    // ── ContactPoint ─────────────────────────────────────────────────────────
602
603    #[test]
604    fn test_contact_point_construction() {
605        let cp = ContactPoint {
606            point: [1.0, 2.0, 3.0],
607            normal: [0.0, 1.0, 0.0],
608            depth: 0.05,
609        };
610        assert!((cp.depth - 0.05).abs() < 1e-12);
611        assert_eq!(cp.normal[1], 1.0);
612    }
613
614    // ── Integrator length mismatch safety ────────────────────────────────────
615
616    #[test]
617    fn test_integrator_length_mismatch_safe() {
618        // Should not panic when arrays have different lengths.
619        let integrator = SemiImplicitEuler;
620        let mut q = vec![0.0, 0.0, 0.0];
621        let mut v = vec![1.0, 1.0];
622        let f = vec![0.0, 0.0, 0.0];
623        let inv_mass = vec![1.0, 1.0, 1.0];
624        // min length is 2; should process only 2 elements without panic.
625        integrator.integrate(&mut q, &mut v, &f, &inv_mass, 0.1);
626        assert_eq!(q[2], 0.0); // third element untouched
627    }
628
629    // ── Velocity Verlet ───────────────────────────────────────────────────────
630
631    #[test]
632    fn test_velocity_verlet_constant_accel() {
633        let mut q = [0.0f64];
634        let mut v = [0.0f64];
635        let a = [2.0f64];
636        let dt = 1.0;
637        VelocityVerlet.integrate(&mut q, &mut v, &a, &[1.0], dt);
638        // q = 0 + 0*1 + 0.5*2*1 = 1; v = 0 + 2*1 = 2
639        assert!((q[0] - 1.0).abs() < 1e-12, "q={}", q[0]);
640        assert!((v[0] - 2.0).abs() < 1e-12, "v={}", v[0]);
641    }
642
643    // ── Leapfrog ──────────────────────────────────────────────────────────────
644
645    #[test]
646    fn test_leapfrog_energy_conservation() {
647        // For a harmonic oscillator x'' = -x, energy E = v²/2 + x²/2 is nearly
648        // conserved by the leapfrog integrator over many steps.
649        let omega2 = 1.0f64; // spring constant / mass = 1
650        let dt = 0.01;
651        let n_steps = 1000;
652        let mut x = 1.0f64;
653        let mut v = 0.0f64;
654        // Kick-start half-step for leapfrog
655        let a0 = -omega2 * x;
656        let mut v_half = v + 0.5 * a0 * dt;
657        let e0 = 0.5 * v * v + 0.5 * x * x;
658        for _ in 0..n_steps {
659            x += v_half * dt;
660            let a = -omega2 * x;
661            v_half += a * dt;
662            v = v_half - 0.5 * a * dt;
663        }
664        let e_final = 0.5 * v * v + 0.5 * x * x;
665        // Leapfrog is symplectic; energy oscillates but does not drift.
666        assert!((e_final - e0).abs() < 0.01, "E0={e0}, Ef={e_final}");
667    }
668
669    // ── VelocityVerlet multi-step ─────────────────────────────────────────────
670
671    #[test]
672    fn test_velocity_verlet_free_fall() {
673        // Under constant gravity g = -9.81, starting from rest:
674        // y(t) = -1/2 g t²
675        let g = -9.81f64;
676        let dt = 0.001;
677        let n = 1000;
678        let mut y = 0.0f64;
679        let mut vy = 0.0f64;
680        let mut ay = g;
681        for _ in 0..n {
682            y += vy * dt + 0.5 * ay * dt * dt;
683            let ay_new = g;
684            vy += 0.5 * (ay + ay_new) * dt;
685            ay = ay_new;
686        }
687        let t = dt * n as f64;
688        let expected = 0.5 * g * t * t;
689        assert!((y - expected).abs() < 1e-8, "y={y}, expected={expected}");
690    }
691
692    // ── Runge-Kutta adaptive ──────────────────────────────────────────────────
693
694    #[test]
695    fn test_rkf45_step_count_positive() {
696        // RKF45 step for a simple linear ODE
697        let mut y = [1.0f64];
698        let a = [0.0f64];
699        let inv_mass = [1.0f64];
700        RungeKutta4.integrate(&mut y, &mut a.clone(), &a, &inv_mass, 0.1);
701        assert!(y[0].is_finite());
702    }
703
704    // ── PhysicsMaterial edge cases ────────────────────────────────────────────
705
706    #[test]
707    fn test_material_combine_zero_friction() {
708        let a = PhysicsMaterial {
709            static_friction: 0.0,
710            dynamic_friction: 0.0,
711            restitution: 0.5,
712            rolling_friction: 0.0,
713        };
714        let b = PhysicsMaterial::rubber();
715        let c = a.combine(&b);
716        // sqrt(0 * x) = 0
717        assert_eq!(c.static_friction, 0.0);
718        assert_eq!(c.dynamic_friction, 0.0);
719    }
720
721    #[test]
722    fn test_material_combine_min_restitution() {
723        let a = PhysicsMaterial {
724            restitution: 0.9,
725            ..PhysicsMaterial::default()
726        };
727        let b = PhysicsMaterial {
728            restitution: 0.1,
729            ..PhysicsMaterial::default()
730        };
731        let c = a.combine(&b);
732        assert!((c.restitution - 0.1).abs() < 1e-12);
733    }
734
735    // ── InverseKinematics trait (FABRIK) ──────────────────────────────────────
736
737    #[test]
738    fn test_fabrik_reaches_target_1dof() {
739        // 2-joint arm (2 bones) with total reach = 2.0; target at distance 1.0 → reachable
740        let bones = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
741        let lengths = vec![1.0f64, 1.0];
742        let target = [1.0, 0.5, 0.0]; // within reach
743        let result = fabrik_solve(&bones, &lengths, &target, 50, 1e-6);
744        // end effector should be at target
745        let ee = result.last().unwrap();
746        let d = ((ee[0] - target[0]).powi(2)
747            + (ee[1] - target[1]).powi(2)
748            + (ee[2] - target[2]).powi(2))
749        .sqrt();
750        assert!(d < 0.01, "FABRIK end effector distance to target: {d}");
751    }
752
753    #[test]
754    fn test_fabrik_unreachable_extends_toward_target() {
755        // 1-joint arm with reach 1.0; target at distance 3.0 → unreachable
756        let bones = vec![[0.0f64, 0.0, 0.0], [1.0, 0.0, 0.0]];
757        let lengths = vec![1.0f64];
758        let target = [3.0, 0.0, 0.0];
759        let result = fabrik_solve(&bones, &lengths, &target, 20, 1e-6);
760        // End effector should be pointing toward target
761        let ee = result.last().unwrap();
762        assert!(
763            ee[0] > 0.9,
764            "end effector should extend toward target: x={}",
765            ee[0]
766        );
767    }
768}
769
770// ---------------------------------------------------------------------------
771// Velocity Verlet integrator
772// ---------------------------------------------------------------------------
773
774/// Velocity Verlet integration scheme.
775///
776/// Updates positions using both current and next accelerations:
777/// ```text
778/// q_{n+1} = q_n + v_n * dt + 0.5 * a_n * dt²
779/// v_{n+1} = v_n + 0.5 * (a_n + a_{n+1}) * dt
780/// ```
781/// Since `a_{n+1}` is not generally available (forces depend on positions),
782/// this implementation uses `a_n` for both terms, equivalent to the
783/// "Störmer-Verlet" update.
784pub struct VelocityVerlet;
785
786impl Integrator for VelocityVerlet {
787    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
788        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
789        let dt2 = dt * dt;
790        for i in 0..n {
791            let a = f[i] * inv_mass[i];
792            q[i] += v[i] * dt + 0.5 * a * dt2;
793            v[i] += a * dt;
794        }
795    }
796}
797
798// ---------------------------------------------------------------------------
799// Leapfrog integrator
800// ---------------------------------------------------------------------------
801
802/// Leapfrog (Störmer-Verlet) integration.
803///
804/// The leapfrog scheme stores velocities at half-integer time steps:
805/// ```text
806/// v_{n+1/2} = v_{n-1/2} + a_n * dt
807/// q_{n+1}   = q_n + v_{n+1/2} * dt
808/// ```
809/// This implementation initialises the half-step velocity from the
810/// full-step velocity on first call, making it self-starting.
811pub struct Leapfrog;
812
813impl Integrator for Leapfrog {
814    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
815        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
816        for i in 0..n {
817            let a = f[i] * inv_mass[i];
818            // Kick: advance velocity by half-step
819            let v_half = v[i] + a * dt * 0.5;
820            // Drift: advance position
821            q[i] += v_half * dt;
822            // Kick: advance velocity to full step
823            v[i] = v_half + a * dt * 0.5;
824        }
825    }
826}
827
828// ---------------------------------------------------------------------------
829// Adams-Bashforth 2nd-order integrator
830// ---------------------------------------------------------------------------
831
832/// Adams-Bashforth 2-step explicit integrator.
833///
834/// Requires the acceleration from the previous step (`f_prev`).  When
835/// `f_prev` is `None` the method falls back to a single Euler step.
836pub struct AdamsBashforth2 {
837    /// Stored acceleration from the previous step (optional first-step bootstrap).
838    pub f_prev: Option<Vec<Real>>,
839}
840
841impl AdamsBashforth2 {
842    /// Create a new Adams-Bashforth 2-step integrator.
843    pub fn new() -> Self {
844        Self { f_prev: None }
845    }
846
847    /// Reset the stored previous-step forces.
848    pub fn reset(&mut self) {
849        self.f_prev = None;
850    }
851
852    /// Integrate state using the 2-step formula.
853    pub fn integrate_ab2(
854        &mut self,
855        q: &mut [Real],
856        v: &mut [Real],
857        f: &[Real],
858        inv_mass: &[Real],
859        dt: Real,
860    ) {
861        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
862        match &self.f_prev {
863            None => {
864                // Bootstrap with forward Euler.
865                for i in 0..n {
866                    let a = f[i] * inv_mass[i];
867                    v[i] += a * dt;
868                    q[i] += v[i] * dt;
869                }
870            }
871            Some(fp) => {
872                let m = fp.len().min(n);
873                for i in 0..m {
874                    let a_curr = f[i] * inv_mass[i];
875                    let a_prev = fp[i] * inv_mass[i];
876                    // AB2: v_{n+1} = v_n + dt*(3/2 * a_n - 1/2 * a_{n-1})
877                    let a_eff = 1.5 * a_curr - 0.5 * a_prev;
878                    v[i] += a_eff * dt;
879                    q[i] += v[i] * dt;
880                }
881            }
882        }
883        self.f_prev = Some(f[..n].to_vec());
884    }
885}
886
887impl Default for AdamsBashforth2 {
888    fn default() -> Self {
889        Self::new()
890    }
891}
892
893impl Integrator for AdamsBashforth2 {
894    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
895        // Stateless fallback: just use Euler.
896        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
897        for i in 0..n {
898            let a = f[i] * inv_mass[i];
899            v[i] += a * dt;
900            q[i] += v[i] * dt;
901        }
902    }
903}
904
905// ---------------------------------------------------------------------------
906// Implicit (backward) Euler integrator
907// ---------------------------------------------------------------------------
908
909/// Implicit (backward) Euler integrator — unconditionally stable.
910///
911/// For linear systems `F = k * x` the implicit update solves:
912/// ```text
913/// v_{n+1} = v_n + a_{n+1} * dt   (where a_{n+1} uses new position)
914/// q_{n+1} = q_n + v_{n+1} * dt
915/// ```
916/// In the absence of position-dependent forces this reduces to the
917/// semi-implicit Euler integrator.
918pub struct ImplicitEuler;
919
920impl Integrator for ImplicitEuler {
921    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
922        // For constant forces this is the same as semi-implicit Euler.
923        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
924        for i in 0..n {
925            v[i] += f[i] * inv_mass[i] * dt;
926            q[i] += v[i] * dt;
927        }
928    }
929}
930
931// ---------------------------------------------------------------------------
932// Midpoint (RK2) integrator
933// ---------------------------------------------------------------------------
934
935/// Explicit midpoint (Runge-Kutta 2) integrator.
936///
937/// ```text
938/// k1_v = a_n,  k1_q = v_n
939/// k2_v = a_n,  k2_q = v_n + k1_v * dt/2
940/// v_{n+1} = v_n + k2_v * dt
941/// q_{n+1} = q_n + k2_q * dt
942/// ```
943/// For constant forces this gives the same result as the explicit Euler
944/// update for velocity but uses the midpoint velocity for position.
945pub struct Midpoint;
946
947impl Integrator for Midpoint {
948    fn integrate(&self, q: &mut [Real], v: &mut [Real], f: &[Real], inv_mass: &[Real], dt: Real) {
949        let n = q.len().min(v.len()).min(f.len()).min(inv_mass.len());
950        let h2 = dt / 2.0;
951        for i in 0..n {
952            let a = f[i] * inv_mass[i];
953            let v_mid = v[i] + a * h2;
954            v[i] += a * dt;
955            q[i] += v_mid * dt;
956        }
957    }
958}
959
960// ---------------------------------------------------------------------------
961// Constraint solver helpers
962// ---------------------------------------------------------------------------
963
964/// XPBD position-level constraint correction for a distance constraint.
965///
966/// Corrects the positions of two bodies so that the distance between
967/// `p_a` and `p_b` equals `rest_length`, using XPBD compliance.
968///
969/// Returns the position deltas `(delta_a, delta_b)` for each body.
970///
971/// # Arguments
972/// * `p_a` / `p_b` — current world-space positions of each body's anchor
973/// * `inv_mass_a` / `inv_mass_b` — inverse masses of each body
974/// * `rest_length` — target separation distance
975/// * `compliance` — XPBD compliance (= 1 / stiffness), 0 = rigid
976/// * `dt` — substep duration
977pub fn xpbd_distance_correction(
978    p_a: [Real; 3],
979    p_b: [Real; 3],
980    inv_mass_a: Real,
981    inv_mass_b: Real,
982    rest_length: Real,
983    compliance: Real,
984    dt: Real,
985) -> ([Real; 3], [Real; 3]) {
986    let dx = [p_b[0] - p_a[0], p_b[1] - p_a[1], p_b[2] - p_a[2]];
987    let dist = (dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]).sqrt();
988    if dist < 1e-12 {
989        return ([0.0; 3], [0.0; 3]);
990    }
991    let n = [dx[0] / dist, dx[1] / dist, dx[2] / dist];
992    let c = dist - rest_length;
993    let alpha = compliance / (dt * dt);
994    let w = inv_mass_a + inv_mass_b;
995    if w < 1e-12 {
996        return ([0.0; 3], [0.0; 3]);
997    }
998    let lambda = -c / (w + alpha);
999    let da = [
1000        -lambda * inv_mass_a * n[0],
1001        -lambda * inv_mass_a * n[1],
1002        -lambda * inv_mass_a * n[2],
1003    ];
1004    let db = [
1005        lambda * inv_mass_b * n[0],
1006        lambda * inv_mass_b * n[1],
1007        lambda * inv_mass_b * n[2],
1008    ];
1009    (da, db)
1010}
1011
1012/// Velocity-level constraint impulse for a contact constraint (Baumgarte-free).
1013///
1014/// Resolves relative normal velocity at a contact point given restitution.
1015/// Returns the scalar impulse magnitude.
1016///
1017/// # Arguments
1018/// * `v_rel_n` — relative normal velocity (vB − vA) · normal
1019/// * `inv_mass_a` / `inv_mass_b` — inverse masses
1020/// * `inv_inertia_a` / `inv_inertia_b` — inverse inertia scalars
1021/// * `r_a` / `r_b` — contact offset vectors (world)
1022/// * `normal` — contact normal (unit vector)
1023/// * `restitution` — coefficient of restitution
1024#[allow(clippy::too_many_arguments)]
1025pub fn contact_impulse_scalar(
1026    v_rel_n: Real,
1027    inv_mass_a: Real,
1028    inv_mass_b: Real,
1029    inv_inertia_a: Real,
1030    inv_inertia_b: Real,
1031    r_a: [Real; 3],
1032    r_b: [Real; 3],
1033    normal: [Real; 3],
1034) -> Real {
1035    // Effective mass along normal including angular terms.
1036    fn cross_sq(r: [Real; 3], n: [Real; 3]) -> Real {
1037        let cx = r[1] * n[2] - r[2] * n[1];
1038        let cy = r[2] * n[0] - r[0] * n[2];
1039        let cz = r[0] * n[1] - r[1] * n[0];
1040        cx * cx + cy * cy + cz * cz
1041    }
1042    let k = inv_mass_a
1043        + inv_mass_b
1044        + inv_inertia_a * cross_sq(r_a, normal)
1045        + inv_inertia_b * cross_sq(r_b, normal);
1046    if k < 1e-12 { 0.0 } else { -v_rel_n / k }
1047}
1048
1049// ---------------------------------------------------------------------------
1050// FABRIK inverse kinematics
1051// ---------------------------------------------------------------------------
1052
1053/// FABRIK (Forward And Backward Reaching Inverse Kinematics) solver.
1054///
1055/// Solves an n-joint chain to place the end-effector at `target`.
1056///
1057/// # Arguments
1058/// * `bones` — initial positions of each joint (n+1 points for n segments)
1059/// * `lengths` — bone lengths (must have length == bones.len() - 1)
1060/// * `target` — desired end-effector position
1061/// * `max_iter` — maximum FABRIK iterations
1062/// * `tolerance` — convergence threshold (Euclidean distance)
1063///
1064/// Returns the updated joint positions.
1065pub fn fabrik_solve(
1066    bones: &[[Real; 3]],
1067    lengths: &[Real],
1068    target: &[Real; 3],
1069    max_iter: usize,
1070    tolerance: Real,
1071) -> Vec<[Real; 3]> {
1072    let n = bones.len();
1073    if n < 2 || lengths.len() < n - 1 {
1074        return bones.to_vec();
1075    }
1076    let mut joints: Vec<[Real; 3]> = bones.to_vec();
1077    let root = joints[0];
1078    let total_length: Real = lengths.iter().sum();
1079
1080    // Check if target is reachable
1081    let dist_to_target = dist3(&joints[0], target);
1082    if dist_to_target >= total_length {
1083        // Stretch toward target
1084        for i in 0..n - 1 {
1085            let d = dist3(&joints[i], target);
1086            let t = lengths[i] / d;
1087            joints[i + 1] = lerp3(&joints[i], target, t);
1088        }
1089        return joints;
1090    }
1091
1092    for _ in 0..max_iter {
1093        // Forward pass: start from end effector
1094        joints[n - 1] = *target;
1095        for i in (0..n - 1).rev() {
1096            let d = dist3(&joints[i + 1], &joints[i]);
1097            if d < 1e-12 {
1098                continue;
1099            }
1100            let t = lengths[i] / d;
1101            joints[i] = lerp3(&joints[i + 1], &joints[i], t);
1102        }
1103
1104        // Backward pass: start from root
1105        joints[0] = root;
1106        for i in 0..n - 1 {
1107            let d = dist3(&joints[i], &joints[i + 1]);
1108            if d < 1e-12 {
1109                continue;
1110            }
1111            let t = lengths[i] / d;
1112            joints[i + 1] = lerp3(&joints[i], &joints[i + 1], t);
1113        }
1114
1115        // Check convergence
1116        let ee_dist = dist3(&joints[n - 1], target);
1117        if ee_dist < tolerance {
1118            break;
1119        }
1120    }
1121    joints
1122}
1123
1124/// Euclidean distance between two 3D points.
1125fn dist3(a: &[Real; 3], b: &[Real; 3]) -> Real {
1126    let dx = b[0] - a[0];
1127    let dy = b[1] - a[1];
1128    let dz = b[2] - a[2];
1129    (dx * dx + dy * dy + dz * dz).sqrt()
1130}
1131
1132/// Linear interpolation between two 3D points.
1133fn lerp3(a: &[Real; 3], b: &[Real; 3], t: Real) -> [Real; 3] {
1134    [
1135        a[0] + t * (b[0] - a[0]),
1136        a[1] + t * (b[1] - a[1]),
1137        a[2] + t * (b[2] - a[2]),
1138    ]
1139}
1140
1141// ---------------------------------------------------------------------------
1142// Position-based dynamics (PBD) constraint solver
1143// ---------------------------------------------------------------------------
1144
1145/// Trait for PBD position constraints.
1146pub trait PbdConstraint {
1147    /// Evaluate constraint C(p) — returns violation (0 = satisfied).
1148    fn evaluate(&self, positions: &[[Real; 3]]) -> Real;
1149
1150    /// Compute constraint gradient ∇C with respect to each particle position.
1151    fn gradient(&self, positions: &[[Real; 3]]) -> Vec<[Real; 3]>;
1152
1153    /// Particle indices involved in this constraint.
1154    fn particle_indices(&self) -> &[usize];
1155
1156    /// Stiffness in range \[0, 1\] (1 = fully rigid, 0 = no correction).
1157    fn stiffness(&self) -> Real {
1158        1.0
1159    }
1160}
1161
1162/// A simple PBD distance constraint between two particles.
1163pub struct PbdDistanceConstraint {
1164    /// Indices of the two particles.
1165    pub indices: [usize; 2],
1166    /// Rest (target) distance.
1167    pub rest_length: Real,
1168    /// Stiffness in \[0, 1\].
1169    pub k: Real,
1170}
1171
1172impl PbdDistanceConstraint {
1173    /// Create a new distance constraint.
1174    pub fn new(i: usize, j: usize, rest_length: Real, stiffness: Real) -> Self {
1175        Self {
1176            indices: [i, j],
1177            rest_length,
1178            k: stiffness,
1179        }
1180    }
1181}
1182
1183impl PbdConstraint for PbdDistanceConstraint {
1184    fn evaluate(&self, positions: &[[Real; 3]]) -> Real {
1185        let [i, j] = self.indices;
1186        if i >= positions.len() || j >= positions.len() {
1187            return 0.0;
1188        }
1189        let p = &positions[i];
1190        let q = &positions[j];
1191        dist3(&[p[0], p[1], p[2]], &[q[0], q[1], q[2]]) - self.rest_length
1192    }
1193
1194    fn gradient(&self, positions: &[[Real; 3]]) -> Vec<[Real; 3]> {
1195        let [i, j] = self.indices;
1196        if i >= positions.len() || j >= positions.len() {
1197            return vec![[0.0; 3]; 2];
1198        }
1199        let p = positions[i];
1200        let q = positions[j];
1201        let dx = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
1202        let d = (dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]).sqrt();
1203        if d < 1e-12 {
1204            return vec![[0.0; 3]; 2];
1205        }
1206        let n = [dx[0] / d, dx[1] / d, dx[2] / d];
1207        // ∇C_i = -n, ∇C_j = +n
1208        vec![[-n[0], -n[1], -n[2]], [n[0], n[1], n[2]]]
1209    }
1210
1211    fn particle_indices(&self) -> &[usize] {
1212        &self.indices
1213    }
1214
1215    fn stiffness(&self) -> Real {
1216        self.k
1217    }
1218}
1219
1220/// Solve one PBD iteration over a set of constraints.
1221///
1222/// Updates `positions` in-place by applying each constraint correction
1223/// weighted by stiffness and inverse masses.
1224pub fn pbd_solve_iteration(
1225    positions: &mut Vec<[Real; 3]>,
1226    inv_masses: &[Real],
1227    constraints: &[&dyn PbdConstraint],
1228) {
1229    for c in constraints {
1230        let c_val = c.evaluate(positions);
1231        let grads = c.gradient(positions);
1232        let indices = c.particle_indices();
1233        if grads.len() < indices.len() {
1234            continue;
1235        }
1236        // Compute denominator: sum of w_i * |∇C_i|²
1237        let mut denom = 0.0;
1238        for (k, &idx) in indices.iter().enumerate() {
1239            if idx < inv_masses.len() {
1240                let g = grads[k];
1241                denom += inv_masses[idx] * (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
1242            }
1243        }
1244        if denom < 1e-12 {
1245            continue;
1246        }
1247        let lambda = -c_val / denom * c.stiffness();
1248        for (k, &idx) in indices.iter().enumerate() {
1249            if idx < positions.len() && idx < inv_masses.len() {
1250                let g = grads[k];
1251                let w = inv_masses[idx];
1252                positions[idx][0] += lambda * w * g[0];
1253                positions[idx][1] += lambda * w * g[1];
1254                positions[idx][2] += lambda * w * g[2];
1255            }
1256        }
1257    }
1258}
1259
1260// ---------------------------------------------------------------------------
1261// Collision filter trait
1262// ---------------------------------------------------------------------------
1263
1264/// Trait for filtering which body pairs participate in collision detection.
1265pub trait CollisionFilter {
1266    /// Return `true` if the pair `(id_a, id_b)` should be tested for collision.
1267    fn should_collide(&self, id_a: u64, id_b: u64) -> bool;
1268}
1269
1270/// A simple bitmask-based collision filter.
1271///
1272/// Body A collides with body B only if `mask_a & layer_b != 0` AND
1273/// `mask_b & layer_a != 0`.
1274pub struct LayerFilter {
1275    /// Collision layer bits for each body id.
1276    pub layers: std::collections::HashMap<u64, u32>,
1277    /// Collision mask bits for each body id.
1278    pub masks: std::collections::HashMap<u64, u32>,
1279}
1280
1281impl LayerFilter {
1282    /// Create an empty layer filter.
1283    pub fn new() -> Self {
1284        Self {
1285            layers: std::collections::HashMap::new(),
1286            masks: std::collections::HashMap::new(),
1287        }
1288    }
1289
1290    /// Register a body with its layer and mask.
1291    pub fn register(&mut self, id: u64, layer: u32, mask: u32) {
1292        self.layers.insert(id, layer);
1293        self.masks.insert(id, mask);
1294    }
1295}
1296
1297impl Default for LayerFilter {
1298    fn default() -> Self {
1299        Self::new()
1300    }
1301}
1302
1303impl CollisionFilter for LayerFilter {
1304    fn should_collide(&self, id_a: u64, id_b: u64) -> bool {
1305        let layer_a = self.layers.get(&id_a).copied().unwrap_or(0xFFFF_FFFF);
1306        let layer_b = self.layers.get(&id_b).copied().unwrap_or(0xFFFF_FFFF);
1307        let mask_a = self.masks.get(&id_a).copied().unwrap_or(0xFFFF_FFFF);
1308        let mask_b = self.masks.get(&id_b).copied().unwrap_or(0xFFFF_FFFF);
1309        (mask_a & layer_b) != 0 && (mask_b & layer_a) != 0
1310    }
1311}
1312
1313// ---------------------------------------------------------------------------
1314// Sensor trait
1315// ---------------------------------------------------------------------------
1316
1317/// Trait for physics sensors (trigger volumes, proximity detectors, etc.).
1318pub trait PhysicsSensor {
1319    /// Return `true` if the sensor is currently triggered by any body.
1320    fn is_triggered(&self) -> bool;
1321
1322    /// Return the IDs of all bodies currently overlapping this sensor.
1323    fn overlapping_bodies(&self) -> Vec<u64>;
1324
1325    /// Called each frame to update the sensor's overlap state.
1326    fn update(&mut self, body_positions: &[(u64, [Real; 3])]);
1327}
1328
1329/// A spherical proximity sensor.
1330pub struct SphereSensor {
1331    /// World-space center.
1332    pub center: [Real; 3],
1333    /// Trigger radius.
1334    pub radius: Real,
1335    /// Currently overlapping body IDs.
1336    pub current_overlaps: Vec<u64>,
1337}
1338
1339impl SphereSensor {
1340    /// Create a new sphere sensor.
1341    pub fn new(center: [Real; 3], radius: Real) -> Self {
1342        Self {
1343            center,
1344            radius,
1345            current_overlaps: Vec::new(),
1346        }
1347    }
1348}
1349
1350impl PhysicsSensor for SphereSensor {
1351    fn is_triggered(&self) -> bool {
1352        !self.current_overlaps.is_empty()
1353    }
1354
1355    fn overlapping_bodies(&self) -> Vec<u64> {
1356        self.current_overlaps.clone()
1357    }
1358
1359    fn update(&mut self, body_positions: &[(u64, [Real; 3])]) {
1360        let r2 = self.radius * self.radius;
1361        self.current_overlaps = body_positions
1362            .iter()
1363            .filter_map(|(id, pos)| {
1364                let dx = pos[0] - self.center[0];
1365                let dy = pos[1] - self.center[1];
1366                let dz = pos[2] - self.center[2];
1367                if dx * dx + dy * dy + dz * dz <= r2 {
1368                    Some(*id)
1369                } else {
1370                    None
1371                }
1372            })
1373            .collect();
1374    }
1375}
1376
1377// ---------------------------------------------------------------------------
1378// Interpolation trait for physics state
1379// ---------------------------------------------------------------------------
1380
1381/// Trait for physics state objects that support linear interpolation.
1382pub trait PhysicsInterpolatable: Sized {
1383    /// Linearly interpolate between `self` and `other` at blend factor `t ∈ [0,1]`.
1384    fn lerp_state(&self, other: &Self, t: Real) -> Self;
1385}
1386
1387// ---------------------------------------------------------------------------
1388// Extra tests for expanded traits
1389// ---------------------------------------------------------------------------
1390
1391#[cfg(test)]
1392mod expanded_tests {
1393    use super::*;
1394    use crate::AdamsBashforth2;
1395    use crate::LayerFilter;
1396    use crate::Leapfrog;
1397    use crate::Midpoint;
1398    use crate::PbdConstraint;
1399    use crate::PbdDistanceConstraint;
1400    use crate::SphereSensor;
1401    use crate::VelocityVerlet;
1402    use crate::dist3;
1403    use crate::fabrik_solve;
1404    use crate::pbd_solve_iteration;
1405    use crate::xpbd_distance_correction;
1406
1407    // ── VelocityVerlet ────────────────────────────────────────────────────────
1408
1409    #[test]
1410    fn test_velocity_verlet_no_force() {
1411        let mut q = [3.0f64];
1412        let mut v = [2.0f64];
1413        VelocityVerlet.integrate(&mut q, &mut v, &[0.0], &[1.0], 0.5);
1414        // q = 3 + 2*0.5 + 0 = 4; v = 2
1415        assert!((q[0] - 4.0).abs() < 1e-12, "q={}", q[0]);
1416        assert!((v[0] - 2.0).abs() < 1e-12, "v={}", v[0]);
1417    }
1418
1419    // ── Leapfrog ──────────────────────────────────────────────────────────────
1420
1421    #[test]
1422    fn test_leapfrog_rest() {
1423        let mut q = [1.0f64];
1424        let mut v = [0.0f64];
1425        Leapfrog.integrate(&mut q, &mut v, &[0.0], &[1.0], 1.0);
1426        assert!(
1427            (q[0] - 1.0).abs() < 1e-12,
1428            "q should not change: q={}",
1429            q[0]
1430        );
1431    }
1432
1433    // ── Midpoint ──────────────────────────────────────────────────────────────
1434
1435    #[test]
1436    fn test_midpoint_position_uses_midpoint_velocity() {
1437        let mut q = [0.0f64];
1438        let mut v = [0.0f64];
1439        // a = 4.0; v_mid = 0 + 4*0.5*0.5 = 1; q = 0 + 1*0.5*(2?) wait let dt=1
1440        // a=4, h2=0.5; v_mid = 0 + 4*0.5 = 2; q = 0 + 2*1 = 2; v = 0 + 4*1 = 4
1441        Midpoint.integrate(&mut q, &mut v, &[4.0], &[1.0], 1.0);
1442        assert!((q[0] - 2.0).abs() < 1e-12, "q={}", q[0]);
1443        assert!((v[0] - 4.0).abs() < 1e-12, "v={}", v[0]);
1444    }
1445
1446    // ── Adams-Bashforth 2 ─────────────────────────────────────────────────────
1447
1448    #[test]
1449    fn test_ab2_bootstrap_euler() {
1450        let mut ab2 = AdamsBashforth2::new();
1451        let mut q = [0.0f64];
1452        let mut v = [0.0f64];
1453        ab2.integrate_ab2(&mut q, &mut v, &[2.0], &[1.0], 1.0);
1454        // First step = Euler: v = 0 + 2*1 = 2; q = 0 + 2*1 = 2
1455        assert!((v[0] - 2.0).abs() < 1e-12);
1456        assert!((q[0] - 2.0).abs() < 1e-12);
1457    }
1458
1459    #[test]
1460    fn test_ab2_second_step() {
1461        let mut ab2 = AdamsBashforth2::new();
1462        let mut q = [0.0f64];
1463        let mut v = [0.0f64];
1464        ab2.integrate_ab2(&mut q, &mut v, &[2.0], &[1.0], 1.0);
1465        // Step 2: same force; AB2: a_eff = 1.5*2 - 0.5*2 = 2; same as Euler
1466        ab2.integrate_ab2(&mut q, &mut v, &[2.0], &[1.0], 1.0);
1467        assert!(v[0].is_finite());
1468        assert!(q[0].is_finite());
1469    }
1470
1471    // ── XPBD distance correction ──────────────────────────────────────────────
1472
1473    #[test]
1474    fn test_xpbd_distance_rigid() {
1475        let pa = [0.0, 0.0, 0.0];
1476        let pb = [2.0, 0.0, 0.0];
1477        // rest_length = 1.0 → violation = 1.0; equal masses
1478        let (da, db) = xpbd_distance_correction(pa, pb, 1.0, 1.0, 1.0, 0.0, 0.01);
1479        // Bodies should move together: da positive, db negative
1480        assert!(da[0] > 0.0, "da.x should be positive: {:?}", da);
1481        assert!(db[0] < 0.0, "db.x should be negative: {:?}", db);
1482        // Correction magnitudes equal for equal masses
1483        assert!((da[0].abs() - db[0].abs()).abs() < 1e-10);
1484    }
1485
1486    #[test]
1487    fn test_xpbd_distance_no_violation() {
1488        let pa = [0.0, 0.0, 0.0];
1489        let pb = [1.0, 0.0, 0.0];
1490        let (da, db) = xpbd_distance_correction(pa, pb, 1.0, 1.0, 1.0, 0.0, 0.01);
1491        assert!(da[0].abs() < 1e-12);
1492        assert!(db[0].abs() < 1e-12);
1493    }
1494
1495    #[test]
1496    fn test_xpbd_static_body() {
1497        // inv_mass_b = 0 → body B doesn't move
1498        let (da, db) =
1499            xpbd_distance_correction([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], 1.0, 0.0, 1.0, 0.0, 0.01);
1500        assert!(da[0].abs() > 0.0);
1501        assert_eq!(db[0], 0.0);
1502    }
1503
1504    // ── PBD distance constraint ───────────────────────────────────────────────
1505
1506    #[test]
1507    fn test_pbd_distance_constraint_evaluate() {
1508        let c = PbdDistanceConstraint::new(0, 1, 1.0, 1.0);
1509        let positions = vec![[0.0, 0.0, 0.0f64], [2.0, 0.0, 0.0]];
1510        let violation = c.evaluate(&positions);
1511        assert!((violation - 1.0).abs() < 1e-12, "violation={}", violation);
1512    }
1513
1514    #[test]
1515    fn test_pbd_solve_iteration() {
1516        let c = PbdDistanceConstraint::new(0, 1, 1.0, 1.0);
1517        let constraints: Vec<&dyn PbdConstraint> = vec![&c];
1518        let mut positions = vec![[0.0, 0.0, 0.0f64], [3.0, 0.0, 0.0]];
1519        let inv_masses = [1.0f64, 1.0];
1520        pbd_solve_iteration(&mut positions, &inv_masses, &constraints);
1521        // After correction distance should be closer to 1.0
1522        let d = (positions[1][0] - positions[0][0]).abs();
1523        assert!(d < 3.0, "distance should decrease: {d}");
1524    }
1525
1526    // ── Layer filter ──────────────────────────────────────────────────────────
1527
1528    #[test]
1529    fn test_layer_filter_collision() {
1530        let mut filter = LayerFilter::new();
1531        filter.register(1, 0b01, 0b10); // layer 1, collides with layer 2
1532        filter.register(2, 0b10, 0b01); // layer 2, collides with layer 1
1533        assert!(filter.should_collide(1, 2));
1534        assert!(filter.should_collide(2, 1));
1535    }
1536
1537    #[test]
1538    fn test_layer_filter_no_collision() {
1539        let mut filter = LayerFilter::new();
1540        filter.register(1, 0b01, 0b01); // layer 1, only collides with layer 1
1541        filter.register(2, 0b10, 0b10); // layer 2, only collides with layer 2
1542        assert!(!filter.should_collide(1, 2));
1543    }
1544
1545    // ── SphereSensor ──────────────────────────────────────────────────────────
1546
1547    #[test]
1548    fn test_sphere_sensor_triggers() {
1549        let mut sensor = SphereSensor::new([0.0; 3], 5.0);
1550        let bodies = vec![(1u64, [1.0, 0.0, 0.0]), (2u64, [10.0, 0.0, 0.0])];
1551        sensor.update(&bodies);
1552        assert!(sensor.is_triggered());
1553        assert_eq!(sensor.overlapping_bodies(), vec![1]);
1554    }
1555
1556    #[test]
1557    fn test_sphere_sensor_no_trigger() {
1558        let mut sensor = SphereSensor::new([0.0; 3], 1.0);
1559        let bodies = vec![(1u64, [5.0, 0.0, 0.0])];
1560        sensor.update(&bodies);
1561        assert!(!sensor.is_triggered());
1562    }
1563
1564    // ── FABRIK ────────────────────────────────────────────────────────────────
1565
1566    #[test]
1567    fn test_fabrik_2joint_chain() {
1568        let bones = vec![[0.0, 0.0, 0.0f64], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1569        let lengths = vec![1.0f64, 1.0];
1570        let target = [1.0, 1.0, 0.0];
1571        let result = fabrik_solve(&bones, &lengths, &target, 50, 1e-6);
1572        assert_eq!(result.len(), 3);
1573        // Root should stay fixed
1574        assert!((result[0][0]).abs() < 1e-10);
1575        // End effector near target (reachable)
1576        let ee = result[2];
1577        let d = ((ee[0] - target[0]).powi(2) + (ee[1] - target[1]).powi(2)).sqrt();
1578        assert!(d < 0.01, "end effector dist to target: {d}");
1579    }
1580
1581    #[test]
1582    fn test_fabrik_preserves_bone_lengths() {
1583        let bones = vec![[0.0, 0.0, 0.0f64], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1584        let lengths = vec![1.0f64, 1.0];
1585        let target = [0.5, 0.5, 0.0];
1586        let result = fabrik_solve(&bones, &lengths, &target, 20, 1e-6);
1587        // Check that bone lengths are approximately preserved
1588        for i in 0..2 {
1589            let d = dist3(result[i], result[i + 1]);
1590            assert!((d - lengths[i]).abs() < 1e-6, "bone {i} length: {d}");
1591        }
1592    }
1593}