mod support;
use axiolid_contracts::ExecutionOptions;
use axiolid_core::{BooleanOperator, Tolerance};
use axiolid_mesh::TriMesh;
use axiolid_mesh_boolean_contract::MeshBoolean;
use axiolid_mesh_boolean_boolmesh::BoolmeshBoolean;
use support::{boxx, volume};
fn options() -> ExecutionOptions {
ExecutionOptions::new(Tolerance::MILLIMETRE)
}
fn sequential(subject: &TriMesh, tools: &[TriMesh]) -> TriMesh {
let provider = BoolmeshBoolean::new();
let mut current = subject.clone();
for tool in tools {
current = provider
.boolean(¤t, tool, BooleanOperator::Difference, &options())
.expect("sequential difference")
.mesh;
}
current
}
fn assert_same_volume(left: &TriMesh, right: &TriMesh, what: &str) {
let (a, b) = (volume(left), volume(right));
assert!(
(a - b).abs() <= 1e-9 * a.abs().max(1.0),
"{what}: grouped volume {b} disagrees with sequential {a}"
);
}
#[test]
fn disjoint_cutters_agree_with_the_sequential_path() {
let wall = boxx(2.0, 0.1, 0.0, 4.0, 0.2, 3.0, 0.0);
let tools: Vec<_> = (0..6)
.map(|i| boxx(0.5 + f64::from(i) * 0.6, 0.1, 0.5, 0.3, 0.5, 1.0, 0.0))
.collect();
let expected = sequential(&wall, &tools);
let actual = BoolmeshBoolean::new()
.subtract_many(&wall, &tools, &options())
.expect("grouped")
.mesh;
assert_same_volume(&expected, &actual, "disjoint");
assert!(
volume(&actual) < volume(&wall),
"cutting must remove volume"
);
}
#[test]
fn overlapping_cutters_are_not_fused() {
let wall = boxx(2.0, 0.1, 0.0, 4.0, 0.2, 3.0, 0.0);
let tools: Vec<_> = [0.4, 0.8, 1.2]
.into_iter()
.map(|s| boxx(2.0, 0.1, 1.0, s, 0.5, s, 0.0))
.collect();
let expected = sequential(&wall, &tools);
let actual = BoolmeshBoolean::new()
.subtract_many(&wall, &tools, &options())
.expect("grouped")
.mesh;
assert_same_volume(&expected, &actual, "overlapping");
}
#[test]
fn a_mixed_layout_groups_only_what_is_disjoint() {
let wall = boxx(3.0, 0.1, 0.0, 6.0, 0.2, 3.0, 0.0);
let tools = vec![
boxx(1.0, 0.1, 0.5, 0.4, 0.5, 1.0, 0.0), boxx(3.0, 0.1, 0.5, 0.6, 0.5, 1.0, 0.0), boxx(3.2, 0.1, 0.5, 0.6, 0.5, 1.0, 0.0), boxx(5.0, 0.1, 0.5, 0.4, 0.5, 1.0, 0.0), ];
let expected = sequential(&wall, &tools);
let actual = BoolmeshBoolean::new()
.subtract_many(&wall, &tools, &options())
.expect("grouped")
.mesh;
assert_same_volume(&expected, &actual, "mixed");
}
#[test]
fn empty_and_single_tool_batches_behave_like_the_default() {
let wall = boxx(2.0, 0.1, 0.0, 4.0, 0.2, 3.0, 0.0);
let provider = BoolmeshBoolean::new();
let none = provider
.subtract_many(&wall, &[], &options())
.expect("no tools")
.mesh;
assert_same_volume(&wall, &none, "empty batch");
let tool = boxx(2.0, 0.1, 1.0, 0.5, 0.5, 1.0, 0.0);
let one = provider
.subtract_many(&wall, std::slice::from_ref(&tool), &options())
.expect("one tool")
.mesh;
let expected = sequential(&wall, std::slice::from_ref(&tool));
assert_same_volume(&expected, &one, "single tool");
}
#[test]
fn grouped_and_sequential_agree_across_random_layouts() {
let mut state = 0x9E37_79B9_7F4A_7C15_u64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state >> 11) as f64 / (1_u64 << 53) as f64
};
let provider = BoolmeshBoolean::new();
let mut grouped_wins = 0usize;
for case in 0..25 {
let wall = boxx(3.0, 0.1, 0.0, 6.0, 0.2, 3.0, 0.0);
let count = 2 + (case % 5);
let tools: Vec<_> = (0..count)
.map(|_| {
let cx = 0.6 + next() * 4.8;
let width = 0.2 + next() * 0.5;
boxx(cx, 0.1, 0.5, width, 0.5, 1.0, 0.0)
})
.collect();
let expected = sequential(&wall, &tools);
let actual = provider
.subtract_many(&wall, &tools, &options())
.expect("grouped")
.mesh;
assert_same_volume(&expected, &actual, &format!("random case {case}"));
if actual.triangle_count() != wall.triangle_count() {
grouped_wins += 1;
}
}
assert!(
grouped_wins >= 20,
"most random cases must actually cut, else the gate proves nothing"
);
}