#![allow(missing_docs)]
#[path = "regression_harness.rs"]
mod harness;
use oxiphysics::rigid::world::PhysicsWorld;
use oxiphysics::rigid::{BodyType, RigidBody, RigidBodySet, integrate_bodies};
use oxiphysics_core::math::{Mat3, Vec3};
#[test]
#[ignore = "blocked by PhysicsWorld::solve_contact_velocity bugs (sign-inverted early return + hardcoded e=0.3); re-enable once normal convention and per-collider restitution are wired through"]
fn test_restitution_ladder() {
let mut world = PhysicsWorld::new([0.0, -9.81, 0.0], 1.0 / 240.0);
let mut ball = RigidBody::new(1.0);
ball.transform.position = Vec3::new(0.0, 1.0, 0.0);
ball.linear_damping = 0.0;
ball.angular_damping = 0.0;
let ball_h = world.add_rigid_body(ball);
let mut ground = RigidBody::new_static();
ground.transform.position = Vec3::new(0.0, -1.0, 0.0);
world.add_rigid_body(ground);
let mut prev_y = 1.0;
let mut going_up = false;
let mut peaks: Vec<f64> = Vec::new();
for _ in 0..960 {
world.step();
let y = world
.get_body(ball_h)
.expect("ball handle inserted above must exist")
.transform
.position
.y;
if y < prev_y && going_up {
peaks.push(prev_y);
going_up = false;
} else if y > prev_y {
going_up = true;
}
prev_y = y;
}
assert!(
peaks.len() >= 3,
"expected at least 3 peaks (release + two bounces), got {}: {peaks:?}",
peaks.len()
);
let h2 = peaks[2];
let baseline = harness::load_baseline("rigid_bounce_height_2")
.expect("baseline `rigid_bounce_height_2` must exist");
eprintln!(
"[test_restitution_ladder] second-bounce peak = {h2:.6} m (analytical = 0.25 m, baseline tol_rel = {:.1}%)",
baseline.tolerance_rel * 100.0
);
assert_close!(h2, baseline);
}
#[test]
fn test_angular_momentum_conservation() {
let mut set = RigidBodySet::new();
let mut body = RigidBody::new(1.0);
body.linear_damping = 0.0;
body.angular_damping = 0.0;
body.local_inertia = Mat3::from_diagonal(&Vec3::new(
0.020_833_333_333_333_332,
0.010_833_333_333_333_334,
0.016_666_666_666_666_666,
));
body.update_world_inertia();
body.angular_velocity = Vec3::new(1.0, 0.1, 0.0);
let handle = set.insert(body);
let world_angular_momentum = |set: &RigidBodySet| -> Vec3 {
let b = set.get(handle).expect("handle inserted above must exist");
let rot = b.transform.rotation.to_rotation_matrix();
let l_local = b.local_inertia * b.angular_velocity;
rot.matrix() * l_local
};
let initial_l = world_angular_momentum(&set);
let initial_mag = initial_l.norm();
assert!(
initial_mag > 0.0,
"initial angular momentum must be non-zero"
);
let gravity = Vec3::zeros();
let dt = 0.01_f64;
let steps = 1000usize;
let mut max_drift: f64 = 0.0;
for _ in 0..steps {
integrate_bodies(&mut set, dt, &gravity);
let mag = world_angular_momentum(&set).norm();
let drift = (mag - initial_mag).abs() / initial_mag;
if drift > max_drift {
max_drift = drift;
}
}
let final_mag = world_angular_momentum(&set).norm();
let final_drift = (final_mag - initial_mag).abs() / initial_mag;
eprintln!(
"[test_angular_momentum_conservation] steps = {steps}, dt = {dt} s, \
|L_initial| = {initial_mag:.9}, |L_final| = {final_mag:.9}, \
final_drift = {final_drift:.3e}, max_drift = {max_drift:.3e}",
);
assert!(
max_drift < 0.01,
"angular momentum drift exceeds 1% tolerance: \
max_drift = {max_drift:.3e} ({:.3}%), final_drift = {final_drift:.3e} ({:.3}%), \
|L_initial| = {initial_mag:.9}, |L_final| = {final_mag:.9}",
max_drift * 100.0,
final_drift * 100.0,
);
}
#[test]
#[ignore = "blocked by missing position correction and PhysicsWorld::solve_contact_velocity sign bug; re-enable once Baumgarte/split-impulse is wired into PhysicsWorld::step"]
fn test_stacked_box_equilibrium() {
let mut world = PhysicsWorld::new([0.0, -9.81, 0.0], 1.0 / 100.0);
let mut handles = Vec::with_capacity(3);
let expected_rest_y = [0.5_f64, 1.5, 2.5];
for &y in &expected_rest_y {
let mut b = RigidBody::new(1.0);
b.transform.position = Vec3::new(0.0, y + 0.5, 0.0); b.linear_damping = 0.0;
b.angular_damping = 0.0;
handles.push(world.add_rigid_body(b));
}
let mut ground = RigidBody::new_static();
ground.transform.position = Vec3::new(0.0, -0.5, 0.0);
world.add_rigid_body(ground);
for _ in 0..300 {
world.step();
}
for (i, &h) in handles.iter().enumerate() {
let b = world
.get_body(h)
.expect("stacked body handle inserted above must exist");
assert_eq!(
b.body_type,
BodyType::Dynamic,
"stacked body {i} was demoted to non-dynamic type"
);
let pos = b.transform.position;
let vel = b.velocity;
let ke = 0.5 * b.mass * vel.norm_squared();
let penetration = (expected_rest_y[i] - pos.y).max(0.0);
let lateral = (pos.x * pos.x + pos.z * pos.z).sqrt();
eprintln!(
"[test_stacked_box_equilibrium] body {i}: pos = ({:.6}, {:.6}, {:.6}), \
vel = ({:.6}, {:.6}, {:.6}), penetration = {penetration:.3e}, \
lateral = {lateral:.3e}, KE = {ke:.3e}",
pos.x, pos.y, pos.z, vel.x, vel.y, vel.z,
);
assert!(
penetration < 2e-3,
"body {i} penetration {penetration:.3e} m exceeds 2e-3 m tolerance \
(expected y ~ {:.3}, got y = {:.6})",
expected_rest_y[i],
pos.y,
);
assert!(
lateral < 5e-3,
"body {i} lateral drift {lateral:.3e} m exceeds 5e-3 m tolerance \
(pos = ({:.6}, {:.6}, {:.6}))",
pos.x,
pos.y,
pos.z,
);
assert!(
ke < 1e-3,
"body {i} kinetic energy {ke:.3e} J exceeds 1e-3 J tolerance \
(vel = ({:.6}, {:.6}, {:.6}))",
vel.x,
vel.y,
vel.z,
);
}
}