use glam::DVec3;
use pantometry_shape::{Mesh, Triangle, Voxels};
use pantometry_units::{Length, Volume};
fn box_mesh(low: DVec3, high: DVec3) -> Mesh {
let v = |x: f64, y: f64, z: f64| DVec3::new(x, y, z);
let (l, h) = (low, high);
let corner = [
v(l.x, l.y, l.z),
v(h.x, l.y, l.z),
v(h.x, h.y, l.z),
v(l.x, h.y, l.z),
v(l.x, l.y, h.z),
v(h.x, l.y, h.z),
v(h.x, h.y, h.z),
v(l.x, h.y, h.z),
];
let face = [
[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4], [3, 7, 6, 2], [0, 4, 7, 3], [1, 2, 6, 5], ];
let mut triangles = Vec::with_capacity(12);
for f in face {
triangles.push(Triangle {
a: corner[f[0]],
b: corner[f[1]],
c: corner[f[2]],
});
triangles.push(Triangle {
a: corner[f[0]],
b: corner[f[2]],
c: corner[f[3]],
});
}
Mesh::new(triangles)
}
fn sphere_mesh(r: f64, bands: usize, sectors: usize) -> Mesh {
let point = |b: usize, s: usize| {
if b == 0 {
return DVec3::new(0.0, 0.0, r);
}
if b == bands {
return DVec3::new(0.0, 0.0, -r);
}
let phi = std::f64::consts::PI * b as f64 / bands as f64;
let theta = 2.0 * std::f64::consts::PI * (s % sectors) as f64 / sectors as f64;
DVec3::new(
r * phi.sin() * theta.cos(),
r * phi.sin() * theta.sin(),
r * phi.cos(),
)
};
let mut triangles = Vec::new();
for b in 0..bands {
for s in 0..sectors {
let (a, bb, c, d) = (
point(b, s),
point(b, s + 1),
point(b + 1, s + 1),
point(b + 1, s),
);
if b == 0 {
triangles.push(Triangle { a, b: d, c });
} else if b + 1 == bands {
triangles.push(Triangle { a, b: c, c: bb });
} else {
triangles.push(Triangle { a, b: c, c: bb });
triangles.push(Triangle { a, b: d, c });
}
}
}
Mesh::new(triangles)
}
#[test]
fn a_box_measures_exactly_and_survives_both_stl_flavours() {
let (w, h, d) = (0.0301, 0.0203, 0.0107);
let mesh = box_mesh(DVec3::ZERO, DVec3::new(w, h, d));
let want = w * h * d;
assert!(mesh.is_closed(), "a box of twelve triangles is closed");
assert!(
(mesh.volume().to_si() / want - 1.0).abs() < 1e-15,
"volume {} against {want}",
mesh.volume().to_si()
);
let faces = 2.0 * (w * h + w * d + h * d);
assert!(
(mesh.area().to_si() / faces - 1.0).abs() < 1e-15,
"area {} against {faces}",
mesh.area().to_si()
);
let mut binary = vec![0u8; 84];
binary[80..84].copy_from_slice(&(mesh.triangles().len() as u32).to_le_bytes());
for t in mesh.triangles() {
binary.extend_from_slice(&[0u8; 12]); for v in [t.a, t.b, t.c] {
for c in [v.x, v.y, v.z] {
binary.extend_from_slice(&((c * 1e3) as f32).to_le_bytes());
}
}
binary.extend_from_slice(&[0u8; 2]);
}
let read = Mesh::from_stl(&binary).expect("binary STL parses");
assert_eq!(read.triangles().len(), 12);
let f32_error = (read.volume().to_si() / want - 1.0).abs();
println!(" the binary round-trip costs {f32_error:.3e}, which is f32 and not the reader");
assert!(
f32_error > 1e-9 && f32_error < 1e-6,
"the file stores f32, so this should be near 1e-7 -- and *nonzero*, or the coordinates chosen are dyadic and the check has no content: {f32_error:e}"
);
let mut ascii = String::from("solid box\n");
for t in mesh.triangles() {
ascii.push_str(" facet normal 0 0 0\n outer loop\n");
for v in [t.a, t.b, t.c] {
ascii.push_str(&format!(
" vertex {} {} {}\n",
v.x * 1e3,
v.y * 1e3,
v.z * 1e3
));
}
ascii.push_str(" endloop\n endfacet\n");
}
ascii.push_str("endsolid box\n");
let read = Mesh::from_stl(ascii.as_bytes()).expect("ASCII STL parses");
assert_eq!(read.triangles().len(), 12);
assert!(
(read.volume().to_si() / want - 1.0).abs() < 1e-15,
"ascii volume {} against {want}",
read.volume().to_si()
);
}
#[test]
fn a_cell_aligned_box_rasterises_with_nothing_lost() {
let mesh = box_mesh(DVec3::ZERO, DVec3::new(0.030, 0.020, 0.010));
for (cell_mm, cells) in [(2.0, (15, 10, 5)), (1.0, (30, 20, 10)), (0.5, (60, 40, 20))] {
let want_cells = cells.0 * cells.1 * cells.2;
let voxels = Voxels::of(&mesh, Length::mm(cell_mm)).expect("a box is closed");
let loss = voxels.loss();
println!(
" {cell_mm} mm: {} cells of {want_cells}, volume error {:.2e}, thin runs {}, ambiguous {}",
voxels.filled(),
loss.volume_error,
loss.thin_runs,
loss.ambiguous_rows
);
assert_eq!(
voxels.filled(),
want_cells,
"{cell_mm} mm: an aligned box has exactly this many cells in it"
);
assert!(
loss.volume_error.abs() < 1e-12,
"{cell_mm} mm: nothing should be lost, volume error is {:.3e}",
loss.volume_error
);
assert_eq!(loss.ambiguous_rows, 0, "{cell_mm} mm: no degenerate rows");
assert!(loss.is_clean(), "{cell_mm} mm: {loss:?}");
let (nx, ny, nz) = (cells.0 as f64, cells.1 as f64, cells.2 as f64);
let interior = (nx - 2.0) * (ny - 2.0) * (nz - 2.0);
let want = 1.0 - interior / (nx * ny * nz);
println!(
" boundary {:.6} against an exact {want:.6}",
loss.boundary_fraction
);
assert!(
(loss.boundary_fraction - want).abs() < 1e-12,
"{cell_mm} mm: the exposed cells are the box less its interior, {:.6} against {want:.6}",
loss.boundary_fraction
);
}
}
#[test]
fn a_sphere_has_a_first_order_boundary_and_an_erratic_volume_error() {
let r = 0.010;
let mesh = sphere_mesh(r, 64, 128);
assert!(mesh.is_closed(), "the tessellation is closed");
let meshed = mesh.volume().to_si();
let analytic = 4.0 / 3.0 * std::f64::consts::PI * r.powi(3);
println!(
" the tessellation holds {:.4}% less than the sphere it approximates",
(1.0 - meshed / analytic) * 100.0
);
assert!(
meshed < analytic && meshed / analytic > 0.99,
"a polyhedron inscribed in a sphere holds a little less: {meshed:e} against {analytic:e}"
);
let mut boundary = Vec::new();
for cell_mm in [2.0, 1.0, 0.5, 0.25] {
let voxels = Voxels::of(&mesh, Length::mm(cell_mm)).expect("closed");
let loss = voxels.loss();
let coefficient = loss.boundary_fraction / (3.0 * cell_mm * 1e-3 / r);
println!(
" {cell_mm} mm: volume error {:+.4}%, boundary {:.4} of the volume, \
coefficient {coefficient:.4}",
loss.volume_error * 100.0,
loss.boundary_fraction
);
assert!(
(0.25..=1.5).contains(&coefficient),
"{cell_mm} mm: column counting brackets this in [0.25, 1.5], is {coefficient:.4}"
);
if cell_mm <= 0.5 {
assert!(
(0.75..=0.90).contains(&coefficient),
"{cell_mm} mm: six-connectivity settles near 0.82 and twenty-six near 1.49; this is \
{coefficient:.4}, which is neither"
);
}
boundary.push((cell_mm, loss.boundary_fraction, coefficient));
}
for (n, pair) in boundary.windows(2).enumerate() {
let rate = pair[0].1 / pair[1].1;
let drift = pair[1].2 / pair[0].2;
println!(
" {} mm to {} mm: layer thinned {rate:.3}x, coefficient drifted {:+.2}%",
pair[0].0,
pair[1].0,
(drift - 1.0) * 100.0
);
assert!(
(1.85..=2.15).contains(&rate),
"pair {n}: a one-cell layer over a fixed area halves when the cell halves, this went {rate:.3}x"
);
assert!(
(drift - 1.0).abs() < 0.06,
"pair {n}: the band above is derived from this drift being under 6%, and it is {:.2}%",
(drift - 1.0) * 100.0
);
}
assert!(
boundary[0].1 > 0.4,
"2 mm on a sphere of radius 10 mm leaves 43% of the volume in boundary cells, and a caller \
choosing that cell size should be told so: {:.4}",
boundary[0].1
);
let voxels = Voxels::of(&mesh, Length::mm(0.5)).expect("closed");
let (nx, ny, nz) = voxels.counts();
let cell = voxels.cell().to_si();
let origin = voxels.origin().to_si();
let mut furthest_inside: f64 = 0.0;
let mut nearest_outside = f64::INFINITY;
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let centre = origin
+ glam::DVec3::new(i as f64 + 0.5, j as f64 + 0.5, k as f64 + 0.5) * cell;
let d = centre.length();
if voxels.contains(i, j, k) {
furthest_inside = furthest_inside.max(d);
} else {
nearest_outside = nearest_outside.min(d);
}
}
}
}
let inscribed = r * (std::f64::consts::PI / 64.0).cos();
println!(
" the furthest filled centre is at {:.5} mm and the nearest empty one at {:.5} mm, \
between the inscribed {:.5} and R = {:.5}",
furthest_inside * 1e3,
nearest_outside * 1e3,
inscribed * 1e3,
r * 1e3
);
assert!(
furthest_inside <= r,
"no cell centre outside the sphere may be filled, and one at {:.6} mm is",
furthest_inside * 1e3
);
assert!(
nearest_outside >= inscribed,
"no cell centre inside the tessellation's inscribed radius may be empty, and one at {:.6} mm is",
nearest_outside * 1e3
);
let (coarse_count, fine_count) = (
Voxels::of(&mesh, Length::mm(2.0))
.expect("closed")
.loss()
.small_triangles,
Voxels::of(&mesh, Length::mm(0.2))
.expect("closed")
.loss()
.small_triangles,
);
println!(
" of {} facets, {coarse_count} are under a 2 mm cell face and {fine_count} under a 0.2 mm one",
mesh.triangles().len()
);
assert_eq!(
coarse_count,
mesh.triangles().len(),
"every facet of a 64-band sphere of radius 10 mm is smaller than a 2 mm square"
);
assert!(
fine_count > 0 && fine_count < mesh.triangles().len(),
"and at 0.2 mm only some of them are, which is what says the count tracks the cell: {fine_count}"
);
println!(" and the volume error over a sweep that is not powers of two:");
let coarse = sphere_mesh(r, 32, 64);
let mut errors = Vec::new();
for cell_mm in [3.0, 2.5, 2.0, 1.5, 1.25, 1.0, 0.8, 0.625, 0.5] {
let loss = Voxels::of(&coarse, Length::mm(cell_mm))
.expect("closed")
.loss();
println!(
" {cell_mm:5} mm: {:+.4}%, boundary {:.5}",
loss.volume_error * 100.0,
loss.boundary_fraction
);
errors.push((cell_mm, loss.volume_error));
}
let worsened: Vec<_> = errors
.windows(2)
.filter(|p| p[1].1.abs() > p[0].1.abs())
.map(|p| (p[0].0, p[1].0))
.collect();
println!(" refining made it worse at {worsened:?}");
assert!(
!worsened.is_empty(),
"the whole point of this test is that the signed volume error is not monotone under \
refinement; if it has become monotone, either the rasteriser changed or this sweep no longer \
samples where it is not, and the claim in the documentation above needs re-earning"
);
let signed: Vec<f64> = errors.iter().map(|e| e.1).collect();
let mean = signed.iter().sum::<f64>() / signed.len() as f64;
let rms = (signed.iter().map(|e| e * e).sum::<f64>() / signed.len() as f64).sqrt();
println!(
" mean {:+.4}% against an RMS of {:.4}%, a ratio of {:.3} — biased, not centred",
mean * 100.0,
rms * 100.0,
mean.abs() / rms
);
assert!(
mean > 0.0 && mean.abs() < rms,
"the over-fill is a positive bias smaller than the scatter it sits in: mean {:+.4}% against \
RMS {:.4}%",
mean * 100.0,
rms * 100.0
);
assert!(
signed.iter().any(|e| *e < 0.0),
"and at least one sample still overshoots the other way, so the bias does not swamp the \
cancellation — this rests on a single point of the nine (1.5 mm) and would retire quietly if \
the sweep were changed"
);
}
#[test]
fn a_cube_keeps_the_rows_its_face_diagonal_runs_through() {
for (side_mm, cell_mm, n) in [
(8.0, 1.0, 8),
(8.0, 2.0, 4),
(10.0, 0.5, 20),
(12.0, 3.0, 4),
(5.0, 1.0, 5),
] {
let side = side_mm * 1e-3;
let mesh = box_mesh(DVec3::ZERO, DVec3::splat(side));
let voxels = Voxels::of(&mesh, Length::mm(cell_mm)).expect("a cube is closed");
let loss = voxels.loss();
println!(
" a {side_mm} mm cube at {cell_mm} mm: {} of {} cells, {} rows retried, {} ambiguous, error {:.2e}",
voxels.filled(),
n * n * n,
loss.retried_rows,
loss.ambiguous_rows,
loss.volume_error
);
assert_eq!(
voxels.filled(),
n * n * n,
"{side_mm} mm at {cell_mm} mm: rows on the face diagonal were dropped"
);
assert_eq!(
loss.ambiguous_rows, 0,
"{side_mm} mm at {cell_mm} mm: the fixed perturbations should recover every one of them"
);
assert!(
loss.retried_rows > 0,
"{side_mm} mm at {cell_mm} mm: a cube on cell boundaries sends rows through its face diagonals, so some row must have needed a moved ray"
);
assert!(
loss.volume_error.abs() < 1e-12,
"{side_mm} mm at {cell_mm} mm: {loss:?}"
);
}
let voxels =
Voxels::of(&box_mesh(DVec3::ZERO, DVec3::splat(0.008)), Length::mm(1.0)).expect("closed");
let (nx, _, nz) = voxels.counts();
let mut on_diagonal = 0;
for k in 0..nz {
for i in 0..nx {
if voxels.contains(i, k, k) {
on_diagonal += 1;
}
}
}
println!(" and the diagonal plane holds {on_diagonal} cells, not 0");
assert_eq!(
on_diagonal, 64,
"the plane where j equals k is the one the diagonal edge runs through, and it is solid metal"
);
}
#[test]
fn a_plate_thinner_than_a_cell_is_reported_rather_than_lost() {
let plate = box_mesh(DVec3::ZERO, DVec3::new(0.040, 0.040, 0.0004));
let meshed = plate.volume().to_si();
for cell_mm in [2.0, 1.0] {
let voxels = Voxels::of(&plate, Length::mm(cell_mm)).expect("closed");
let loss = voxels.loss();
let (nx, ny, nz) = voxels.counts();
let thickness = (0..nz)
.filter(|k| (0..ny).any(|j| (0..nx).any(|i| voxels.contains(i, j, *k))))
.count();
println!(
" {cell_mm} mm on a 0.4 mm plate: {} cells, {thickness} thick, volume error {:.1}%, \
thin runs {}",
voxels.filled(),
loss.volume_error * 100.0,
loss.thin_runs
);
assert!(
!loss.is_clean(),
"{cell_mm} mm cannot hold a 0.4 mm plate and must say so: {loss:?}"
);
assert!(
voxels.filled() == 0 || loss.thin_runs > 0,
"a plate at {cell_mm} mm is either gone or one cell thick"
);
}
let two = box_mesh(DVec3::ZERO, DVec3::new(0.040, 0.040, 0.0008));
let voxels = Voxels::of(&two, Length::mm(0.4)).expect("closed");
println!(
" a 0.8 mm plate at 0.4 mm: volume error {:.2e}, thin runs {}",
voxels.loss().volume_error,
voxels.loss().thin_runs
);
assert!(
voxels.loss().volume_error.abs() < 1e-12,
"two cells hold the plate's volume exactly"
);
assert!(
voxels.loss().thin_runs > 0,
"and two cells is still thin, which is the case the counter's second clause is for"
);
assert!(
!voxels.loss().is_clean(),
"so the report is not clean even though the volume is exact — which is the whole point of having a thin-run count beside a volume error: {:?}",
voxels.loss()
);
let voxels = Voxels::of(&plate, Length::mm(0.1)).expect("closed");
println!(
" 0.1 mm: volume error {:.3}%, thin runs {}",
voxels.loss().volume_error * 100.0,
voxels.loss().thin_runs
);
assert!(
voxels.loss().volume_error.abs() < 1e-9,
"an aligned plate at a cell that divides it is exact"
);
assert!(
(voxels.volume().to_si() / meshed - 1.0).abs() < 1e-9,
"and holds the plate's own volume"
);
}
#[test]
fn an_open_mesh_is_refused_and_one_bit_opens_it() {
let mut triangles = box_mesh(DVec3::ZERO, DVec3::new(0.01, 0.01, 0.01))
.triangles()
.to_vec();
let whole = Mesh::new(triangles.clone());
assert!(whole.is_closed());
assert!(Voxels::of(&whole, Length::mm(1.0)).is_ok());
triangles.pop();
let holed = Mesh::new(triangles.clone());
assert!(!holed.is_closed(), "eleven triangles cannot close a box");
let error = Voxels::of(&holed, Length::mm(1.0)).expect_err("refused");
assert!(
error.contains("not closed") && error.contains("parity"),
"the message should say what cannot be done and why: {error}"
);
let mut triangles = whole.triangles().to_vec();
let nudged = f64::from_bits(triangles[0].a.x.to_bits() + 1);
triangles[0].a.x = nudged;
assert!(
!Mesh::new(triangles).is_closed(),
"a vertex moved by one unit in the last place leaves an edge unshared, and a ray goes through \
it as readily as through a visible gap"
);
}
#[test]
fn rasterising_twice_gives_the_same_cells() {
let mesh = box_mesh(DVec3::ZERO, DVec3::new(0.010, 0.010, 0.010));
for cell_mm in [1.0, 0.7, 0.3] {
let a = Voxels::of(&mesh, Length::mm(cell_mm)).expect("closed");
let b = Voxels::of(&mesh, Length::mm(cell_mm)).expect("closed");
let (nx, ny, nz) = a.counts();
assert_eq!(a.counts(), b.counts());
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
assert_eq!(
a.contains(i, j, k),
b.contains(i, j, k),
"{cell_mm} mm: cell ({i}, {j}, {k}) differs between two runs"
);
}
}
}
assert_eq!(a.volume(), b.volume());
assert_eq!(a.loss(), b.loss());
}
}
#[test]
fn the_mistakes_a_caller_makes_are_refused() {
let mesh = box_mesh(DVec3::ZERO, DVec3::new(0.01, 0.01, 0.01));
for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
assert!(
Voxels::of(&mesh, Length::from_si(bad)).is_err(),
"a cell size of {bad} was accepted"
);
}
let empty = Mesh::new(Vec::new());
assert!(!empty.is_closed(), "nothing is not a closed surface");
assert_eq!(empty.volume(), Volume::from_si(0.0));
assert!(
empty.bounds().is_none(),
"the bounds of nothing are not a box"
);
assert!(Voxels::of(&empty, Length::mm(1.0)).is_err());
assert!(Mesh::from_stl(b"not an stl at all").is_err());
assert!(
Mesh::from_stl(b"solid x\n facet normal 0 0 0\n outer loop\n vertex 1 2 3\n")
.is_err(),
"a facet with one vertex is incomplete and should say so"
);
}