use axiolid_brep::ExactBRep;
use axiolid_construct::boolean_exact::{
boolean_arc_prisms_exact, boolean_arc_prisms_exact_solids, boolean_prisms_exact,
boolean_prisms_exact_solids, ArcPrism, Prism,
};
use axiolid_contracts::GeomError;
use axiolid_core::{BooleanOperator, Point2, Tolerance};
use axiolid_overlay::{ArcRing, ArcVertex};
use axiolid_surface::Surface;
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<Point2> {
vec![
Point2::new(x0, y0),
Point2::new(x1, y0),
Point2::new(x1, y1),
Point2::new(x0, y1),
]
}
fn prism(ring: Vec<Point2>, bottom: f64, top: f64) -> Prism {
Prism {
rings: vec![ring],
bottom,
top,
}
}
fn arc_rect(x0: f64, y0: f64, x1: f64, y1: f64) -> ArcRing {
ArcRing {
vertices: rect(x0, y0, x1, y1)
.into_iter()
.map(ArcVertex::straight)
.collect(),
}
}
fn volume(solid: &ExactBRep) -> f64 {
axiolid_measure::exact_properties(solid, Tolerance::METRE)
.expect("all-planar piece is measurable")
.signed_volume
}
fn extent(solid: &ExactBRep) -> (f64, f64, f64, f64) {
let mut e = (
f64::INFINITY,
f64::NEG_INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
);
for v in solid.topology().vertices() {
e.0 = e.0.min(v.position.x);
e.1 = e.1.max(v.position.x);
e.2 = e.2.min(v.position.z);
e.3 = e.3.max(v.position.z);
}
e
}
fn assert_sound(solid: &ExactBRep) {
let health = axiolid_brep_audit::geometric_audit(solid, Tolerance::METRE);
assert!(health.is_consistent(), "{:?}", health.defects());
}
fn is_disconnected_refusal(error: &GeomError) -> bool {
matches!(error, GeomError::UnsupportedInput { input, .. } if input.contains("disconnected"))
}
#[test]
fn a_wall_cut_through_by_a_full_height_slot_is_two_walls() {
let wall = prism(rect(0.0, 0.0, 10.0, 1.0), 0.0, 3.0);
let slot = prism(rect(4.0, -1.0, 6.0, 2.0), 0.0, 3.0);
let error = boolean_prisms_exact(&wall, &slot, BooleanOperator::Difference, Tolerance::METRE)
.expect_err("one ExactBRep cannot hold two solids");
assert!(is_disconnected_refusal(&error), "{error:?}");
let pieces =
boolean_prisms_exact_solids(&wall, &slot, BooleanOperator::Difference, Tolerance::METRE)
.expect("two pieces are representable");
assert_eq!(pieces.len(), 2);
for piece in &pieces {
assert_sound(piece);
assert!((volume(piece) - 12.0).abs() < 1e-12, "{}", volume(piece));
}
assert_eq!(extent(&pieces[0]).0, 0.0, "left piece first");
assert_eq!(extent(&pieces[1]).1, 10.0, "right piece second");
}
#[test]
fn three_separate_islands_come_back_in_a_stable_order() {
let comb = Prism {
rings: vec![vec![
Point2::new(0.0, 0.0),
Point2::new(5.0, 0.0),
Point2::new(5.0, 3.0),
Point2::new(4.0, 3.0),
Point2::new(4.0, 1.0),
Point2::new(3.0, 1.0),
Point2::new(3.0, 3.0),
Point2::new(2.0, 3.0),
Point2::new(2.0, 1.0),
Point2::new(1.0, 1.0),
Point2::new(1.0, 3.0),
Point2::new(0.0, 3.0),
]],
bottom: 0.0,
top: 2.0,
};
let spine = prism(rect(-1.0, -1.0, 6.0, 1.0), 0.0, 2.0);
let pieces =
boolean_prisms_exact_solids(&comb, &spine, BooleanOperator::Difference, Tolerance::METRE)
.expect("three teeth");
assert_eq!(pieces.len(), 3);
let starts: Vec<f64> = pieces.iter().map(|p| extent(p).0).collect();
assert_eq!(starts, vec![0.0, 2.0, 4.0]);
for piece in &pieces {
assert_sound(piece);
assert!((volume(piece) - 4.0).abs() < 1e-12, "1 x 2 x 2 tooth");
}
}
#[test]
fn a_single_piece_matches_the_single_solid_entry_point() {
let wall = prism(rect(0.0, 0.0, 10.0, 1.0), 0.0, 3.0);
let notch = prism(rect(4.0, 0.5, 6.0, 2.0), 0.0, 3.0);
let one = boolean_prisms_exact(&wall, ¬ch, BooleanOperator::Difference, Tolerance::METRE)
.expect("a notch leaves one solid");
let all =
boolean_prisms_exact_solids(&wall, ¬ch, BooleanOperator::Difference, Tolerance::METRE)
.expect("same");
assert_eq!(all, vec![one]);
}
#[test]
fn an_empty_result_is_an_empty_list_not_an_error() {
let a = prism(rect(0.0, 0.0, 1.0, 1.0), 0.0, 1.0);
let far = prism(rect(5.0, 5.0, 6.0, 6.0), 0.0, 1.0);
let above = prism(rect(0.0, 0.0, 1.0, 1.0), 2.0, 3.0);
for (tool, what) in [(&far, "disjoint sections"), (&above, "disjoint heights")] {
let pieces =
boolean_prisms_exact_solids(&a, tool, BooleanOperator::Intersection, Tolerance::METRE)
.unwrap_or_else(|e| panic!("{what}: {e:?}"));
assert!(pieces.is_empty(), "{what}");
}
assert!(matches!(
boolean_prisms_exact(&a, &far, BooleanOperator::Intersection, Tolerance::METRE),
Err(GeomError::Degenerate(_))
));
}
#[test]
fn refusals_other_than_emptiness_are_kept() {
let a = prism(rect(0.0, 0.0, 1.0, 1.0), 0.0, 1.0);
let buried = prism(rect(0.25, 0.25, 0.75, 0.75), 0.25, 0.75);
let pieces =
boolean_prisms_exact_solids(&a, &buried, BooleanOperator::Difference, Tolerance::METRE)
.expect("a cavity is representable");
assert_eq!(pieces.len(), 1);
assert_eq!(pieces[0].topology().solids()[0].voids.len(), 1);
let bad = prism(rect(0.0, 0.0, 1.0, 1.0), 1.0, 0.0);
assert!(matches!(
boolean_prisms_exact_solids(&a, &bad, BooleanOperator::Union, Tolerance::METRE),
Err(GeomError::InvalidInput(_))
));
}
#[test]
fn a_disc_cut_by_a_strip_is_two_curved_pieces() {
let column = ArcPrism {
section: ArcRing::circle(Point2::new(0.0, 0.0), 1.0),
bottom: 0.0,
top: 2.0,
};
let strip = ArcPrism {
section: arc_rect(-0.25, -2.0, 0.25, 2.0),
bottom: 0.0,
top: 2.0,
};
let error = boolean_arc_prisms_exact(
&column,
&strip,
BooleanOperator::Difference,
Tolerance::METRE,
)
.expect_err("one ExactBRep cannot hold two solids");
assert!(is_disconnected_refusal(&error), "{error:?}");
let pieces = boolean_arc_prisms_exact_solids(
&column,
&strip,
BooleanOperator::Difference,
Tolerance::METRE,
)
.expect("two pieces");
assert_eq!(pieces.len(), 2);
let spans: Vec<(f64, f64)> = pieces
.iter()
.map(|p| {
let e = extent(p);
(e.0, e.1)
})
.collect();
assert!((spans[0].0 + 1.0).abs() < 1e-9 && (spans[0].1 + 0.25).abs() < 1e-9);
assert!((spans[1].0 - 0.25).abs() < 1e-9 && (spans[1].1 - 1.0).abs() < 1e-9);
for piece in &pieces {
assert_sound(piece);
let e = extent(piece);
assert_eq!((e.2, e.3), (0.0, 2.0));
let radii: Vec<f64> = piece
.surfaces()
.iter()
.filter_map(|s| match s {
Surface::Cylinder(c) => Some(c.radius),
_ => None,
})
.collect();
assert!(!radii.is_empty(), "the curved wall stays a cylinder");
assert!(radii.iter().all(|r| (r - 1.0).abs() < 1e-12), "{radii:?}");
}
}
#[test]
fn raised_curved_pieces_keep_their_height() {
let raised = ArcPrism {
section: ArcRing::circle(Point2::new(0.0, 0.0), 1.0),
bottom: 1.0,
top: 2.0,
};
let strip = ArcPrism {
section: arc_rect(-0.25, -2.0, 0.25, 2.0),
bottom: 0.0,
top: 5.0,
};
let pieces = boolean_arc_prisms_exact_solids(
&raised,
&strip,
BooleanOperator::Difference,
Tolerance::METRE,
)
.expect("the tool spans the subject's height");
assert_eq!(pieces.len(), 2);
for piece in &pieces {
assert_sound(piece);
let e = extent(piece);
assert_eq!((e.2, e.3), (1.0, 2.0));
}
}
#[test]
fn pieces_sharing_their_lowest_x_are_ordered_by_y() {
let bar = prism(rect(0.0, 0.0, 1.0, 9.0), 0.0, 1.0);
let slots = Prism {
rings: vec![rect(-1.0, 2.0, 2.0, 3.0)],
bottom: 0.0,
top: 1.0,
};
let first =
boolean_prisms_exact_solids(&bar, &slots, BooleanOperator::Difference, Tolerance::METRE)
.expect("two pieces");
assert_eq!(first.len(), 2);
let slots_two = Prism {
rings: vec![vec![
Point2::new(-1.0, 2.0),
Point2::new(2.0, 2.0),
Point2::new(2.0, 6.0),
Point2::new(-1.0, 6.0),
Point2::new(-1.0, 5.0),
Point2::new(1.5, 5.0),
Point2::new(1.5, 3.0),
Point2::new(-1.0, 3.0),
]],
bottom: 0.0,
top: 1.0,
};
let pieces = boolean_prisms_exact_solids(
&bar,
&slots_two,
BooleanOperator::Difference,
Tolerance::METRE,
)
.expect("three pieces");
assert_eq!(pieces.len(), 3);
let lows: Vec<f64> = pieces
.iter()
.map(|piece| {
piece
.topology()
.vertices()
.iter()
.map(|v| v.position.y)
.fold(f64::INFINITY, f64::min)
})
.collect();
assert_eq!(lows, vec![0.0, 3.0, 6.0]);
let volumes: Vec<f64> = pieces.iter().map(volume).collect();
for (got, want) in volumes.iter().zip([2.0, 2.0, 3.0]) {
assert!((got - want).abs() < 1e-12, "{volumes:?}");
}
}
#[test]
fn arc_pieces_are_ordered_left_to_right_whatever_the_operand_order() {
let right = ArcPrism {
section: ArcRing::circle(Point2::new(5.0, 0.0), 1.0),
bottom: 0.0,
top: 1.0,
};
let left = ArcPrism {
section: ArcRing::circle(Point2::new(0.0, 0.0), 1.0),
bottom: 0.0,
top: 1.0,
};
for (subject, tool) in [(&right, &left), (&left, &right)] {
let pieces = boolean_arc_prisms_exact_solids(
subject,
tool,
BooleanOperator::Union,
Tolerance::METRE,
)
.expect("two separate cylinders");
let lows: Vec<f64> = pieces
.iter()
.map(|piece| {
piece
.topology()
.vertices()
.iter()
.map(|v| v.position.x)
.fold(f64::INFINITY, f64::min)
})
.collect();
assert_eq!(lows.len(), 2);
assert!(lows[0] < 0.0 && lows[1] > 3.0, "left first: {lows:?}");
}
}
#[test]
fn a_slot_cut_through_the_middle_heights_leaves_two_slabs() {
let block = prism(rect(0.0, 0.0, 1.0, 1.0), 0.0, 3.0);
let plate = prism(rect(-1.0, -1.0, 2.0, 2.0), 1.0, 2.0);
let solids = boolean_prisms_exact_solids(
&block,
&plate,
BooleanOperator::Difference,
Tolerance::METRE,
)
.expect("two slabs");
assert_eq!(solids.len(), 2);
for solid in &solids {
assert_sound(solid);
assert!((volume(solid) - 1.0).abs() < 1e-12, "{}", volume(solid));
}
let heights: Vec<_> = solids.iter().map(|s| (extent(s).2, extent(s).3)).collect();
assert!(
heights.contains(&(0.0, 1.0)) && heights.contains(&(2.0, 3.0)),
"{heights:?}"
);
let error = boolean_prisms_exact(
&block,
&plate,
BooleanOperator::Difference,
Tolerance::METRE,
)
.expect_err("two pieces");
assert!(is_disconnected_refusal(&error), "{error:?}");
}