use axiolid_contracts::ExecutionOptions;
use axiolid_core::{BooleanOperator, Point3, Tolerance};
use axiolid_mesh::TriMesh;
use axiolid_mesh_boolean_contract::MeshBoolean;
use axiolid_reference::ScalarBoolean;
fn options() -> ExecutionOptions {
ExecutionOptions::new(Tolerance::METRE)
}
fn box_at(min: [f64; 3], max: [f64; 3]) -> TriMesh {
let [x0, y0, z0] = min;
let [x1, y1, z1] = max;
let positions = vec![
Point3::new(x0, y0, z0),
Point3::new(x1, y0, z0),
Point3::new(x1, y1, z0),
Point3::new(x0, y1, z0),
Point3::new(x0, y0, z1),
Point3::new(x1, y0, z1),
Point3::new(x1, y1, z1),
Point3::new(x0, y1, z1),
];
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 volume(mesh: &TriMesh) -> f64 {
mesh.indices
.chunks_exact(3)
.map(|t| {
let a = mesh.positions[t[0] as usize];
let b = mesh.positions[t[1] as usize];
let c = mesh.positions[t[2] as usize];
a.dot(b.cross(c))
})
.sum::<f64>()
/ 6.0
}
fn apply(subject: &TriMesh, tool: &TriMesh, op: BooleanOperator) -> TriMesh {
ScalarBoolean::new()
.boolean(subject, tool, op, &options())
.unwrap_or_else(|error| panic!("{op:?}: {error}"))
.mesh
}
#[test]
fn the_fixture_has_the_analytic_volume() {
assert!((volume(&box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0])) - 1.0).abs() < 1e-12);
assert!((volume(&box_at([0.0, 0.0, 0.0], [2.0, 3.0, 4.0])) - 24.0).abs() < 1e-12);
}
#[test]
fn disjoint_operands_follow_the_set_algebra() {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_at([5.0, 5.0, 5.0], [6.0, 6.0, 6.0]);
assert!((volume(&apply(&a, &b, BooleanOperator::Union)) - 2.0).abs() < 1e-12);
assert_eq!(
apply(&a, &b, BooleanOperator::Intersection).triangle_count(),
0
);
assert!((volume(&apply(&a, &b, BooleanOperator::Difference)) - 1.0).abs() < 1e-12);
assert!((volume(&apply(&a, &b, BooleanOperator::SymmetricDifference)) - 2.0).abs() < 1e-12);
}
#[test]
fn a_nested_operand_is_absorbed_or_subtracted_exactly() {
let outer = box_at([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]); let inner = box_at([1.0, 1.0, 1.0], [2.0, 2.0, 2.0]);
assert!((volume(&apply(&outer, &inner, BooleanOperator::Union)) - 64.0).abs() < 1e-12);
assert!((volume(&apply(&outer, &inner, BooleanOperator::Intersection)) - 1.0).abs() < 1e-12);
assert!((volume(&apply(&outer, &inner, BooleanOperator::Difference)) - 63.0).abs() < 1e-12);
assert!(
(volume(&apply(&outer, &inner, BooleanOperator::SymmetricDifference)) - 63.0).abs() < 1e-12
);
}
#[test]
fn containment_is_detected_in_both_directions() {
let outer = box_at([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]);
let inner = box_at([1.0, 1.0, 1.0], [2.0, 2.0, 2.0]);
assert_eq!(
apply(&inner, &outer, BooleanOperator::Difference).triangle_count(),
0
);
assert!((volume(&apply(&outer, &inner, BooleanOperator::Difference)) - 63.0).abs() < 1e-12);
}
#[test]
fn identical_operands_are_idempotent_and_annihilating() {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
assert!((volume(&apply(&a, &a, BooleanOperator::Union)) - 1.0).abs() < 1e-12);
assert!((volume(&apply(&a, &a, BooleanOperator::Intersection)) - 1.0).abs() < 1e-12);
assert_eq!(
apply(&a, &a, BooleanOperator::Difference).triangle_count(),
0
);
assert_eq!(
apply(&a, &a, BooleanOperator::SymmetricDifference).triangle_count(),
0
);
}
#[test]
fn commutative_operations_are_order_independent() {
let a = box_at([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]);
let b = box_at([1.0, 1.0, 1.0], [2.0, 2.0, 2.0]);
for op in [
BooleanOperator::Union,
BooleanOperator::Intersection,
BooleanOperator::SymmetricDifference,
] {
let forward = volume(&apply(&a, &b, op));
let reverse = volume(&apply(&b, &a, op));
assert!(
(forward - reverse).abs() < 1e-12,
"{op:?} is commutative but gave {forward} vs {reverse}"
);
}
}
#[test]
fn interpenetrating_surfaces_are_answered_exactly() {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_at([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
assert!((volume(&apply(&a, &b, BooleanOperator::Union)) - 1.875).abs() < 1e-12);
assert!((volume(&apply(&a, &b, BooleanOperator::Intersection)) - 0.125).abs() < 1e-12);
assert!((volume(&apply(&a, &b, BooleanOperator::Difference)) - 0.875).abs() < 1e-12);
}
#[test]
fn shapes_beyond_the_exact_path_are_refused() {
let lower = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let upper = box_at([0.0, 0.0, 1.0], [1.0, 1.0, 2.0]);
assert!((volume(&apply(&lower, &upper, BooleanOperator::Union)) - 2.0).abs() < 1e-12);
}
#[test]
fn face_contact_is_not_interpenetration() {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_at([1.0, 0.0, 0.0], [2.0, 1.0, 1.0]);
let union = ScalarBoolean::new()
.boolean(&a, &b, BooleanOperator::Union, &options())
.expect("touching operands are not interpenetrating");
assert!((volume(&union.mesh) - 2.0).abs() < 1e-12);
}
#[test]
fn containment_is_exact_at_coordinates_that_defeat_floating_point() {
let outer = box_at([0.0, 0.0, 0.0], [1e9, 1e9, 1e9]);
let inner = box_at([1.0, 1.0, 1.0], [1e9 - 1.0, 1e9 - 1.0, 1e9 - 1.0]);
let difference = apply(&outer, &inner, BooleanOperator::Difference);
assert_eq!(difference.triangle_count(), 24);
assert!(
volume(&difference) > 0.0,
"cavity must not invert the solid"
);
}
#[test]
fn evidence_reports_the_arrangement() {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let far = box_at([9.0, 9.0, 9.0], [10.0, 10.0, 10.0]);
let outcome = ScalarBoolean::new()
.boolean(&a, &far, BooleanOperator::Union, &options())
.unwrap();
assert_eq!(outcome.evidence.disjoint_tools, 1);
assert_eq!(outcome.evidence.output_components, 2, "two separate solids");
}