extern crate std;
use crate::fields::Sphere;
use crate::marching_cubes::MarchingCubes;
use crate::mesh::MeshBuffer;
use crate::validate;
use crate::{RuntimeShape3, Sdf, Shape3};
use super::{MeshField, signed_distance_from_mesh};
fn sample_grid<F: Sdf<Scalar = f64>>(
field: &F,
shape: &RuntimeShape3,
origin: [f64; 3],
h: f64,
) -> std::vec::Vec<f64> {
let size = shape.size();
let mut out = std::vec::Vec::with_capacity(shape.element_count());
for z in 0..size[2] {
for y in 0..size[1] {
for x in 0..size[0] {
out.push(field.sample([
origin[0] + h * f64::from(x),
origin[1] + h * f64::from(y),
origin[2] + h * f64::from(z),
]));
}
}
}
out
}
fn mesh(values: &[f64], shape: &RuntimeShape3, origin: [f64; 3], h: f64) -> MeshBuffer<f64> {
let mut out = MeshBuffer::new();
let mut mc = MarchingCubes::<f64>::new();
let field = crate::construct::SampledField::new(values, shape, origin, h).expect("wrap");
mc.extract(&field, shape, origin, h, &mut out)
.expect("extraction");
out
}
#[test]
fn a_sphere_survives_the_round_trip() {
let field = Sphere::<f64>::canonical();
let shape = RuntimeShape3::new([33; 3]).expect("valid shape");
let h = 0.125_f64;
let origin = [-2.0; 3];
let analytic = sample_grid(&field, &shape, origin, h);
let first = mesh(&analytic, &shape, origin, h);
assert!(!first.indices.is_empty(), "nothing meshed");
let rebuilt = signed_distance_from_mesh(&first.positions, &first.indices, &shape, origin, h)
.expect("mesh to field");
let second = mesh(&rebuilt, &shape, origin, h);
let mut sign_disagreements = 0usize;
for (i, (&a, &b)) in analytic.iter().zip(&rebuilt).enumerate() {
if a.abs() > 0.5 * h && (a < 0.0) != (b < 0.0) {
sign_disagreements += 1;
if sign_disagreements < 4 {
std::println!(" sample {i}: analytic {a:.5}, rebuilt {b:.5}");
}
}
}
assert_eq!(
sign_disagreements, 0,
"the pseudonormal sign disagrees with the analytic field"
);
let mut worst = 0.0f64;
for p in &second.positions {
worst = worst.max(field.sample(*p).abs());
}
let mut worst_first = 0.0f64;
for p in &first.positions {
worst_first = worst_first.max(field.sample(*p).abs());
}
let cfg = validate::ValidateConfig::from_cell_size(h).expect("valid cell size");
let a = validate::validate(&first, &cfg);
let b = validate::validate(&second, &cfg);
std::println!(
"measured: round trip — {} → {} vertices, {} → {} triangles; \
worst |field| on vertices {worst_first:.6} → {worst:.6}",
first.positions.len(),
second.positions.len(),
first.indices.len() / 3,
second.indices.len() / 3,
);
std::println!(
"measured: χ {} → {}, boundary edges {} → {}, non-manifold edges {} → {}",
a.euler_characteristic,
b.euler_characteristic,
a.boundary_edges,
b.boundary_edges,
a.non_manifold_edges,
b.non_manifold_edges
);
assert!(b.is_closed(), "the round trip opened the surface");
assert_eq!(
a.euler_characteristic, b.euler_characteristic,
"the round trip changed the genus"
);
assert!(
worst < 0.5 * h,
"re-meshed vertices sit {worst} from the analytic sphere, which is {} cells",
worst / h
);
}
#[test]
fn pseudonormals_collapse_to_the_face_normal_on_a_lone_triangle() {
let positions = std::vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let indices = std::vec![0u32, 1, 2];
let n = super::Pseudonormals::build(&positions, &indices);
#[allow(clippy::float_cmp)]
{
assert!(n.face[0] == [0.0, 0.0, 1.0]);
for edge in 0..3u8 {
assert!(n.at(&indices, 0, super::Feature::Edge(edge)) == [0.0, 0.0, 1.0]);
}
}
let total: f64 = (0..3).map(|i| n.vertex[i][2]).sum();
assert!(
(total - core::f64::consts::PI).abs() < 1e-12,
"interior angles sum to {total}, not π"
);
assert!((n.vertex[0][2] - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
}
#[test]
fn the_sign_is_right_diagonally_outside_a_cube_corner() {
let positions = std::vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
[0.0, 1.0, 1.0],
];
let indices: std::vec::Vec<u32> = std::vec![
0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 3, 7, 6, 3, 6, 2, 0, 4, 7, 0, 7, 3, 1, 2, 6, 1, 6, 5, ];
let shape = RuntimeShape3::new([9; 3]).expect("valid shape");
let h = 0.25_f64;
let origin = [-0.5; 3];
let out =
signed_distance_from_mesh(&positions, &indices, &shape, origin, h).expect("mesh to field");
let size = shape.size();
let at = |x: u32, y: u32, z: u32| out[((z * size[1] + y) * size[0] + x) as usize];
for (x, y, z) in [
(0, 0, 0),
(8, 0, 0),
(0, 8, 0),
(8, 8, 0),
(0, 0, 8),
(8, 0, 8),
(0, 8, 8),
(8, 8, 8),
] {
let v = at(x, y, z);
assert!(v > 0.0, "sample ({x},{y},{z}) is outside but read {v}");
assert!(
(v - (3.0f64).sqrt() / 2.0).abs() < 1e-12,
"corner distance {v}"
);
}
assert!(
(at(4, 4, 4) + 0.5).abs() < 1e-12,
"centre read {}",
at(4, 4, 4)
);
}
#[test]
fn it_refuses_what_it_cannot_mesh() {
let positions = std::vec![[0.0f64; 3]; 3];
let shape = RuntimeShape3::new([4; 3]).expect("valid shape");
assert!(signed_distance_from_mesh(&positions, &[0, 1], &shape, [0.0; 3], 0.1).is_err());
assert!(signed_distance_from_mesh(&positions, &[0, 1, 9], &shape, [0.0; 3], 0.1).is_err());
let tiny = RuntimeShape3::new([1, 4, 4]).expect("valid shape");
assert!(signed_distance_from_mesh(&positions, &[0, 1, 2], &tiny, [0.0; 3], 0.1).is_err());
}
#[test]
fn the_reject_agrees_with_brute_force_and_is_faster() {
use std::time::Instant;
let field = Sphere::<f64>::canonical();
let shape = RuntimeShape3::new([21; 3]).expect("valid shape");
let h = 0.2_f64;
let origin = [-2.0; 3];
let analytic = sample_grid(&field, &shape, origin, h);
let m = mesh(&analytic, &shape, origin, h);
let triangles = m.indices.len() / 3;
let start = Instant::now();
let fast = signed_distance_from_mesh(&m.positions, &m.indices, &shape, origin, h)
.expect("mesh to field");
let fast_ms = start.elapsed().as_secs_f64() * 1e3;
let start = Instant::now();
let normals = super::Pseudonormals::build(&m.positions, &m.indices);
let size = shape.size();
let mut slow = std::vec::Vec::with_capacity(shape.element_count());
for z in 0..size[2] {
for y in 0..size[1] {
for x in 0..size[0] {
let p = [
origin[0] + h * f64::from(x),
origin[1] + h * f64::from(y),
origin[2] + h * f64::from(z),
];
let mut best = f64::INFINITY;
let mut sign = 1.0;
for t in 0..triangles {
let tri = &m.indices[t * 3..t * 3 + 3];
let (c, feature) = super::closest_on_triangle(
p,
m.positions[tri[0] as usize],
m.positions[tri[1] as usize],
m.positions[tri[2] as usize],
);
let r = [p[0] - c[0], p[1] - c[1], p[2] - c[2]];
let d = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt();
if d < best {
best = d;
let n = normals.at(&m.indices, t, feature);
sign = if n[0] * r[0] + n[1] * r[1] + n[2] * r[2] < 0.0 {
-1.0
} else {
1.0
};
}
}
slow.push(best * sign);
}
}
}
let brute_ms = start.elapsed().as_secs_f64() * 1e3;
#[allow(clippy::float_cmp)]
{
for (i, (a, b)) in fast.iter().zip(&slow).enumerate() {
assert!(*a == *b, "sample {i}: reject {a} vs brute force {b}");
}
}
std::println!(
"measured: {} samples × {triangles} triangles — box reject {fast_ms:.1} ms, \
brute force {brute_ms:.1} ms ({:.1}×)",
shape.element_count(),
brute_ms / fast_ms
);
assert!(
fast_ms < brute_ms,
"the box reject lost to brute force: {fast_ms} vs {brute_ms} ms"
);
}
#[test]
fn the_grid_path_is_this_field_in_a_loop() {
let field = Sphere::<f64>::canonical();
let shape = RuntimeShape3::new([33; 3]).expect("valid shape");
let h = 0.125_f64;
let origin = [-2.0; 3];
let meshed = mesh(&sample_grid(&field, &shape, origin, h), &shape, origin, h);
assert!(!meshed.indices.is_empty(), "nothing meshed");
let batch = signed_distance_from_mesh(&meshed.positions, &meshed.indices, &shape, origin, h)
.expect("mesh to field");
let on_demand = MeshField::new(&meshed.positions, &meshed.indices).expect("build");
assert_eq!(on_demand.triangles(), meshed.indices.len() / 3);
let queried = sample_grid(&on_demand, &shape, origin, h);
assert_eq!(batch.len(), queried.len());
let differing = batch
.iter()
.zip(&queried)
.filter(|(a, b)| a.to_bits() != b.to_bits())
.count();
assert_eq!(
differing,
0,
"the batch path and the on-demand path disagree at {differing} of {} samples",
batch.len()
);
}
#[test]
fn a_ragged_tail_is_dropped_and_a_bad_index_is_refused() {
let positions = std::vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let ragged = MeshField::new(&positions, &[0, 1, 2, 0]).expect("build");
assert_eq!(
ragged.triangles(),
1,
"the trailing index is not a triangle"
);
let err = MeshField::new(&positions, &[0, 1, 3]).expect_err("index 3 does not exist");
assert!(
matches!(
err,
crate::Error::IndexOutOfRange {
at: 2,
index: 3,
vertices: 3
}
),
"expected IndexOutOfRange, got {err:?}"
);
}