Full engine port in libs/box3d: math, geometry, GJK/TOI, hull builder, dynamic tree, manifolds, constraint graph, solver (serial, scalar SIMD path), all 8 joint types, sensors, mover, world API. 147 ported C unit tests green in debug and release. See libs/box3d/README.md for the upstream revision and sync notes, PORTING.md for conventions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
207 lines
7.8 KiB
Rust
207 lines
7.8 KiB
Rust
// Port of box3d/test/test_mover.c
|
|
|
|
use makepad_box3d::capsule::collide_mover_and_capsule;
|
|
use makepad_box3d::hull::{collide_mover_and_hull, make_box_hull};
|
|
use makepad_box3d::math_functions::*;
|
|
use makepad_box3d::mover::solve_planes;
|
|
use makepad_box3d::sphere::collide_mover_and_sphere;
|
|
use makepad_box3d::types::*;
|
|
use makepad_box3d::{ensure, ensure_small};
|
|
|
|
#[test]
|
|
fn parallel_planes() {
|
|
let mut planes = [CollisionPlane::default(); 3];
|
|
planes[0].plane.normal = vec3(0.0, 0.0, 1.0);
|
|
planes[0].plane.offset = 0.5;
|
|
planes[0].push_limit = f32::MAX;
|
|
planes[1].plane.normal = vec3(0.0, 0.0, 1.0);
|
|
planes[1].plane.offset = 1.0;
|
|
planes[1].push_limit = f32::MAX;
|
|
|
|
let target = vec3(0.0, 0.0, 0.0);
|
|
let result = solve_planes(target, &mut planes[..2]);
|
|
|
|
ensure!(result.iteration_count == 2);
|
|
ensure_small!(result.delta.z - 1.0, 0.0055);
|
|
}
|
|
|
|
#[test]
|
|
fn game_planes() {
|
|
// This scenario takes many iterations because the target is deep into the plane.
|
|
let mut planes = [CollisionPlane::default(); 3];
|
|
planes[0].plane.normal = vec3(0.0, -0.23941046, 0.970918416);
|
|
planes[0].plane.offset = 0.390724182;
|
|
planes[0].push_limit = f32::MAX;
|
|
planes[1].plane.normal = vec3(0.0, 0.0, 1.0);
|
|
planes[1].plane.offset = 1.49998093;
|
|
planes[1].push_limit = f32::MAX;
|
|
|
|
let mut target = vec3(-2.5390625, 0.0, -73.6880798);
|
|
|
|
planes[0].plane.offset -= dot(planes[0].plane.normal, target);
|
|
planes[1].plane.offset -= dot(planes[1].plane.normal, target);
|
|
target = Vec3::ZERO;
|
|
|
|
let result = solve_planes(target, &mut planes[..2]);
|
|
|
|
ensure!(result.iteration_count == 20);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mover-collide overlap handling
|
|
//
|
|
// collide_mover_and_sphere / capsule / hull must never emit a plane with a
|
|
// degenerate (zero) normal, even when the mover deeply penetrates the shape.
|
|
// On deep overlap the GJK path returns a {0,0,0} normal; these tests guard the
|
|
// fix that replaces it with an analytic (sphere/capsule) or dropped (hull) result.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn mover_sphere_separated() {
|
|
let shape = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
|
|
let mover = Capsule { center1: vec3(4.0, 3.0, 0.0), center2: vec3(6.0, 3.0, 0.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_sphere(&mut result, &shape, &mover);
|
|
ensure!(count == 0);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_sphere_touching() {
|
|
let shape = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
|
|
|
|
// Mover core segment runs along X at y = 0.6, leaving it 0.1 inside the
|
|
// 0.7 combined radius.
|
|
let mover = Capsule { center1: vec3(-1.0, 0.6, 0.0), center2: vec3(1.0, 0.6, 0.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_sphere(&mut result, &shape, &mover);
|
|
ensure!(count == 1);
|
|
ensure!(is_normalized(result.plane.normal));
|
|
|
|
// Push-out points from the sphere straight up toward the mover.
|
|
ensure!(result.plane.normal.y > 0.99);
|
|
ensure_small!(result.plane.offset - 0.1, 1e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_sphere_deep_overlap() {
|
|
let shape = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
|
|
|
|
// Mover axis runs straight through the sphere center: the bug case where
|
|
// GJK reports a zero normal.
|
|
let mover = Capsule { center1: vec3(-1.0, 0.0, 0.0), center2: vec3(1.0, 0.0, 0.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_sphere(&mut result, &shape, &mover);
|
|
ensure!(count == 1);
|
|
|
|
// The normal must still be a valid unit vector.
|
|
ensure!(is_normalized(result.plane.normal));
|
|
|
|
// The fallback axis is perpendicular to the mover axis (X).
|
|
ensure_small!(result.plane.normal.x, 1e-5);
|
|
|
|
// Deepest possible penetration: the full combined radius.
|
|
ensure_small!(result.plane.offset - 0.7, 1e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_capsule_separated() {
|
|
let shape = Capsule { center1: vec3(-1.0, 0.0, 0.0), center2: vec3(1.0, 0.0, 0.0), radius: 0.3 };
|
|
let mover = Capsule { center1: vec3(-1.0, 5.0, 0.0), center2: vec3(1.0, 5.0, 0.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_capsule(&mut result, &shape, &mover);
|
|
ensure!(count == 0);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_capsule_touching() {
|
|
let shape = Capsule { center1: vec3(-1.0, 0.0, 0.0), center2: vec3(1.0, 0.0, 0.0), radius: 0.3 };
|
|
|
|
// Parallel mover 0.4 above, leaving it 0.1 inside the 0.5 combined radius.
|
|
let mover = Capsule { center1: vec3(-1.0, 0.4, 0.0), center2: vec3(1.0, 0.4, 0.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_capsule(&mut result, &shape, &mover);
|
|
ensure!(count == 1);
|
|
ensure!(is_normalized(result.plane.normal));
|
|
ensure!(result.plane.normal.y > 0.99);
|
|
ensure_small!(result.plane.offset - 0.1, 1e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_capsule_deep_overlap() {
|
|
// Shape capsule along X, mover capsule along Z; their core segments cross
|
|
// exactly at the origin, so GJK reports a zero normal.
|
|
let shape = Capsule { center1: vec3(-1.0, 0.0, 0.0), center2: vec3(1.0, 0.0, 0.0), radius: 0.3 };
|
|
let mover = Capsule { center1: vec3(0.0, 0.0, -1.0), center2: vec3(0.0, 0.0, 1.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_capsule(&mut result, &shape, &mover);
|
|
ensure!(count == 1);
|
|
ensure!(is_normalized(result.plane.normal));
|
|
|
|
// The separating axis of two crossing segments is perpendicular to both.
|
|
ensure_small!(result.plane.normal.x, 1e-5);
|
|
ensure_small!(result.plane.normal.z, 1e-5);
|
|
ensure_small!(result.plane.offset - 0.5, 1e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_capsule_parallel_overlap() {
|
|
// Mover core segment coincides with the shape core segment: the cross-product
|
|
// axis degenerates, so a perpendicular of the mover axis is used instead.
|
|
let shape = Capsule { center1: vec3(-1.0, 0.0, 0.0), center2: vec3(1.0, 0.0, 0.0), radius: 0.3 };
|
|
let mover = Capsule { center1: vec3(-1.0, 0.0, 0.0), center2: vec3(1.0, 0.0, 0.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_capsule(&mut result, &shape, &mover);
|
|
ensure!(count == 1);
|
|
ensure!(is_normalized(result.plane.normal));
|
|
|
|
// The fallback axis is perpendicular to the mover axis (X).
|
|
ensure_small!(result.plane.normal.x, 1e-5);
|
|
ensure_small!(result.plane.offset - 0.5, 1e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_hull_separated() {
|
|
let box_hull = make_box_hull(0.5, 0.5, 0.5);
|
|
let mover = Capsule { center1: vec3(-0.3, 5.0, 0.0), center2: vec3(0.3, 5.0, 0.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_hull(&mut result, &box_hull, &mover);
|
|
ensure!(count == 0);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_hull_touching() {
|
|
let box_hull = make_box_hull(0.5, 0.5, 0.5);
|
|
|
|
// Mover core segment above the +Y face; the 0.2 radius reaches 0.1 into it.
|
|
let mover = Capsule { center1: vec3(-0.3, 0.6, 0.0), center2: vec3(0.3, 0.6, 0.0), radius: 0.2 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_hull(&mut result, &box_hull, &mover);
|
|
ensure!(count == 1);
|
|
ensure!(is_normalized(result.plane.normal));
|
|
ensure!(result.plane.normal.y > 0.99);
|
|
ensure_small!(result.plane.offset - 0.1, 1e-4);
|
|
}
|
|
|
|
#[test]
|
|
fn mover_hull_deep_overlap() {
|
|
let box_hull = make_box_hull(0.5, 0.5, 0.5);
|
|
|
|
// Mover core segment lies entirely inside the box, so GJK reports overlap.
|
|
let mover = Capsule { center1: vec3(-0.2, 0.0, 0.0), center2: vec3(0.2, 0.0, 0.0), radius: 0.1 };
|
|
|
|
let mut result = PlaneResult::default();
|
|
let count = collide_mover_and_hull(&mut result, &box_hull, &mover);
|
|
|
|
// The overlap guard drops the plane rather than emit a zero normal.
|
|
// todo replace with SAT once collide_mover_and_hull resolves overlaps.
|
|
ensure!(count == 0);
|
|
}
|