use super::*;
fn aabb_to_mesh(min: Point3<f64>, max: Point3<f64>) -> Mesh {
let mut mesh = Mesh::with_capacity(8, 36);
let v0 = Point3::new(min.x, min.y, min.z);
let v1 = Point3::new(max.x, min.y, min.z);
let v2 = Point3::new(max.x, max.y, min.z);
let v3 = Point3::new(min.x, max.y, min.z);
let v4 = Point3::new(min.x, min.y, max.z);
let v5 = Point3::new(max.x, min.y, max.z);
let v6 = Point3::new(max.x, max.y, max.z);
let v7 = Point3::new(min.x, max.y, max.z);
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v2, v1));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v3, v2));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v4, v5, v6));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v4, v6, v7));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v4, v7));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v7, v3));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v1, v2, v6));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v1, v6, v5));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v1, v5));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v5, v4));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v3, v7, v6));
add_triangle_to_mesh(&mut mesh, &Triangle::new(v3, v6, v2));
mesh
}
#[test]
fn subtract_mesh_many_chunks_match_sequential() {
fn vol(m: &Mesh) -> f64 {
let p = |i: u32| {
let k = i as usize * 3;
[
m.positions[k] as f64,
m.positions[k + 1] as f64,
m.positions[k + 2] as f64,
]
};
let mut v = 0.0;
for t in m.indices.chunks_exact(3) {
let (a, b, c) = (p(t[0]), p(t[1]), p(t[2]));
v += a[0] * (b[1] * c[2] - c[1] * b[2])
- a[1] * (b[0] * c[2] - c[0] * b[2])
+ a[2] * (b[0] * c[1] - c[0] * b[1]);
}
(v / 6.0).abs()
}
let csg = ClippingProcessor::new();
let wall = aabb_to_mesh(Point3::new(0., 0., 0.), Point3::new(40., 3., 0.2));
let cutters: Vec<Mesh> = (0..20)
.map(|i| {
let x = 1.0 + i as f64 * 2.0; aabb_to_mesh(Point3::new(x, 1., -0.5), Point3::new(x + 1.0, 2., 0.7))
})
.collect();
let refs: Vec<&Mesh> = cutters.iter().collect();
let batched = csg
.subtract_mesh_many(&wall, &refs)
.expect("chunked subtract must conform");
let mut seq = wall.clone();
for c in &cutters {
seq = csg.subtract_mesh(&seq, c).expect("sequential subtract");
}
let (vb, vs) = (vol(&batched), vol(&seq));
assert!(
(vb - vs).abs() < 1e-4,
"chunked volume {vb} != sequential {vs} on 20 disjoint cutters"
);
assert!(
vb < vol(&wall) - 3.0,
"expected ~20 holes removed; wall {} -> {vb}",
vol(&wall)
);
}
#[test]
fn test_plane_signed_distance() {
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
assert_eq!(plane.signed_distance(&Point3::new(0.0, 0.0, 5.0)), 5.0);
assert_eq!(plane.signed_distance(&Point3::new(0.0, 0.0, -5.0)), -5.0);
assert_eq!(plane.signed_distance(&Point3::new(5.0, 5.0, 0.0)), 0.0);
}
#[test]
fn test_clip_triangle_all_front() {
let processor = ClippingProcessor::new();
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 1.0),
Point3::new(1.0, 0.0, 1.0),
Point3::new(0.5, 1.0, 1.0),
);
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
match processor.clip_triangle(&triangle, &plane) {
ClipResult::AllFront(_) => {}
_ => panic!("Expected AllFront"),
}
}
#[test]
fn test_clip_triangle_all_behind() {
let processor = ClippingProcessor::new();
let triangle = Triangle::new(
Point3::new(0.0, 0.0, -1.0),
Point3::new(1.0, 0.0, -1.0),
Point3::new(0.5, 1.0, -1.0),
);
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
match processor.clip_triangle(&triangle, &plane) {
ClipResult::AllBehind => {}
_ => panic!("Expected AllBehind"),
}
}
#[test]
fn test_clip_triangle_split_one_front() {
let processor = ClippingProcessor::new();
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, -1.0), Point3::new(0.5, 1.0, -1.0), );
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
match processor.clip_triangle(&triangle, &plane) {
ClipResult::Split(triangles) => {
assert_eq!(triangles.len(), 1);
}
_ => panic!("Expected Split"),
}
}
#[test]
fn test_clip_triangle_split_two_front() {
let processor = ClippingProcessor::new();
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, 1.0), Point3::new(0.5, 1.0, -1.0), );
let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
match processor.clip_triangle(&triangle, &plane) {
ClipResult::Split(triangles) => {
assert_eq!(triangles.len(), 2);
}
_ => panic!("Expected Split with 2 triangles"),
}
}
#[test]
fn test_triangle_normal() {
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
);
let normal = triangle.normal();
assert!((normal.z - 1.0).abs() < 1e-6);
}
#[test]
fn test_triangle_area() {
let triangle = Triangle::new(
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
);
let area = triangle.area();
assert!((area - 0.5).abs() < 1e-6);
}
#[test]
fn degenerate_triangle_normal_is_plus_z_not_nan() {
let collapsed = Triangle::new(
Point3::new(1.0, 2.0, 3.0),
Point3::new(1.0, 2.0, 3.0),
Point3::new(4.0, 5.0, 6.0),
);
let collinear = Triangle::new(
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
);
for (label, tri) in [("collapsed", collapsed), ("collinear", collinear)] {
let n = tri.normal();
assert!(
n.x.is_finite() && n.y.is_finite() && n.z.is_finite(),
"{label} triangle normal must be finite, got {n:?}"
);
assert_eq!(
n,
Vector3::new(0.0, 0.0, 1.0),
"{label} triangle must get the +Z convention"
);
}
}
#[test]
fn clip_mesh_never_emits_non_finite_normals() {
let clipper = ClippingProcessor::new();
let mut mesh = aabb_to_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.0));
add_triangle_to_mesh(
&mut mesh,
&Triangle::new(
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.5, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
),
);
assert!(
mesh.normals.iter().all(|v| v.is_finite()),
"the sliver's own normal must already be finite at insertion"
);
let plane = Plane::new(Point3::new(0.0, 0.0, 0.5), Vector3::new(0.0, 0.0, 1.0));
let flipped = Plane::new(plane.point, -plane.normal);
for (label, p) in [("front", &plane), ("back", &flipped)] {
let out = clipper.clip_mesh(&mesh, p).expect("clip must succeed");
assert!(
out.normals.iter().all(|v| v.is_finite()),
"{label} half produced a non-finite normal"
);
}
}
#[test]
fn plane_eps_is_invariant_under_negating_the_normal() {
use super::plane_eps::PlaneEps;
let mesh = Mesh {
positions: vec![
5.0e4, 0.0, 0.0, 0.0, 3.0e3, 0.0, 0.0, 0.0, 7.0e2, ],
indices: vec![0, 1, 2],
..Default::default()
};
let eps = PlaneEps::new(&mesh, 1e-6);
let normals = [
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(1.0, 1.0, 1.0).normalize(),
Vector3::new(0.6, 0.0, 0.8),
Vector3::new(1.0, 2.0, 3.0).normalize(),
];
for n in normals {
let pos = eps.for_normal(&n);
let neg = eps.for_normal(&(-n));
assert_eq!(
pos, neg,
"eps({n:?}) = {pos:e} but eps({:?}) = {neg:e}: the classification \
tolerance must depend on the plane's ORIENTATION, not on which \
way its normal happens to point. `router/layers.rs` clips one \
remainder with both `+n` and `-n` and welds the halves, so a \
direction-dependent epsilon opens a gap or an overlap at every \
material interface",
-n
);
assert!(
pos > 1e-6,
"fixture is vacuous: at this magnitude eps({n:?}) = {pos:e} must \
be set by the projected term, not by the 1e-6 floor, or the \
equality above is trivially true"
);
}
}
#[test]
fn difference_result_wrong_piece_check_is_per_axis_not_longest_dimension() {
let host = aabb_to_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(5.0, 0.4, 7.0));
let result = aabb_to_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(5.0, 0.41, 7.0));
assert!(
ClippingProcessor::difference_result_looks_degenerate(&host, &result),
"a result overshooting the host's thin Y face by 1 cm (4 mm per-axis \
slack on that axis) must be flagged as a wrong-piece degenerate result"
);
}
#[path = "world_frame_tests.rs"]
mod world_frame_tests;