use axiolid_construct::boolean_exact::{boolean_arc_prisms_exact, ArcPrism};
use axiolid_core::{BooleanOperator, Point2, Tolerance};
use axiolid_overlay::{ArcRing, ArcVertex};
use axiolid_surface::Surface;
fn disc(cx: f64, cy: f64, r: f64) -> ArcRing {
ArcRing::circle(Point2::new(cx, cy), r)
}
fn square(half: f64) -> ArcRing {
ArcRing {
vertices: vec![
ArcVertex::straight(Point2::new(-half, -half)),
ArcVertex::straight(Point2::new(half, -half)),
ArcVertex::straight(Point2::new(half, half)),
ArcVertex::straight(Point2::new(-half, half)),
],
}
}
fn prism(section: ArcRing, top: f64) -> ArcPrism {
ArcPrism {
section,
bottom: 0.0,
top,
}
}
fn surface_kinds(solid: &axiolid_brep::ExactBRep) -> (usize, usize) {
let mut planes = 0;
let mut cylinders = 0;
for surface in solid.surfaces() {
match surface {
Surface::Plane(_) => planes += 1,
Surface::Cylinder(_) => cylinders += 1,
_ => {}
}
}
(planes, cylinders)
}
#[test]
fn a_cylinder_intersected_with_a_box_keeps_a_cylindrical_face() {
let cylinder = prism(disc(0.0, 0.0, 1.0), 2.0);
let box_solid = prism(square(0.8), 2.0);
let solid = boolean_arc_prisms_exact(
&cylinder,
&box_solid,
BooleanOperator::Intersection,
Tolerance::METRE,
)
.expect("a cylinder clipped by a box is representable");
let (planes, cylinders) = surface_kinds(&solid);
assert!(
cylinders > 0,
"the curved walls must stay cylindrical, got {cylinders} cylinder faces"
);
assert!(
planes >= 2,
"the two caps are planar at minimum, got {planes}"
);
}
#[test]
fn the_clipped_cylinder_is_a_sound_brep_of_the_closed_form_area() {
let cylinder = prism(disc(0.0, 0.0, 1.0), 2.0);
let box_solid = prism(square(0.8), 2.0);
let solid = boolean_arc_prisms_exact(
&cylinder,
&box_solid,
BooleanOperator::Intersection,
Tolerance::METRE,
)
.expect("representable");
assert!(
solid.topology().faces().len() >= 4,
"two caps plus at least two walls, got {}",
solid.topology().faces().len()
);
let mut checked = 0;
for surface in solid.surfaces() {
if let Surface::Cylinder(cylinder) = surface {
assert!(
(cylinder.radius - 1.0).abs() < 1.0e-12,
"wall radius drifted to {}",
cylinder.radius
);
checked += 1;
}
}
assert!(checked > 0, "no cylindrical wall was checked");
}
#[test]
fn a_result_with_an_interior_hole_is_a_plate_with_a_round_passage() {
let plate = prism(square(3.0), 1.0);
let hole = prism(disc(0.0, 0.0, 0.5), 1.0);
let solid =
boolean_arc_prisms_exact(&plate, &hole, BooleanOperator::Difference, Tolerance::METRE)
.expect("a holed result is representable");
let health = axiolid_brep_audit::geometric_audit(&solid, Tolerance::METRE);
assert!(health.is_consistent(), "{:?}", health.defects());
let faces = solid.topology().faces();
let caps_with_two_bounds = faces.iter().filter(|face| face.bounds.len() == 2).count();
assert_eq!(caps_with_two_bounds, 2, "both caps must carry the opening");
let (_, cylinders) = surface_kinds(&solid);
assert!(cylinders >= 1, "the passage wall must be cylindrical");
let on_passage = solid
.topology()
.vertices()
.iter()
.filter(|v| {
let r = (v.position.x.powi(2) + v.position.y.powi(2)).sqrt();
(r - 0.5).abs() < 1e-9
})
.count();
assert!(on_passage >= 4, "only {on_passage} vertices on the passage");
}
#[test]
fn a_result_starting_above_the_ground_plane_stays_at_its_height() {
let column = prism(disc(0.0, 0.0, 1.0), 10.0);
let slab = ArcPrism {
section: square(0.8),
bottom: 3.0,
top: 3.5,
};
let solid = boolean_arc_prisms_exact(
&column,
&slab,
BooleanOperator::Intersection,
Tolerance::METRE,
)
.expect("a raised intersection is representable");
let health = axiolid_brep_audit::geometric_audit(&solid, Tolerance::METRE);
assert!(health.is_consistent(), "{:?}", health.defects());
let heights: Vec<f64> = solid
.topology()
.vertices()
.iter()
.map(|v| v.position.z)
.collect();
let low = heights.iter().copied().fold(f64::INFINITY, f64::min);
let high = heights.iter().copied().fold(f64::NEG_INFINITY, f64::max);
assert_eq!((low, high), (3.0, 3.5));
}
#[test]
fn differing_spans_give_a_stepped_solid_with_cylinder_walls() {
let short = prism(disc(0.0, 0.0, 1.0), 1.0);
let tall = prism(disc(0.5, 0.0, 1.0), 5.0);
let solid = boolean_arc_prisms_exact(&short, &tall, BooleanOperator::Union, Tolerance::METRE)
.expect("a stepped union is an exact solid");
let (_, cylinders) = surface_kinds(&solid);
assert!(cylinders >= 2, "both discs keep cylindrical walls");
for surface in solid.surfaces() {
assert!(
matches!(surface, Surface::Plane(_) | Surface::Cylinder(_)),
"no approximating surface: {surface:?}"
);
}
let health = axiolid_brep_audit::geometric_audit(&solid, Tolerance::METRE);
assert!(health.is_consistent(), "{:?}", health.defects());
}
#[test]
fn a_cylindrical_wall_is_centred_on_the_disc_axis() {
let cylinder = prism(disc(0.0, 0.0, 1.0), 2.0);
let box_solid = prism(square(0.8), 2.0);
let solid = boolean_arc_prisms_exact(
&cylinder,
&box_solid,
BooleanOperator::Intersection,
Tolerance::METRE,
)
.expect("representable");
let mut checked = 0;
for surface in solid.surfaces() {
if let Surface::Cylinder(cylinder) = surface {
let origin = cylinder.frame.origin;
assert!(
origin.x.abs() < 1.0e-12 && origin.y.abs() < 1.0e-12,
"wall axis moved off the disc centre to ({}, {})",
origin.x,
origin.y
);
checked += 1;
}
}
assert!(checked > 0, "no cylindrical wall was checked");
}
#[test]
fn opposite_bulges_curve_to_opposite_sides() {
let tool = ArcPrism {
section: square(50.0),
bottom: 0.0,
top: 1.0,
};
let centre_of = |bulge: f64| {
let section = ArcRing::new(vec![
ArcVertex::bulged(Point2::new(0.0, 0.0), bulge),
ArcVertex::straight(Point2::new(2.0, 0.0)),
ArcVertex::straight(Point2::new(2.0, 2.0)),
ArcVertex::straight(Point2::new(0.0, 2.0)),
]);
let subject = ArcPrism {
section,
bottom: 0.0,
top: 1.0,
};
let solid = boolean_arc_prisms_exact(
&subject,
&tool,
BooleanOperator::Intersection,
Tolerance::METRE,
)
.expect("a bulged ring clipped by a large square");
solid
.surfaces()
.iter()
.find_map(|surface| match surface {
Surface::Cylinder(cylinder) => Some(cylinder.frame.origin),
_ => None,
})
.expect("a cylindrical wall")
};
let positive = centre_of(0.5);
let negative = centre_of(-0.5);
assert!(
positive.y * negative.y < 0.0,
"opposite bulges must place centres on opposite sides, got {} and {}",
positive.y,
negative.y
);
assert!(
(positive.y + negative.y).abs() < 1e-12,
"equal magnitudes must mirror exactly, got {} and {}",
positive.y,
negative.y
);
}