use axiolid_construct::offset::{offset_solid, shell_solid, OffsetDirection};
use axiolid_construct::polyhedron::{triangulate, Polyhedron};
use axiolid_core::{Point3, Tolerance};
use axiolid_heal::mesh::MeshHealer;
use axiolid_heal::{self_intersections, Diagnose};
use axiolid_measure::volume_properties;
fn tol() -> Tolerance {
Tolerance::new(1e-6, 1e-9).expect("tolerance")
}
fn box_solid(min: [f64; 3], max: [f64; 3]) -> Polyhedron {
let [x0, y0, z0] = min;
let [x1, y1, z1] = max;
let p = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
Polyhedron::new(vec![
vec![p(x0, y0, z0), p(x0, y1, z0), p(x1, y1, z0), p(x1, y0, z0)],
vec![p(x0, y0, z1), p(x1, y0, z1), p(x1, y1, z1), p(x0, y1, z1)],
vec![p(x0, y0, z0), p(x1, y0, z0), p(x1, y0, z1), p(x0, y0, z1)],
vec![p(x0, y1, z0), p(x0, y1, z1), p(x1, y1, z1), p(x1, y1, z0)],
vec![p(x0, y0, z0), p(x0, y0, z1), p(x0, y1, z1), p(x0, y1, z0)],
vec![p(x1, y0, z0), p(x1, y1, z0), p(x1, y1, z1), p(x1, y0, z1)],
])
.expect("box is a valid solid")
}
fn volume(solid: &Polyhedron) -> f64 {
volume_properties(&triangulate(solid), tol())
.expect("closed solid")
.signed_volume
}
#[test]
fn a_cube_offset_outward_grows_by_twice_the_distance() {
let cube = box_solid([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
let d = 0.5;
let grown = offset_solid(&cube, d, OffsetDirection::Outward).expect("offset");
let expected = (2.0 + 2.0 * d).powi(3);
let actual = volume(&grown);
assert!(
(actual - expected).abs() < 1e-9,
"cube offset outward by {d}: expected {expected}, got {actual}"
);
}
#[test]
fn a_cube_offset_inward_shrinks_by_twice_the_distance() {
let cube = box_solid([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
let d = 0.25;
let shrunk = offset_solid(&cube, d, OffsetDirection::Inward).expect("offset");
let expected = (2.0 - 2.0 * d).powi(3);
let actual = volume(&shrunk);
assert!(
(actual - expected).abs() < 1e-9,
"cube offset inward by {d}: expected {expected}, got {actual}"
);
}
#[test]
fn an_offset_result_is_closed_manifold_and_clean() {
let cube = box_solid([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
for direction in [OffsetDirection::Outward, OffsetDirection::Inward] {
let result = offset_solid(&cube, 0.3, direction).expect("offset");
let mesh = triangulate(&result);
let diagnosis = MeshHealer.diagnose(&mesh, tol()).expect("diagnose");
assert!(
diagnosis.is_clean(),
"{direction:?} offset produced defects: {:?}",
diagnosis.defects
);
assert!(
self_intersections(&mesh).is_empty(),
"{direction:?} offset self-intersects"
);
}
}
#[test]
fn an_inward_offset_past_the_half_thickness_is_refused() {
let cube = box_solid([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
let error = offset_solid(&cube, 1.5, OffsetDirection::Inward)
.expect_err("over-offset must be refused, not emitted");
let text = format!("{error}");
assert!(
text.contains("half-thickness") || text.contains("passes through itself"),
"the refusal must name the collapse, got: {text}"
);
}
#[test]
fn an_exactly_collapsing_offset_is_refused() {
let cube = box_solid([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
assert!(
offset_solid(&cube, 1.0, OffsetDirection::Inward).is_err(),
"an offset that collapses the solid to nothing must be refused"
);
}
#[test]
fn a_non_positive_distance_is_refused() {
let cube = box_solid([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
assert!(offset_solid(&cube, 0.0, OffsetDirection::Outward).is_err());
assert!(offset_solid(&cube, -1.0, OffsetDirection::Outward).is_err());
assert!(offset_solid(&cube, f64::NAN, OffsetDirection::Outward).is_err());
}
#[test]
fn a_shelled_box_has_the_volume_of_outer_minus_inner() {
let outer = box_solid([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]);
let thickness = 0.5;
let shelled = shell_solid(&outer, thickness).expect("shell");
let expected = 4.0_f64.powi(3) - 3.0_f64.powi(3);
let actual = volume(&shelled);
assert!(
(actual - expected).abs() < 1e-9,
"shelled box: expected {expected}, got {actual}"
);
}
#[test]
fn a_shelled_box_has_the_requested_wall_thickness() {
let outer = box_solid([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]);
let thickness = 0.75;
let shelled = shell_solid(&outer, thickness).expect("shell");
let xs: Vec<f64> = shelled
.faces()
.iter()
.flat_map(|f| f.iter().map(|p| p.x))
.collect();
let inner_low = xs
.iter()
.copied()
.filter(|&x| x > 1e-9)
.fold(f64::INFINITY, f64::min);
assert!(
(inner_low - thickness).abs() < 1e-9,
"wall thickness: expected {thickness}, measured {inner_low}"
);
}
#[test]
fn a_thickness_that_collapses_the_cavity_is_refused() {
let outer = box_solid([0.0, 0.0, 0.0], [2.0, 2.0, 2.0]);
assert!(
shell_solid(&outer, 1.5).is_err(),
"a wall thicker than the half-thickness leaves no cavity and must refuse"
);
}
fn l_prism(z0: f64, z1: f64) -> Polyhedron {
let p = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
let ring = [
(0.0, 0.0),
(4.0, 0.0),
(4.0, 2.0),
(2.0, 2.0),
(2.0, 4.0),
(0.0, 4.0),
];
let mut faces: Vec<Vec<Point3>> = Vec::new();
faces.push(ring.iter().rev().map(|&(x, y)| p(x, y, z0)).collect());
faces.push(ring.iter().map(|&(x, y)| p(x, y, z1)).collect());
for i in 0..ring.len() {
let (x0, y0) = ring[i];
let (x1, y1) = ring[(i + 1) % ring.len()];
faces.push(vec![
p(x0, y0, z0),
p(x1, y1, z0),
p(x1, y1, z1),
p(x0, y0, z1),
]);
}
Polyhedron::new(faces).expect("L-prism is a valid solid")
}
#[test]
fn a_non_convex_solid_offsets_its_reflex_edge_correctly() {
let l = l_prism(0.0, 2.0);
let d = 0.5;
let shrunk = offset_solid(&l, d, OffsetDirection::Inward).expect("offset");
let outer_side = (4.0 - d) - d;
let notch_side = (4.0 - d) - (2.0 - d);
let expected = (outer_side * outer_side - notch_side * notch_side) * (2.0 - 2.0 * d);
let actual = volume(&shrunk);
assert!(
(actual - expected).abs() < 1e-9,
"L-prism offset inward by {d}: expected {expected}, got {actual}"
);
}