use alloc::vec;
use proptest::prelude::*;
use super::{DOMAIN, SurfaceGate, assert_extracted_mesh_is_valid, convex_body, sphere_union};
use crate::cube::{corner_offset, edge_crossing};
use crate::dual_contouring::DualContouring;
use crate::manifold_dual_contouring::ManifoldDualContouring;
use crate::marching_cubes::table::{
CASES, EDGE_AXIS, EDGE_CORNERS, McCase, corner_inside, is_inside,
};
use crate::marching_cubes::{FaceAmbiguity, MarchingCubes};
use crate::marching_tetrahedra::MarchingTetrahedra;
use crate::surface_nets::SurfaceNets;
use crate::validate::{ValidateConfig, validate_indexed};
use crate::{MeshBuffer, MeshSink, RuntimeShape3, Sdf, Shape3, vec3};
fn extraction_resolution() -> impl Strategy<Value = [u32; 3]> {
(6u32..=18, 6u32..=18, 6u32..=18).prop_map(|(x, y, z)| [x, y, z])
}
fn grid_for(size: [u32; 3]) -> (RuntimeShape3, [f64; 3], f64) {
let shortest = size.iter().copied().min().expect("three axes");
let cell_size = 2.0 * DOMAIN / f64::from(shortest - 1);
(
RuntimeShape3::new(size).expect("generated sizes fit u32"),
[-DOMAIN; 3],
cell_size,
)
}
fn check_mc<S: Sdf<Scalar = f64>>(label: &str, field: &S, size: [u32; 3]) -> usize {
let (shape, origin, cell_size) = grid_for(size);
let mut mc = MarchingCubes::<f64>::new();
let mut out = MeshBuffer::<f64>::new();
mc.extract(field, &shape, origin, cell_size, &mut out)
.expect("extraction");
assert_extracted_mesh_is_valid(
label,
&out.positions,
&out.indices,
cell_size,
SurfaceGate::Closed,
);
out.triangle_count()
}
fn check_mc33<S: Sdf<Scalar = f64>>(label: &str, field: &S, size: [u32; 3]) -> usize {
let (shape, origin, cell_size) = grid_for(size);
let mut mc = MarchingCubes::<f64>::new();
mc.set_face_ambiguity(FaceAmbiguity::AsymptoticDecider);
let mut out = MeshBuffer::<f64>::new();
mc.extract(field, &shape, origin, cell_size, &mut out)
.expect("extraction");
assert_extracted_mesh_is_valid(
label,
&out.positions,
&out.indices,
cell_size,
SurfaceGate::Closed,
);
out.triangle_count()
}
fn check_trilinear<S: Sdf<Scalar = f64>>(label: &str, field: &S, size: [u32; 3]) -> usize {
let (shape, origin, cell_size) = grid_for(size);
let mut mc = MarchingCubes::<f64>::new();
mc.set_face_ambiguity(FaceAmbiguity::AsymptoticDecider);
mc.set_interior_ambiguity(crate::marching_cubes::InteriorAmbiguity::Trilinear);
let mut out = MeshBuffer::<f64>::new();
mc.extract(field, &shape, origin, cell_size, &mut out)
.expect("extraction");
assert_extracted_mesh_is_valid(
label,
&out.positions,
&out.indices,
cell_size,
SurfaceGate::Closed,
);
out.triangle_count()
}
fn check_mt<S: Sdf<Scalar = f64>>(label: &str, field: &S, size: [u32; 3]) -> usize {
let (shape, origin, cell_size) = grid_for(size);
let mut mt = MarchingTetrahedra::<f64>::new();
let mut out = MeshBuffer::<f64>::new();
mt.extract(field, &shape, origin, cell_size, &mut out)
.expect("extraction");
assert_extracted_mesh_is_valid(
label,
&out.positions,
&out.indices,
cell_size,
SurfaceGate::Closed,
);
out.triangle_count()
}
fn check_sn<S: Sdf<Scalar = f64>>(label: &str, field: &S, size: [u32; 3]) -> usize {
let (shape, origin, cell_size) = grid_for(size);
let mut sn = SurfaceNets::<f64>::new();
let mut out = MeshBuffer::<f64>::new();
sn.extract(field, &shape, origin, cell_size, &mut out)
.expect("extraction");
assert_extracted_mesh_is_valid(
label,
&out.positions,
&out.indices,
cell_size,
SurfaceGate::ClosedAllowingUnresolvedTopology,
);
out.triangle_count()
}
fn check_dc<S: Sdf<Scalar = f64>>(label: &str, field: &S, size: [u32; 3]) -> usize {
let (shape, origin, cell_size) = grid_for(size);
let mut dc = DualContouring::<f64>::new();
let mut out = MeshBuffer::<f64>::new();
dc.extract(field, &shape, origin, cell_size, &mut out)
.expect("extraction");
assert_extracted_mesh_is_valid(
label,
&out.positions,
&out.indices,
cell_size,
SurfaceGate::ClosedAllowingUnresolvedTopology,
);
out.triangle_count()
}
fn check_mdc<S: Sdf<Scalar = f64>>(label: &str, field: &S, size: [u32; 3]) -> usize {
let (shape, origin, cell_size) = grid_for(size);
let mut mdc = ManifoldDualContouring::<f64>::new();
let mut out = MeshBuffer::<f64>::new();
mdc.extract(field, &shape, origin, cell_size, &mut out)
.expect("extraction");
assert_extracted_mesh_is_valid(
label,
&out.positions,
&out.indices,
cell_size,
SurfaceGate::ClosedAllowingUnresolvedTopology,
);
out.triangle_count()
}
fn largest_radius(field: &super::SphereUnion) -> f64 {
field
.spheres
.iter()
.map(|s| s.radius)
.fold(0.0f64, f64::max)
}
fn spacing_for(size: [u32; 3]) -> f64 {
2.0 * DOMAIN / f64::from(size.iter().copied().min().expect("three axes") - 1)
}
proptest! {
#![proptest_config(ProptestConfig { cases: 256, ..ProptestConfig::default() })]
#[test]
fn marching_cubes_meshes_sphere_unions(field in sphere_union(), size in extraction_resolution()) {
let tris = check_mc("mc / sphere union", &field, size);
if largest_radius(&field) >= 2.0 * spacing_for(size) {
prop_assert!(tris > 0, "a resolvable sphere was missed entirely");
}
}
#[test]
fn marching_cubes_meshes_convex_bodies(field in convex_body(), size in extraction_resolution()) {
check_mc("mc / convex body", &field, size);
}
#[test]
fn the_decider_meshes_sphere_unions(field in sphere_union(), size in extraction_resolution()) {
let tris = check_mc33("mc33 / sphere union", &field, size);
if largest_radius(&field) >= 2.0 * spacing_for(size) {
prop_assert!(tris > 0, "a resolvable sphere was missed entirely");
}
}
#[test]
fn the_decider_meshes_convex_bodies(field in convex_body(), size in extraction_resolution()) {
check_mc33("mc33 / convex body", &field, size);
}
#[test]
fn the_interior_rule_meshes_sphere_unions(field in sphere_union(), size in extraction_resolution()) {
let tris = check_trilinear("trilinear / sphere union", &field, size);
if largest_radius(&field) >= 2.0 * spacing_for(size) {
prop_assert!(tris > 0, "a resolvable sphere was missed entirely");
}
}
#[test]
fn the_interior_rule_meshes_convex_bodies(field in convex_body(), size in extraction_resolution()) {
check_trilinear("trilinear / convex body", &field, size);
}
#[test]
fn marching_tetrahedra_meshes_sphere_unions(field in sphere_union(), size in extraction_resolution()) {
let tris = check_mt("mt / sphere union", &field, size);
if largest_radius(&field) >= 2.0 * spacing_for(size) {
prop_assert!(tris > 0, "a resolvable sphere was missed entirely");
}
}
#[test]
fn marching_tetrahedra_meshes_convex_bodies(field in convex_body(), size in extraction_resolution()) {
check_mt("mt / convex body", &field, size);
}
#[test]
fn surface_nets_meshes_sphere_unions(field in sphere_union(), size in extraction_resolution()) {
let tris = check_sn("sn / sphere union", &field, size);
if largest_radius(&field) >= 2.0 * spacing_for(size) {
prop_assert!(tris > 0, "a resolvable sphere was missed entirely");
}
}
#[test]
fn surface_nets_meshes_convex_bodies(field in convex_body(), size in extraction_resolution()) {
check_sn("sn / convex body", &field, size);
}
#[test]
fn dual_contouring_meshes_sphere_unions(field in sphere_union(), size in extraction_resolution()) {
let tris = check_dc("dc / sphere union", &field, size);
if largest_radius(&field) >= 2.0 * spacing_for(size) {
prop_assert!(tris > 0, "a resolvable sphere was missed entirely");
}
}
#[test]
fn dual_contouring_meshes_convex_bodies(field in convex_body(), size in extraction_resolution()) {
check_dc("dc / convex body", &field, size);
}
#[test]
fn manifold_dual_contouring_meshes_sphere_unions(field in sphere_union(), size in extraction_resolution()) {
let tris = check_mdc("mdc / sphere union", &field, size);
if largest_radius(&field) >= 2.0 * spacing_for(size) {
prop_assert!(tris > 0, "a resolvable sphere was missed entirely");
}
}
#[test]
fn manifold_dual_contouring_meshes_convex_bodies(field in convex_body(), size in extraction_resolution()) {
check_mdc("mdc / convex body", &field, size);
}
}
pub(crate) fn march_with_table<S: Sdf<Scalar = f64>>(
sdf: &S,
shape: &RuntimeShape3,
origin: [f64; 3],
cell_size: f64,
cases: &[McCase; 256],
) -> MeshBuffer<f64> {
let size = shape.size();
let mut values = vec![0.0f64; shape.element_count()];
for z in 0..size[2] {
for y in 0..size[1] {
for x in 0..size[0] {
let i = shape.linearize([x, y, z]) as usize;
values[i] = sdf.sample(corner_world([x, y, z], [0, 0, 0], origin, cell_size));
}
}
}
let mut out = MeshBuffer::<f64>::new();
let mut edge_vertices = vec![u32::MAX; shape.element_count() * 3];
for z in 0..size[2] - 1 {
for y in 0..size[1] - 1 {
for x in 0..size[0] - 1 {
let base = [x, y, z];
let mut case = 0u8;
let mut corner_value = [0.0f64; 8];
for c in 0..8u8 {
let s = corner_linear(shape, base, c);
let v = values[s as usize];
corner_value[c as usize] = v;
if is_inside(v) {
case |= 1 << c;
}
}
let entry = &cases[case as usize];
if entry.count == 0 {
continue;
}
for tri in &entry.triangles[..entry.count as usize] {
let mut idx = [0u32; 3];
for (k, &edge) in tri.iter().enumerate() {
let [lo_corner, hi_corner] = EDGE_CORNERS[edge as usize];
let axis = EDGE_AXIS[edge as usize] as usize;
let lo_sample = corner_linear(shape, base, lo_corner);
let key = lo_sample as usize * 3 + axis;
if edge_vertices[key] != u32::MAX {
idx[k] = edge_vertices[key];
continue;
}
let a = corner_value[lo_corner as usize];
let b = corner_value[hi_corner as usize];
let t = edge_crossing(a, b);
let lo_pos =
corner_world(base, corner_offset(lo_corner), origin, cell_size);
let hi_pos =
corner_world(base, corner_offset(hi_corner), origin, cell_size);
let position = [
lo_pos[0] + (hi_pos[0] - lo_pos[0]) * t,
lo_pos[1] + (hi_pos[1] - lo_pos[1]) * t,
lo_pos[2] + (hi_pos[2] - lo_pos[2]) * t,
];
let g = sdf.gradient(position);
let len = vec3::length(g);
let normal = vec3::scale(g, len.recip());
let index = out.vertex(position, normal);
edge_vertices[key] = index;
idx[k] = index;
}
out.triangle(idx[0], idx[1], idx[2]);
}
}
}
}
out
}
fn corner_world(base: [u32; 3], offset: [u32; 3], origin: [f64; 3], cell_size: f64) -> [f64; 3] {
[
origin[0] + cell_size * f64::from(base[0] + offset[0]),
origin[1] + cell_size * f64::from(base[1] + offset[1]),
origin[2] + cell_size * f64::from(base[2] + offset[2]),
]
}
fn corner_linear(shape: &RuntimeShape3, base: [u32; 3], corner: u8) -> u32 {
let o = corner_offset(corner);
shape.linearize([base[0] + o[0], base[1] + o[1], base[2] + o[2]])
}
#[test]
fn the_double_reproduces_marching_cubes() {
let field = crate::fields::Sphere::<f64>::canonical();
for size in [[9, 9, 9], [12, 7, 15], [6, 18, 11]] {
let (shape, origin, cell_size) = grid_for(size);
let mut mc = MarchingCubes::<f64>::new();
let mut real = MeshBuffer::<f64>::new();
mc.extract(&field, &shape, origin, cell_size, &mut real)
.expect("extraction");
let double = march_with_table(&field, &shape, origin, cell_size, &CASES);
assert_eq!(double.indices, real.indices, "{size:?}: indices differ");
assert_eq!(
double.positions.len(),
real.positions.len(),
"{size:?}: vertex count differs"
);
for (i, (d, r)) in double.positions.iter().zip(&real.positions).enumerate() {
for a in 0..3 {
assert_eq!(
d[a].to_bits(),
r[a].to_bits(),
"{size:?}: vertex {i} axis {a}"
);
}
}
for (i, (d, r)) in double.normals.iter().zip(&real.normals).enumerate() {
for a in 0..3 {
assert_eq!(
d[a].to_bits(),
r[a].to_bits(),
"{size:?}: normal {i} axis {a}"
);
}
}
assert!(
!real.indices.is_empty(),
"{size:?}: fixture produced nothing"
);
}
}
fn mutation_fixture() -> (crate::fields::Sphere<f64>, RuntimeShape3, [f64; 3], f64) {
let (shape, origin, cell_size) = grid_for([15, 15, 15]);
(crate::fields::Sphere::canonical(), shape, origin, cell_size)
}
fn cases_used(
shape: &RuntimeShape3,
field: &crate::fields::Sphere<f64>,
origin: [f64; 3],
h: f64,
) -> [u32; 256] {
let size = shape.size();
let mut counts = [0u32; 256];
for z in 0..size[2] - 1 {
for y in 0..size[1] - 1 {
for x in 0..size[0] - 1 {
let mut case = 0u8;
for c in 0..8u8 {
let o = corner_offset(c);
let p = corner_world([x, y, z], o, origin, h);
if is_inside(field.sample(p)) {
case |= 1 << c;
}
}
counts[case as usize] += 1;
}
}
}
counts
}
#[test]
#[should_panic(expected = "expected a closed surface")]
fn a_flipped_triangle_in_the_case_table_is_caught() {
let (field, shape, origin, cell_size) = mutation_fixture();
let used = cases_used(&shape, &field, origin, cell_size);
let mut cases = CASES;
let victim = (1..255u16)
.map(|c| c as usize)
.find(|&c| used[c] > 0 && cases[c].count > 0)
.expect("the fixture must exercise some non-empty case");
cases[victim].triangles[0].swap(1, 2);
let mesh = march_with_table(&field, &shape, origin, cell_size, &cases);
assert_extracted_mesh_is_valid(
"flipped triangle",
&mesh.positions,
&mesh.indices,
cell_size,
SurfaceGate::Closed,
);
}
#[test]
#[should_panic(expected = "expected a closed surface")]
fn a_missing_triangle_in_the_case_table_is_caught() {
let (field, shape, origin, cell_size) = mutation_fixture();
let used = cases_used(&shape, &field, origin, cell_size);
let mut cases = CASES;
let victim = (1..255u16)
.map(|c| c as usize)
.find(|&c| used[c] > 0 && cases[c].count > 0)
.expect("the fixture must exercise some non-empty case");
cases[victim].count -= 1;
let mesh = march_with_table(&field, &shape, origin, cell_size, &cases);
assert_extracted_mesh_is_valid(
"missing triangle",
&mesh.positions,
&mesh.indices,
cell_size,
SurfaceGate::Closed,
);
}
#[test]
#[should_panic(expected = "expected a closed surface")]
fn a_wrong_edge_reference_in_the_case_table_is_caught() {
let (field, shape, origin, cell_size) = mutation_fixture();
let used = cases_used(&shape, &field, origin, cell_size);
let mut cases = CASES;
let (victim, replacement) = (1..255u16)
.map(|c| c as u8)
.filter(|&c| used[c as usize] > 0 && cases[c as usize].count > 0)
.find_map(|c| {
let tri = cases[c as usize].triangles[0];
(0..12u8)
.find(|&e| {
let [lo, hi] = EDGE_CORNERS[e as usize];
corner_inside(c, lo) != corner_inside(c, hi) && !tri.contains(&e)
})
.map(|e| (c as usize, e))
})
.expect("some exercised case cuts an edge its first triangle does not name");
cases[victim].triangles[0][0] = replacement;
let mesh = march_with_table(&field, &shape, origin, cell_size, &cases);
assert_extracted_mesh_is_valid(
"wrong edge reference",
&mesh.positions,
&mesh.indices,
cell_size,
SurfaceGate::Closed,
);
}
#[test]
fn the_uncorrupted_table_passes_the_same_bundle() {
let (field, shape, origin, cell_size) = mutation_fixture();
let mesh = march_with_table(&field, &shape, origin, cell_size, &CASES);
let report = validate_indexed(
&mesh.positions,
&mesh.indices,
&ValidateConfig::from_cell_size(cell_size).expect("valid cell size"),
);
assert!(report.is_closed(), "the control must pass\n{report}");
assert_extracted_mesh_is_valid(
"uncorrupted control",
&mesh.positions,
&mesh.indices,
cell_size,
SurfaceGate::Closed,
);
}
#[test]
fn the_fixture_that_falsified_unconditional_manifoldness_is_now_manifold() {
use crate::fields::Sphere;
let field = super::SphereUnion {
spheres: alloc::vec![
Sphere {
center: [
0.216_424_612_766_318_28,
0.529_307_710_262_215_2,
-0.804_663_039_989_917_6
],
radius: 0.619_553_810_790_568_1,
},
Sphere {
center: [0.514_601_202_644_422_7, 0.230_953_855_883_975_85, 0.0],
radius: 0.495_969_042_463_108_13,
},
Sphere {
center: [0.449_324_060_565_480_9, -0.870_601_428_657_975_9, 0.0],
radius: 0.875_530_864_840_149_2,
},
],
};
let report_at = |n: u32| {
let (shape, origin, h) = grid_for([n; 3]);
let mut mc = MarchingCubes::<f64>::new();
let mut out = MeshBuffer::<f64>::new();
mc.extract(&field, &shape, origin, h, &mut out)
.expect("extraction");
validate_indexed(
&out.positions,
&out.indices,
&ValidateConfig::from_cell_size(h).expect("valid cell size"),
)
};
let coarse = report_at(7);
assert_eq!(coarse.non_manifold_edges, 0, "{coarse}");
assert_eq!(coarse.non_manifold_vertices, 0, "{coarse}");
assert_eq!(coarse.boundary_edges, 0, "{coarse}");
assert_eq!(coarse.inconsistently_oriented_edges, 0, "{coarse}");
assert_eq!(coarse.euler_characteristic, 0, "{coarse}");
assert_eq!(coarse.genus, Some(1), "{coarse}");
assert!(coarse.is_closed(), "{coarse}");
for n in [9u32, 13, 17, 25, 33, 49, 65] {
let r = report_at(n);
assert_eq!(r.non_manifold_edges, 0, "n={n}\n{r}");
assert_eq!(r.non_manifold_vertices, 0, "n={n}\n{r}");
assert!(r.is_closed(), "n={n}\n{r}");
}
}