use axiolid_construct::boolean_exact::{boolean_prisms_exact, Prism};
use axiolid_contracts::GeomError;
use axiolid_core::{BooleanOperator, Point2, Tolerance};
fn rect_ring(cx: f64, cy: f64, w: f64, h: f64) -> Vec<Point2> {
let (hw, hh) = (w / 2.0, h / 2.0);
vec![
Point2::new(cx - hw, cy - hh),
Point2::new(cx + hw, cy - hh),
Point2::new(cx + hw, cy + hh),
Point2::new(cx - hw, cy + hh),
]
}
fn prism(rings: Vec<Vec<Point2>>, bottom: f64, top: f64) -> Prism {
Prism { rings, bottom, top }
}
fn base_extent(brep: &axiolid_brep::ExactBRep) -> (f64, f64) {
let points: Vec<(f64, f64)> = brep
.topology()
.vertices()
.iter()
.filter(|v| v.position.z.abs() < 1e-9)
.map(|v| (v.position.x, v.position.y))
.collect();
let (mut min_x, mut max_x) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut min_y, mut max_y) = (f64::INFINITY, f64::NEG_INFINITY);
for (x, y) in points {
min_x = min_x.min(x);
max_x = max_x.max(x);
min_y = min_y.min(y);
max_y = max_y.max(y);
}
(max_x - min_x, max_y - min_y)
}
fn base_area(brep: &axiolid_brep::ExactBRep) -> f64 {
let mut points: Vec<(f64, f64)> = brep
.topology()
.vertices()
.iter()
.filter(|v| v.position.z.abs() < 1e-9)
.map(|v| (v.position.x, v.position.y))
.collect();
points.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9);
(0..points.len())
.map(|i| {
let (x0, y0) = points[i];
let (x1, y1) = points[(i + 1) % points.len()];
x0 * y1 - x1 * y0
})
.sum::<f64>()
.abs()
/ 2.0
}
#[test]
fn a_general_exact_intersection_now_succeeds() {
let subject = prism(vec![rect_ring(0.0, 0.0, 4.0, 4.0)], 0.0, 3.0);
let tool = prism(vec![rect_ring(2.0, 0.0, 4.0, 4.0)], 0.0, 3.0);
let result = boolean_prisms_exact(
&subject,
&tool,
BooleanOperator::Intersection,
Tolerance::METRE,
)
.expect("coaxial prism intersection is exactly constructible");
assert!(
(base_area(&result) - 8.0).abs() < 1e-9,
"expected the 2x4 overlap area 8, got {}",
base_area(&result)
);
assert!(
result
.surfaces()
.iter()
.all(|s| matches!(s, axiolid_surface::Surface::Plane(_))),
"a prism boolean must stay planar-faced"
);
}
#[test]
fn a_wall_with_an_interior_opening_differences_exactly() {
let wall = prism(vec![rect_ring(0.0, 0.0, 10.0, 4.0)], 0.0, 3.0);
let opening = prism(vec![rect_ring(0.0, 0.0, 2.0, 2.0)], 0.0, 3.0);
let result = boolean_prisms_exact(
&wall,
&opening,
BooleanOperator::Difference,
Tolerance::METRE,
)
.expect("a full-height interior opening leaves one prism with a hole");
let (width, height) = base_extent(&result);
assert!(
(width - 10.0).abs() < 1e-9 && (height - 4.0).abs() < 1e-9,
"the outer boundary must be unchanged at 10 x 4, got {width} x {height}"
);
let cap_bounds = result
.topology()
.faces()
.iter()
.map(|f| f.bounds.len())
.max()
.expect("the solid has faces");
assert!(
cap_bounds >= 2,
"the opening must appear as a hole loop on the cap, got {cap_bounds}"
);
}
#[test]
fn a_union_with_differing_spans_is_refused() {
let short = prism(vec![rect_ring(0.0, 0.0, 4.0, 4.0)], 0.0, 1.0);
let tall = prism(vec![rect_ring(2.0, 0.0, 4.0, 4.0)], 0.0, 5.0);
let error = boolean_prisms_exact(&short, &tall, BooleanOperator::Union, Tolerance::METRE)
.expect_err("a stepped union is not a prism");
assert!(
matches!(
error,
GeomError::UnsupportedInput {
input: "exact prism union with differing extrusion spans",
..
}
),
"got {error:?}"
);
}
#[test]
fn a_difference_with_a_short_tool_is_refused() {
let subject = prism(vec![rect_ring(0.0, 0.0, 10.0, 4.0)], 0.0, 3.0);
let tool = prism(vec![rect_ring(0.0, 0.0, 2.0, 2.0)], 0.0, 1.5);
let error = boolean_prisms_exact(
&subject,
&tool,
BooleanOperator::Difference,
Tolerance::METRE,
)
.expect_err("a partial-height cut leaves a stepped solid");
assert!(
matches!(
error,
GeomError::UnsupportedInput {
input: "exact prism difference with a tool shorter than the subject",
..
}
),
"got {error:?}"
);
}