use axiolid_core::{Point3, Tolerance};
use axiolid_mesh::TriMesh;
use axiolid_reference::clash::{interference, point_inside, Interference};
fn box_mesh(min: Point3, max: Point3) -> TriMesh {
let p = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
let positions = vec![
p(min.x, min.y, min.z),
p(max.x, min.y, min.z),
p(max.x, max.y, min.z),
p(min.x, max.y, min.z),
p(min.x, min.y, max.z),
p(max.x, min.y, max.z),
p(max.x, max.y, max.z),
p(min.x, max.y, max.z),
];
let indices = vec![
0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7, 6, 3, 0, 4, 3, 4, 7, ];
TriMesh::new(positions, indices)
}
fn unit_box_at(x: f64) -> TriMesh {
box_mesh(Point3::new(x, 0.0, 0.0), Point3::new(x + 1.0, 1.0, 1.0))
}
#[test]
fn separated_solids_are_clear() {
let a = unit_box_at(0.0);
let b = unit_box_at(5.0);
let report = interference(&a, &b, Tolerance::MILLIMETRE).expect("interference");
assert_eq!(report.kind, Interference::Clear);
assert!(report.is_clear());
assert!(report.penetrating_pairs.is_empty());
assert!(report.touching_pairs.is_empty());
}
#[test]
fn overlapping_solids_penetrate() {
let a = unit_box_at(0.0);
let b = unit_box_at(0.5);
let report = interference(&a, &b, Tolerance::MILLIMETRE).expect("interference");
assert_eq!(report.kind, Interference::Penetrating);
assert!(report.is_penetrating());
assert!(
report.containment,
"a face-to-face overlap is decided by containment"
);
}
#[test]
fn face_to_face_solids_touch_but_do_not_penetrate() {
let a = unit_box_at(0.0);
let b = unit_box_at(1.0);
let report = interference(&a, &b, Tolerance::MILLIMETRE).expect("interference");
assert_eq!(
report.kind,
Interference::Touching,
"shared face must be contact, not penetration"
);
assert!(report.penetrating_pairs.is_empty());
assert!(!report.touching_pairs.is_empty());
}
#[test]
fn a_hair_of_separation_is_still_clear() {
let a = unit_box_at(0.0);
let b = unit_box_at(1.0 + 1e-12);
let report = interference(&a, &b, Tolerance::ZERO).expect("interference");
assert_eq!(
report.kind,
Interference::Clear,
"1e-12 gap must not read as contact under an exact predicate"
);
}
#[test]
fn a_hair_of_overlap_is_still_penetration() {
let a = unit_box_at(0.0);
let b = unit_box_at(1.0 - 1e-9);
let report = interference(&a, &b, Tolerance::ZERO).expect("interference");
assert_eq!(
report.kind,
Interference::Penetrating,
"1e-9 overlap must not be rounded away"
);
}
#[test]
fn the_broad_phase_rejects_the_quadratic_majority() {
let a = unit_box_at(0.0);
let b = unit_box_at(5.0);
let report = interference(&a, &b, Tolerance::MILLIMETRE).expect("interference");
assert_eq!(
report.narrow_phase_tests + report.broad_phase_rejections,
144,
"every pair must be either tested or rejected"
);
assert_eq!(
report.narrow_phase_tests, 0,
"well-separated solids need no exact test at all"
);
}
#[test]
fn tolerance_widens_the_broad_phase_without_changing_the_verdict() {
let a = box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.0));
let b = box_mesh(Point3::new(1.05, 2.0, 2.0), Point3::new(2.05, 3.0, 3.0));
let tight = interference(&a, &b, Tolerance::ZERO).expect("interference");
let loose =
interference(&a, &b, Tolerance::new(3.0, 1e-9).expect("tolerance")).expect("interference");
assert!(
loose.narrow_phase_tests > tight.narrow_phase_tests,
"a wider tolerance must test more pairs: {} vs {}",
loose.narrow_phase_tests,
tight.narrow_phase_tests
);
assert_eq!(tight.kind, Interference::Clear);
assert_eq!(
loose.kind,
Interference::Clear,
"tolerance widens the search, it must not change the verdict"
);
}
#[test]
fn a_degenerate_triangle_is_counted_not_silently_ignored() {
let a = unit_box_at(0.0);
let mut b = unit_box_at(0.5);
let first = b.indices[0] as usize;
b.indices[1] = first as u32;
let report = interference(&a, &b, Tolerance::MILLIMETRE).expect("interference");
assert!(
report.degenerate_skips > 0,
"a collapsed triangle must be reported as unclassifiable"
);
}
#[test]
fn a_non_finite_tolerance_is_refused() {
let a = unit_box_at(0.0);
let b = unit_box_at(0.5);
let bad = Tolerance::new(f64::NAN, 1e-9);
if let Ok(t) = bad {
assert!(interference(&a, &b, t).is_err());
}
}
#[test]
fn an_empty_mesh_clashes_with_nothing() {
let a = unit_box_at(0.0);
let empty = TriMesh::new(Vec::new(), Vec::new());
let report = interference(&a, &empty, Tolerance::MILLIMETRE).expect("interference");
assert_eq!(report.kind, Interference::Clear);
assert_eq!(report.narrow_phase_tests, 0);
}
#[test]
fn a_solid_fully_inside_another_is_penetrating() {
let outer = box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(10.0, 10.0, 10.0));
let inner = box_mesh(Point3::new(4.0, 4.0, 4.0), Point3::new(6.0, 6.0, 6.0));
let report = interference(&outer, &inner, Tolerance::MILLIMETRE).expect("interference");
assert_eq!(
report.kind,
Interference::Penetrating,
"a contained solid must not read as clear"
);
assert!(report.containment, "verdict came from containment");
assert!(
report.penetrating_pairs.is_empty(),
"no surfaces cross in this arrangement"
);
}
#[test]
fn a_transversal_crossing_is_penetration() {
let block = box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 2.0, 2.0));
let blade = box_mesh(Point3::new(0.73, -1.0, 0.61), Point3::new(1.31, 3.0, 1.17));
let report = interference(&block, &blade, Tolerance::ZERO).expect("interference");
assert_eq!(
report.kind,
Interference::Penetrating,
"a blade through a block penetrates"
);
assert!(
!report.penetrating_pairs.is_empty(),
"a transversal crossing must be named by the surface test, not \
inferred only from containment"
);
assert!(
!report.containment,
"the blade's vertices are all outside the block: {report:?}"
);
}
#[test]
fn contact_is_reported_as_touching_not_clear() {
let a = unit_box_at(0.0);
let b = unit_box_at(1.0);
let report = interference(&a, &b, Tolerance::ZERO).expect("interference");
assert_eq!(report.kind, Interference::Touching);
assert!(
!report.is_clear(),
"face contact must not read as no interference"
);
assert!(
!report.touching_pairs.is_empty(),
"contact must name its pairs"
);
}
#[test]
fn coplanar_faces_are_decided_by_shared_area() {
let lower = box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.0));
let upper = box_mesh(Point3::new(0.5, 0.0, 1.0), Point3::new(1.5, 1.0, 2.0));
let overlapping = interference(&lower, &upper, Tolerance::ZERO).expect("interference");
assert_eq!(
overlapping.kind,
Interference::Touching,
"coplanar faces that share area are contact"
);
let apart = box_mesh(Point3::new(5.0, 0.0, 1.0), Point3::new(6.0, 1.0, 2.0));
let separated = interference(&lower, &apart, Tolerance::ZERO).expect("interference");
assert_eq!(
separated.kind,
Interference::Clear,
"coplanar faces that share no area are not contact"
);
}
#[test]
fn the_interior_threshold_rejects_a_surface_point() {
let cube = box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 2.0, 2.0));
let t = Tolerance::ZERO;
let deep = Point3::new(1.0, 1.0, 1.0);
let face = Point3::new(1.0, 1.0, 2.0);
let outside = Point3::new(1.0, 1.0, 3.0);
assert_eq!(point_inside(deep, &cube, t), Some(true), "centre is inside");
assert_eq!(
point_inside(face, &cube, t),
Some(false),
"a point ON the face must not count as inside"
);
assert_eq!(
point_inside(outside, &cube, t),
Some(false),
"a point beyond the face is outside"
);
}
#[test]
fn a_partial_face_overlap_is_contact_not_clearance() {
let lower = box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 2.0, 1.0));
let upper = box_mesh(Point3::new(1.0, 1.0, 1.0), Point3::new(3.0, 3.0, 2.0));
let report = interference(&lower, &upper, Tolerance::ZERO).expect("interference");
assert_eq!(
report.kind,
Interference::Touching,
"overlapping coplanar faces are contact"
);
assert!(
!report.containment,
"neither solid is inside the other: {report:?}"
);
assert!(
report.penetrating_pairs.is_empty(),
"no volume is shared, so nothing penetrates"
);
assert_eq!(
report.touching_pairs.len(),
18,
"coplanar overlap must contribute pairs beyond edge contact"
);
}
#[test]
fn coplanar_faces_that_share_no_area_stay_clear() {
let left = box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.0));
let right = box_mesh(Point3::new(3.0, 0.5, 0.25), Point3::new(4.0, 1.5, 1.25));
let t = Tolerance::new(3.0, 1e-9).expect("tolerance");
let report = interference(&left, &right, t).expect("interference");
assert!(
report.narrow_phase_tests > 0,
"the pairs must actually reach the narrow phase"
);
assert_eq!(
report.kind,
Interference::Clear,
"coplanar faces 2m apart share no area and are not contact"
);
assert!(report.touching_pairs.is_empty(), "nothing touches");
}
#[test]
fn the_broad_phase_prunes_most_triangle_pairs() {
let a = unit_box_at(0.0);
let b = unit_box_at(1.0);
let report = interference(&a, &b, Tolerance::ZERO).expect("interference");
let total = 12 * 12;
assert!(
report.narrow_phase_tests <= 84,
"broad phase tested {} of {total} pairs: it is not pruning",
report.narrow_phase_tests
);
}
#[test]
fn the_bounds_precheck_preserves_nested_containment() {
let outer = box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(4.0, 4.0, 4.0));
let inner = box_mesh(Point3::new(1.0, 1.0, 1.0), Point3::new(2.0, 2.0, 2.0));
let report = interference(&outer, &inner, Tolerance::ZERO).expect("interference");
assert!(report.containment, "nested solid must still be detected");
}