#![cfg(feature = "std")]
use alice_physics::collider::Contact;
use alice_physics::debug_render::{
debug_draw_world, DebugColor, DebugDrawData, DebugDrawFlags, DebugLine, DebugPoint,
};
use alice_physics::math::{Fix128, QuatFix, Vec3Fix};
use alice_physics::solver::{
ContactConstraint, DistanceConstraint, PhysicsWorld, RigidBody, SolverConfig,
};
use std::panic::{catch_unwind, AssertUnwindSafe};
fn scene_with_one_contact_and_one_joint() -> PhysicsWorld {
let mut world = PhysicsWorld::new(SolverConfig::default());
let anchor = world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
let bob = world.add_body(RigidBody::new_dynamic(
Vec3Fix::new(Fix128::ZERO, Fix128::from_int(2), Fix128::ZERO),
Fix128::ONE,
));
world.add_distance_constraint(DistanceConstraint::new(
anchor,
bob,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Fix128::from_int(2),
));
world.add_contact(ContactConstraint::new(
anchor,
bob,
Contact {
depth: Fix128::from_ratio(1, 10),
normal: Vec3Fix::UNIT_Y,
point_a: Vec3Fix::ZERO,
point_b: Vec3Fix::new(Fix128::ZERO, Fix128::from_ratio(1, 10), Fix128::ZERO),
},
));
world
}
fn all_flags_false() -> DebugDrawFlags {
DebugDrawFlags {
draw_aabbs: false,
draw_centers: false,
draw_velocities: false,
draw_contacts: false,
draw_contact_normals: false,
draw_joints: false,
draw_bvh: false,
draw_axes: false,
}
}
#[test]
fn debug_color_constants_pin_exact_rgba_bytes() {
let cases: [(DebugColor, (u8, u8, u8, u8)); 9] = [
(DebugColor::RED, (255, 50, 50, 255)),
(DebugColor::GREEN, (50, 255, 50, 255)),
(DebugColor::BLUE, (50, 50, 255, 255)),
(DebugColor::YELLOW, (255, 255, 50, 255)),
(DebugColor::CYAN, (50, 255, 255, 255)),
(DebugColor::MAGENTA, (255, 50, 255, 255)),
(DebugColor::WHITE, (255, 255, 255, 255)),
(DebugColor::GRAY, (128, 128, 128, 255)),
(DebugColor::ORANGE, (255, 165, 0, 255)),
];
for (color, (r, g, b, a)) in cases {
assert_eq!(
(color.r, color.g, color.b, color.a),
(r, g, b, a),
"{color:?}"
);
}
let all = [
DebugColor::RED,
DebugColor::GREEN,
DebugColor::BLUE,
DebugColor::YELLOW,
DebugColor::CYAN,
DebugColor::MAGENTA,
DebugColor::WHITE,
DebugColor::GRAY,
DebugColor::ORANGE,
];
for i in 0..all.len() {
for j in (i + 1)..all.len() {
assert_ne!(all[i], all[j], "constants {i} and {j} collide");
}
}
}
#[test]
fn point_stores_position_color_size_verbatim() {
let mut data = DebugDrawData::new();
let pos = Vec3Fix::new(
Fix128::from_ratio(7, 3),
Fix128::from_ratio(-11, 4),
Fix128::from_int(42),
);
let size = Fix128::from_ratio(9, 5);
data.point(pos, DebugColor::CYAN, size);
assert_eq!(data.points.len(), 1);
assert_eq!(data.points[0], DebugPoint::new(pos, DebugColor::CYAN, size));
assert_eq!(data.points[0].position, pos);
assert_eq!(data.points[0].color, DebugColor::CYAN);
assert_eq!(data.points[0].size, size);
}
#[test]
fn primitive_count_is_zero_before_any_add() {
assert_eq!(DebugDrawData::new().primitive_count(), 0);
assert_eq!(DebugDrawData::default().primitive_count(), 0);
}
#[test]
fn arrow_records_line_verbatim_and_head_point_size_is_fifth_of_length() {
let mut data = DebugDrawData::new();
let start = Vec3Fix::from_int(1, 2, 3);
let end = start + Vec3Fix::from_int(3, 4, 0); data.arrow(start, end, DebugColor::ORANGE);
assert_eq!(data.lines.len(), 3);
assert_eq!(
data.lines[0],
DebugLine::new(start, end, DebugColor::ORANGE)
);
let head_point = end
- Vec3Fix::new(
Fix128::from_ratio(3, 5),
Fix128::from_ratio(4, 5),
Fix128::ZERO,
);
let half = Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::from_ratio(1, 2));
let ends = [data.lines[1].end, data.lines[2].end];
for b in &data.lines[1..] {
assert_eq!(b.start, end);
assert_eq!(b.color, DebugColor::ORANGE);
}
let close = |a: Vec3Fix, b: Vec3Fix| (a - b).length().to_f64() < 1e-9;
assert!(
(close(ends[0], head_point + half) && close(ends[1], head_point - half))
|| (close(ends[0], head_point - half) && close(ends[1], head_point + half)),
"barb ends {ends:?}"
);
assert_eq!(data.points.len(), 1);
assert_eq!(data.points[0].position, end);
assert_eq!(data.points[0].color, DebugColor::ORANGE);
let expected_head_len = Fix128::from_int(5) * Fix128::from_ratio(2, 10);
assert_eq!(data.points[0].size, expected_head_len);
}
#[test]
fn zero_length_arrow_draws_the_line_but_skips_the_head_point() {
let mut data = DebugDrawData::new();
data.arrow(Vec3Fix::ZERO, Vec3Fix::ZERO, DebugColor::GRAY);
assert_eq!(data.lines.len(), 1);
assert_eq!(
data.lines[0],
DebugLine::new(Vec3Fix::ZERO, Vec3Fix::ZERO, DebugColor::GRAY)
);
assert_eq!(data.points.len(), 0);
}
#[test]
fn sphere_emits_48_lines_in_3_rings_and_no_points() {
let mut data = DebugDrawData::new();
data.sphere(Vec3Fix::ZERO, Fix128::ONE, DebugColor::MAGENTA);
assert_eq!(data.lines.len(), 48);
assert_eq!(data.points.len(), 0);
assert!(data.lines.iter().all(|l| l.color == DebugColor::MAGENTA));
}
#[test]
fn zero_radius_sphere_does_not_panic_and_still_emits_48_degenerate_lines() {
let mut data = DebugDrawData::new();
let result = catch_unwind(AssertUnwindSafe(|| {
data.sphere(Vec3Fix::ZERO, Fix128::ZERO, DebugColor::WHITE);
}));
assert!(result.is_ok(), "zero-radius sphere panicked: {result:?}");
assert_eq!(data.lines.len(), 48);
assert!(data
.lines
.iter()
.all(|l| l.start == Vec3Fix::ZERO && l.end == Vec3Fix::ZERO));
}
#[test]
fn axes_emits_three_arrows_hardcoded_rgb_in_xyz_order() {
let mut data = DebugDrawData::new();
data.axes(Vec3Fix::ZERO, QuatFix::IDENTITY, Fix128::ONE);
assert_eq!(data.lines.len(), 9);
assert_eq!(data.points.len(), 3);
assert_eq!(data.lines[0].color, DebugColor::RED);
assert_eq!(data.lines[3].color, DebugColor::GREEN);
assert_eq!(data.lines[6].color, DebugColor::BLUE);
assert_eq!(data.lines[0].end, Vec3Fix::UNIT_X);
assert_eq!(data.lines[3].end, Vec3Fix::UNIT_Y);
assert_eq!(data.lines[6].end, Vec3Fix::UNIT_Z);
}
#[test]
fn debug_draw_world_primitive_count_matches_closed_form() {
let world = scene_with_one_contact_and_one_joint();
let flags = DebugDrawFlags {
draw_aabbs: false,
draw_centers: true,
draw_velocities: false,
draw_contacts: true,
draw_contact_normals: true,
draw_joints: true,
draw_bvh: false,
draw_axes: true,
};
let mut data = DebugDrawData::new();
debug_draw_world(&world, &flags, &mut data);
assert_eq!(data.lines.len(), 22);
assert_eq!(data.points.len(), 11);
assert_eq!(data.primitive_count(), 33);
}
#[test]
fn all_draw_flags_false_yields_zero_primitives_on_a_non_empty_scene() {
let world = scene_with_one_contact_and_one_joint();
let mut data = DebugDrawData::new();
debug_draw_world(&world, &all_flags_false(), &mut data);
assert_eq!(data.primitive_count(), 0);
}
#[test]
fn draw_contact_normals_false_omits_the_normal_arrow_but_keeps_contact_points() {
let world = scene_with_one_contact_and_one_joint();
let mut flags = all_flags_false();
flags.draw_contacts = true;
flags.draw_contact_normals = false;
let mut data = DebugDrawData::new();
debug_draw_world(&world, &flags, &mut data);
assert_eq!(data.lines.len(), 0);
assert_eq!(data.points.len(), 2);
assert_eq!(data.points[0].color, DebugColor::RED);
assert_eq!(data.points[1].color, DebugColor::BLUE);
}
#[test]
fn draw_velocities_only_adds_an_arrow_for_the_non_static_body() {
let mut world = PhysicsWorld::new(SolverConfig::default());
world.add_body(RigidBody::new_static(Vec3Fix::ZERO));
let mut bob = RigidBody::new_dynamic(Vec3Fix::from_int(0, 2, 0), Fix128::ONE);
bob.velocity = Vec3Fix::from_int(3, 4, 0); world.add_body(bob);
let mut flags = all_flags_false();
flags.draw_velocities = true;
let mut data = DebugDrawData::new();
debug_draw_world(&world, &flags, &mut data);
assert_eq!(data.lines.len(), 3);
assert_eq!(data.points.len(), 1);
assert_eq!(data.lines[0].color, DebugColor::YELLOW);
assert_eq!(data.lines[0].start, Vec3Fix::from_int(0, 2, 0));
assert_eq!(
data.lines[0].end,
Vec3Fix::from_int(0, 2, 0) + Vec3Fix::from_int(3, 4, 0)
);
}
#[test]
fn debug_draw_world_on_empty_world_clears_prior_contents_to_zero() {
let world = PhysicsWorld::new(SolverConfig::default());
let flags = DebugDrawFlags::default();
let mut data = DebugDrawData::new();
data.point(Vec3Fix::ZERO, DebugColor::RED, Fix128::ONE);
assert_eq!(data.primitive_count(), 1);
debug_draw_world(&world, &flags, &mut data);
assert_eq!(data.primitive_count(), 0);
}
#[test]
fn repeated_debug_draw_world_calls_do_not_accumulate() {
let world = scene_with_one_contact_and_one_joint();
let flags = DebugDrawFlags {
draw_aabbs: false,
draw_centers: true,
draw_velocities: false,
draw_contacts: true,
draw_contact_normals: true,
draw_joints: true,
draw_bvh: false,
draw_axes: true,
};
let mut data = DebugDrawData::new();
debug_draw_world(&world, &flags, &mut data);
let first = data.primitive_count();
debug_draw_world(&world, &flags, &mut data);
assert_eq!(data.primitive_count(), first);
}
#[test]
fn extreme_coordinates_round_trip_exactly_through_point_without_panicking() {
let extreme = Fix128::from_raw(i64::MAX, u64::MAX);
let pos = Vec3Fix::new(extreme, extreme, extreme);
let mut data = DebugDrawData::new();
let result = catch_unwind(AssertUnwindSafe(|| {
data.point(pos, DebugColor::RED, extreme)
}));
assert!(
result.is_ok(),
"point() panicked on extreme input: {result:?}"
);
assert_eq!(data.points[0].position, pos);
assert_eq!(data.points[0].size, extreme);
}
#[test]
fn extreme_coordinates_do_not_panic_through_arrow_and_line_endpoints_are_the_raw_inputs() {
let extreme = Fix128::from_raw(i64::MAX, u64::MAX);
let pos = Vec3Fix::new(extreme, extreme, extreme);
let mut data = DebugDrawData::new();
let result = catch_unwind(AssertUnwindSafe(|| {
data.arrow(Vec3Fix::ZERO, pos, DebugColor::BLUE);
}));
assert!(
result.is_ok(),
"arrow() panicked on extreme input: {result:?}"
);
assert_eq!(data.lines.len(), 1);
assert_eq!(data.lines[0].start, Vec3Fix::ZERO);
assert_eq!(data.lines[0].end, pos);
}
#[test]
fn extreme_radius_and_center_do_not_panic_through_sphere() {
let extreme = Fix128::from_raw(i64::MAX, u64::MAX);
let pos = Vec3Fix::new(extreme, extreme, extreme);
let mut data = DebugDrawData::new();
let result = catch_unwind(AssertUnwindSafe(|| {
data.sphere(pos, extreme, DebugColor::GREEN);
}));
assert!(
result.is_ok(),
"sphere() panicked on extreme input: {result:?}"
);
assert_eq!(data.lines.len(), 48);
}
fn v(x: Fix128, y: Fix128, z: Fix128) -> Vec3Fix {
Vec3Fix::new(x, y, z)
}
fn box_edges(lo: Vec3Fix, hi: Vec3Fix) -> [(Vec3Fix, Vec3Fix); 12] {
let c = [
v(lo.x, lo.y, lo.z),
v(hi.x, lo.y, lo.z),
v(hi.x, hi.y, lo.z),
v(lo.x, hi.y, lo.z),
v(lo.x, lo.y, hi.z),
v(hi.x, lo.y, hi.z),
v(hi.x, hi.y, hi.z),
v(lo.x, hi.y, hi.z),
];
[
(c[0], c[1]),
(c[1], c[2]),
(c[2], c[3]),
(c[3], c[0]),
(c[4], c[5]),
(c[5], c[6]),
(c[6], c[7]),
(c[7], c[4]),
(c[0], c[4]),
(c[1], c[5]),
(c[2], c[6]),
(c[3], c[7]),
]
}
fn scene_with_radii() -> PhysicsWorld {
let mut world = PhysicsWorld::new(SolverConfig::default());
world.add_body_with_radius(
RigidBody::new_dynamic(
v(Fix128::ONE, Fix128::from_int(2), Fix128::from_int(3)),
Fix128::ONE,
),
Fix128::from_ratio(1, 2),
);
world.add_body_with_radius(RigidBody::new_static(Vec3Fix::ZERO), Fix128::from_int(2));
world.add_body(RigidBody::new_dynamic(
v(Fix128::from_int(10), Fix128::ZERO, Fix128::ZERO),
Fix128::ONE,
));
world
}
#[test]
fn draw_aabbs_emits_the_broadphase_box_of_every_body_with_a_radius() {
let world = scene_with_radii();
let flags = DebugDrawFlags {
draw_aabbs: true,
..all_flags_false()
};
let mut data = DebugDrawData::new();
debug_draw_world(&world, &flags, &mut data);
assert_eq!(data.lines.len(), 24);
assert!(data.points.is_empty());
let half = Fix128::from_ratio(1, 2);
let dynamic = box_edges(
v(half, Fix128::from_ratio(3, 2), Fix128::from_ratio(5, 2)),
v(
Fix128::from_ratio(3, 2),
Fix128::from_ratio(5, 2),
Fix128::from_ratio(7, 2),
),
);
let two = Fix128::from_int(2);
let stat = box_edges(v(-two, -two, -two), v(two, two, two));
for (k, (start, end)) in dynamic.iter().enumerate() {
assert_eq!(data.lines[k].start, *start, "dynamic edge {k} start");
assert_eq!(data.lines[k].end, *end, "dynamic edge {k} end");
assert_eq!(data.lines[k].color, DebugColor::GREEN);
}
for (k, (start, end)) in stat.iter().enumerate() {
assert_eq!(data.lines[12 + k].start, *start, "static edge {k} start");
assert_eq!(data.lines[12 + k].end, *end, "static edge {k} end");
assert_eq!(data.lines[12 + k].color, DebugColor::GRAY);
}
}
#[test]
fn draw_aabbs_off_emits_no_box() {
let world = scene_with_radii();
let mut data = DebugDrawData::new();
debug_draw_world(&world, &all_flags_false(), &mut data);
assert_eq!(data.primitive_count(), 0);
let mut data = DebugDrawData::new();
debug_draw_world(&world, &DebugDrawFlags::default(), &mut data);
assert_eq!(data.lines.len(), 24);
assert_eq!(data.points.len(), 3);
let flags = DebugDrawFlags {
draw_aabbs: false,
..DebugDrawFlags::default()
};
let mut data = DebugDrawData::new();
debug_draw_world(&world, &flags, &mut data);
assert!(data.lines.is_empty());
assert_eq!(data.points.len(), 3);
}