use axiolid_construct::polyhedron::{boolean_polyhedra_exact, BooleanOp, Polyhedron};
use axiolid_core::{Point3, Tolerance};
use axiolid_heal::mesh::MeshHealer;
use axiolid_heal::self_intersections;
use axiolid_heal::Diagnose;
use axiolid_measure::volume_properties;
use axiolid_mesh::TriMesh;
fn tol() -> Tolerance {
Tolerance::new(1e-6, 1e-9).expect("tolerance")
}
fn box_solid(min: [f64; 3], max: [f64; 3]) -> Polyhedron {
let [x0, y0, z0] = min;
let [x1, y1, z1] = max;
let p = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
Polyhedron::new(vec![
vec![p(x0, y0, z0), p(x0, y1, z0), p(x1, y1, z0), p(x1, y0, z0)],
vec![p(x0, y0, z1), p(x1, y0, z1), p(x1, y1, z1), p(x0, y1, z1)],
vec![p(x0, y0, z0), p(x1, y0, z0), p(x1, y0, z1), p(x0, y0, z1)],
vec![p(x0, y1, z0), p(x0, y1, z1), p(x1, y1, z1), p(x1, y1, z0)],
vec![p(x0, y0, z0), p(x0, y0, z1), p(x0, y1, z1), p(x0, y1, z0)],
vec![p(x1, y0, z0), p(x1, y1, z0), p(x1, y1, z1), p(x1, y0, z1)],
])
.expect("box is a valid solid")
}
fn to_mesh(solid: &Polyhedron) -> TriMesh {
let mut positions: Vec<Point3> = Vec::new();
let mut indices = Vec::new();
let mut lookup: std::collections::HashMap<[u64; 3], u32> = std::collections::HashMap::new();
let mut index_of = |p: Point3, positions: &mut Vec<Point3>| -> u32 {
let key = [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
*lookup.entry(key).or_insert_with(|| {
positions.push(p);
(positions.len() - 1) as u32
})
};
for face in solid.faces() {
let ring: Vec<u32> = face.iter().map(|&p| index_of(p, &mut positions)).collect();
for i in 1..ring.len() - 1 {
indices.extend([ring[0], ring[i], ring[i + 1]]);
}
}
TriMesh::new(positions, indices)
}
fn volume(solid: &Polyhedron) -> f64 {
volume_properties(&to_mesh(solid), tol())
.expect("boolean result must be a closed solid")
.signed_volume
}
#[test]
fn overlapping_boxes_satisfy_the_volume_identity() {
let a = box_solid([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_solid([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
let union = boolean_polyhedra_exact(&a, &b, BooleanOp::Union).expect("union");
let intersection =
boolean_polyhedra_exact(&a, &b, BooleanOp::Intersection).expect("intersection");
let (va, vb) = (volume(&a), volume(&b));
let (vu, vi) = (volume(&union), volume(&intersection));
assert!(
(vu + vi - (va + vb)).abs() < 1e-9,
"|A u B| + |A n B| = |A| + |B| violated: {vu} + {vi} != {va} + {vb}"
);
assert!(
(vi - 0.125).abs() < 1e-9,
"intersection of the two boxes is a 0.5-cube, got {vi}"
);
}
#[test]
fn difference_removes_exactly_the_shared_volume() {
let a = box_solid([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_solid([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
let difference = boolean_polyhedra_exact(&a, &b, BooleanOp::Difference).expect("difference");
let expected = volume(&a) - 0.125;
let actual = volume(&difference);
assert!(
(actual - expected).abs() < 1e-9,
"difference volume {actual} != {expected}"
);
}
fn l_prism(z0: f64, z1: f64) -> Polyhedron {
let p = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
let ring = [
(0.0, 0.0),
(2.0, 0.0),
(2.0, 1.0),
(1.0, 1.0),
(1.0, 2.0),
(0.0, 2.0),
];
let mut faces = Vec::new();
faces.push(ring.iter().rev().map(|&(x, y)| p(x, y, z0)).collect());
faces.push(ring.iter().map(|&(x, y)| p(x, y, z1)).collect());
for i in 0..ring.len() {
let (x0, y0) = ring[i];
let (x1, y1) = ring[(i + 1) % ring.len()];
faces.push(vec![
p(x0, y0, z0),
p(x1, y1, z0),
p(x1, y1, z1),
p(x0, y0, z1),
]);
}
Polyhedron::new(faces).expect("L-prism is a valid solid")
}
fn l_prism_volume(z0: f64, z1: f64) -> f64 {
3.0 * (z1 - z0)
}
#[test]
fn non_convex_operands_are_handled_correctly() {
let l = l_prism(0.0, 1.0);
assert!(
(l_prism_volume(0.0, 1.0) - 3.0).abs() < 1e-12,
"the L footprint encloses area 3"
);
let notch = box_solid([1.2, 1.2, 0.2], [1.8, 1.8, 0.8]);
let result = boolean_polyhedra_exact(&l, ¬ch, BooleanOp::Intersection);
assert!(
result.is_err(),
"the notch interior is OUTSIDE the L, so the intersection is empty; \
a convex-only classifier would wrongly return a solid here"
);
let arm = box_solid([1.5, 0.0, 0.0], [2.5, 0.5, 1.0]);
let hit = boolean_polyhedra_exact(&l, &arm, BooleanOp::Intersection).expect("real overlap");
assert!(
(volume(&hit) - 0.25).abs() < 1e-9,
"notch-free overlap volume {}",
volume(&hit)
);
}
#[test]
fn results_are_closed_manifold_and_free_of_self_intersection() {
let a = box_solid([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_solid([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
for op in [
BooleanOp::Union,
BooleanOp::Intersection,
BooleanOp::Difference,
] {
let result = boolean_polyhedra_exact(&a, &b, op).expect("boolean");
let mesh = to_mesh(&result);
let diagnosis = MeshHealer.diagnose(&mesh, tol()).expect("diagnose");
assert!(
diagnosis.is_clean(),
"{op:?} produced defects: {:?}",
diagnosis.defects
);
assert!(
self_intersections(&mesh).is_empty(),
"{op:?} produced a self-intersecting shell"
);
}
}
#[test]
fn a_non_planar_face_is_refused_not_approximated() {
let p = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
let warped = vec![
vec![
p(0.0, 0.0, 0.0),
p(1.0, 0.0, 0.0),
p(1.0, 1.0, 0.5),
p(0.0, 1.0, 0.0),
],
vec![p(0.0, 0.0, 0.0), p(0.0, 1.0, 0.0), p(0.0, 0.0, 1.0)],
vec![p(1.0, 0.0, 0.0), p(0.0, 0.0, 1.0), p(0.0, 0.0, 0.0)],
vec![p(1.0, 1.0, 0.5), p(0.0, 0.0, 1.0), p(1.0, 0.0, 0.0)],
];
let error = Polyhedron::new(warped).expect_err("a warped face has no single plane");
let text = format!("{error}");
assert!(
text.contains("planar"),
"the refusal must name planarity as the reason, got: {text}"
);
}
#[test]
fn a_grid_aligned_cutter_is_classified_not_refused() {
let host = box_solid([0.0, 0.0, 0.0], [3.0, 3.0, 3.0]);
let cutter = box_solid([1.0, 1.0, -1.0], [2.0, 2.0, 4.0]);
let result = boolean_polyhedra_exact(&host, &cutter, BooleanOp::Difference)
.expect("grid-aligned difference must be answered, not refused");
let volume = volume(&result);
assert!(
(volume - 24.0).abs() < 1e-9,
"expected volume 24.0, got {volume}"
);
}
#[test]
fn accumulated_grid_aligned_subtraction_is_answered() {
let mut current = box_solid([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let t = 1.0 / 3.0;
let cutters = [
([t, t, -1.0], [2.0 * t, 2.0 * t, 2.0]),
([t, -1.0, t], [2.0 * t, 2.0, 2.0 * t]),
([-1.0, t, t], [2.0, 2.0 * t, 2.0 * t]),
(
[t / 3.0, t / 3.0, -1.0],
[2.0 * t / 3.0, 2.0 * t / 3.0, 2.0],
),
(
[t / 3.0, -1.0, t / 3.0],
[2.0 * t / 3.0, 2.0, 2.0 * t / 3.0],
),
(
[-1.0, t / 3.0, t / 3.0],
[2.0, 2.0 * t / 3.0, 2.0 * t / 3.0],
),
];
for (i, (mn, mx)) in cutters.iter().enumerate() {
let cutter = box_solid(*mn, *mx);
current = boolean_polyhedra_exact(¤t, &cutter, BooleanOp::Difference)
.unwrap_or_else(|e| panic!("subtraction {i} refused: {e}"));
}
assert!(
current.faces().len() > 6,
"expected the subtracted solid to carry more faces than the host box, \
got {}",
current.faces().len()
);
for face in current.faces() {
assert!(
face.len() >= 3,
"every face of the result must be a real polygon"
);
}
}