#![allow(clippy::float_cmp)]
use super::*;
const SAMPLES: [[f64; 3]; 12] = [
[0.137, 0.523, -0.311],
[-0.412, 0.229, 0.671],
[0.733, -0.158, 0.394],
[-0.259, -0.617, 0.142],
[0.481, 0.352, -0.706],
[-0.694, 0.443, -0.217],
[0.312, -0.771, 0.556],
[-0.148, 0.685, 0.423],
[0.607, -0.294, -0.469],
[-0.535, -0.371, -0.628],
[0.226, 0.114, 0.759],
[-0.783, 0.596, 0.183],
];
const RADII: [f64; 2] = [0.4, 0.9];
fn scaled_samples<S: ReferenceField<Scalar = f64>>(field: &S) -> impl Iterator<Item = [f64; 3]> {
let (_, hi) = field.domain();
let half = hi[0];
RADII.into_iter().flat_map(move |r| {
SAMPLES
.into_iter()
.map(move |p| [p[0] * half * r, p[1] * half * r, p[2] * half * r])
})
}
#[test]
fn analytic_gradients_match_central_differences() {
fn check<S: ReferenceField<Scalar = f64>>(name: &str, field: &S) {
let h = 1e-5f64;
for p in scaled_samples(field) {
let analytic = field.gradient(p);
for axis in 0..3 {
let mut lo = p;
let mut hi = p;
lo[axis] -= h;
hi[axis] += h;
let numeric = (field.sample(hi) - field.sample(lo)) / (2.0 * h);
assert!(
(analytic[axis] - numeric).abs() < 1e-6,
"{name} at {p:?} axis {axis}: analytic {} vs numeric {numeric}",
analytic[axis],
);
}
}
}
for_each_reference_field!(f64, |name, field| {
check(name, &field);
});
}
#[test]
fn exact_distance_fields_have_unit_gradients() {
fn check<S: ReferenceField<Scalar = f64>>(name: &str, field: &S) {
if !field.bound().is_exact() {
return;
}
for p in scaled_samples(field) {
let g = field.gradient(p);
let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
assert!(
(len - 1.0).abs() < 1e-12,
"{name} at {p:?}: |grad| = {len}, grad = {g:?}"
);
}
}
for_each_reference_field!(f64, |name, field| {
check(name, &field);
});
}
#[test]
fn closed_fields_have_constant_sign_on_their_domain_boundary() {
fn check<S: ReferenceField<Scalar = f64>>(name: &str, field: &S) {
if !field.closed_in_domain() {
return;
}
let (lo, hi) = field.domain();
let steps = 8;
for axis in 0..3 {
for &wall in &[lo[axis], hi[axis]] {
for i in 0..=steps {
for j in 0..=steps {
let a = axis;
let b = (axis + 1) % 3;
let c = (axis + 2) % 3;
let mut p = [0.0f64; 3];
p[a] = wall;
p[b] = lo[b] + (hi[b] - lo[b]) * f64::from(i) / f64::from(steps);
p[c] = lo[c] + (hi[c] - lo[c]) * f64::from(j) / f64::from(steps);
let v = field.sample(p);
assert!(v > 0.0, "{name}: sample{p:?} = {v}, not outside the solid");
}
}
}
}
}
for_each_reference_field!(f64, |name, field| {
check(name, &field);
});
}
#[test]
fn every_field_instantiates_in_f32() {
for_each_reference_field!(f32, |name, field| {
let (lo, hi) = field.domain();
assert!(hi[0] > lo[0], "{name}");
let v = field.sample([0.1f32, 0.2, 0.3]);
assert!(v.is_finite(), "{name}: {v}");
let g = field.gradient([0.1f32, 0.2, 0.3]);
assert!(g.iter().all(|c| c.is_finite()), "{name}: {g:?}");
});
}
#[test]
fn negative_is_inside() {
for_each_reference_field!(f64, |name, field| {
let _ = name;
let _ = &field;
});
assert!(Sphere::<f64>::canonical().sample([0.0, 0.0, 0.0]) < 0.0);
assert!(BoxExact::<f64>::canonical().sample([0.0, 0.0, 0.0]) < 0.0);
}
#[test]
fn sphere_signs() {
let f = Sphere::<f64>::canonical();
assert_eq!(f.sample([0.0, 0.0, 0.0]), -1.0); assert_eq!(f.sample([2.0, 0.0, 0.0]), 1.0); assert_eq!(f.sample([1.0, 0.0, 0.0]), 0.0); }
#[test]
fn torus_signs() {
let f = Torus::<f64>::canonical();
assert_eq!(f.sample([1.0, 0.0, 0.0]), -0.3);
assert_eq!(f.sample([0.0, 0.0, 1.0]), -0.3); assert_eq!(f.sample([0.0, 0.0, 0.0]), 0.7); assert!(f.sample([1.3, 0.0, 0.0]).abs() < 8.0 * f64::EPSILON);
assert!(f.sample([0.0, 1.0, 0.0]) > 0.0);
}
#[test]
fn box_signs() {
let f = BoxExact::<f64>::canonical();
assert_eq!(f.sample([0.0, 0.0, 0.0]), -1.0);
assert_eq!(f.sample([2.0, 0.0, 0.0]), 1.0); assert_eq!(f.sample([1.0, 0.0, 0.0]), 0.0); assert_eq!(f.sample([2.0, 2.0, 2.0]), 3.0f64.sqrt()); }
#[test]
fn thin_plate_signs() {
let f = ThinPlate::<f64>::canonical();
let h = ThinPlate::<f64>::CANONICAL_CELL_SIZE;
let half_thickness = h * ThinPlate::<f64>::THICKNESS_IN_CELLS * 0.5;
assert_eq!(f.sample([0.0, 0.0, 0.0]), -half_thickness);
assert_eq!(f.sample([0.0, half_thickness, 0.0]), 0.0);
assert_eq!(f.sample([0.0, h, 0.0]), h - half_thickness);
assert!(half_thickness * 2.0 < h, "the plate must be sub-voxel");
}
#[test]
fn csg_difference_signs() {
let f = csg_difference::<f64>();
assert!(f.sample([0.9, -0.9, 0.0]) < 0.0);
assert!(f.sample([0.9, 0.9, 0.9]) > 0.0);
assert_eq!(f.sample([2.0, 0.0, 0.0]), 1.0);
assert!(f.sample([1.0, 1.0, 1.0]) > 0.0);
}
#[test]
fn gyroid_signs() {
let f = capped_gyroid::<f64>();
assert_eq!(f.sample([0.0, 0.0, 0.0]), 0.0);
let inside = f.sample([-core::f64::consts::FRAC_PI_2, 0.0, 0.0]);
assert!((inside + 1.0).abs() < 1e-12, "{inside}");
let outside = f.sample([core::f64::consts::FRAC_PI_2, 0.0, 0.0]);
assert!((outside - 1.0).abs() < 1e-12, "{outside}");
assert!(f.sample([6.5, 0.0, 0.0]) > 0.0);
assert!(!f.bound().is_exact());
}
#[test]
fn fbm_terrain_signs() {
let f = FbmTerrain::<f64>::canonical();
let bound = f.height_bound();
assert!(f.sample([0.0, -(bound + 1.0), 0.0]) < 0.0);
assert!(f.sample([0.0, bound + 1.0, 0.0]) > 0.0);
assert_eq!(bound, 7.5);
assert!(!f.closed_in_domain());
assert!(f.expected_euler().is_none());
}
#[test]
fn reference_field_names_match_the_sweep() {
let mut count = 0;
for_each_reference_field!(f64, |name, field| {
let _ = &field;
count += 1;
assert!(!name.is_empty());
});
assert_eq!(count, 14);
assert_eq!(Sphere::<f64>::NAME, "sphere");
assert_eq!(Torus::<f64>::NAME, "torus");
assert_eq!(BoxExact::<f64>::NAME, "box_exact");
assert_eq!(CsgDifference::<f64>::NAME, "csg_difference");
assert_eq!(ThinPlate::<f64>::NAME, "thin_plate");
assert_eq!(CappedGyroid::<f64>::NAME, "gyroid");
assert_eq!(FbmTerrain::<f64>::NAME, "fbm_terrain");
assert_eq!(<NoiseCavity<f64> as ReferenceField>::NAME, "noise_cavity");
assert_eq!(DrilledBall::<f64, 1>::NAME, "ball_drilled_g1");
assert_eq!(ThickenedGraph::<f64, 2>::NAME, "graph_theta_g2");
assert_eq!(DrilledBall::<f64, 2>::NAME, "ball_drilled_g2");
assert_eq!(DrilledBall::<f64, 3>::NAME, "ball_drilled_g3");
assert_eq!(ThickenedGraph::<f64, 3>::NAME, "graph_k4_g3");
assert_eq!(ThickenedGraph::<f64, 5>::NAME, "graph_cube_g5");
}
#[test]
fn only_the_analytically_known_euler_characteristics_are_declared() {
assert_eq!(Sphere::<f64>::canonical().expected_euler(), Some(2));
assert_eq!(Torus::<f64>::canonical().expected_euler(), Some(0)); assert_eq!(BoxExact::<f64>::canonical().expected_euler(), Some(2));
assert_eq!(ThinPlate::<f64>::canonical().expected_euler(), Some(2));
assert_eq!(csg_difference::<f64>().expected_euler(), Some(2));
assert_eq!(capped_gyroid::<f64>().expected_euler(), None);
assert_eq!(FbmTerrain::<f64>::canonical().expected_euler(), None);
assert_eq!(ball_drilled_g1::<f64>().expected_euler(), Some(0)); assert_eq!(graph_theta_g2::<f64>().expected_euler(), Some(-2)); assert_eq!(ball_drilled_g2::<f64>().expected_euler(), Some(-2)); assert_eq!(ball_drilled_g3::<f64>().expected_euler(), Some(-4)); assert_eq!(graph_k4_g3::<f64>().expected_euler(), Some(-4)); assert_eq!(graph_cube_g5::<f64>().expected_euler(), Some(-8)); }
fn chi_at<S: ReferenceField<Scalar = f64>>(field: &S, samples: u32) -> (i64, usize) {
use crate::marching_cubes::MarchingCubes;
use crate::validate::{ValidateConfig, validate_indexed};
use crate::{MeshBuffer, RuntimeShape3};
let (lo, hi) = field.domain();
let h = (hi[0] - lo[0]) / f64::from(samples - 1);
let shape = RuntimeShape3::new([samples; 3]).expect("test grids fit u32");
let mut mesh = MeshBuffer::<f64>::new();
MarchingCubes::<f64>::new()
.extract(field, &shape, lo, h, &mut mesh)
.expect("test grids are extractable");
let config = ValidateConfig::from_cell_size(h).expect("positive spacing");
let report = validate_indexed(&mesh.positions, &mesh.indices, &config);
(report.euler_characteristic, mesh.triangle_count())
}
#[test]
fn every_prescribed_field_extracts_its_prescribed_chi() {
fn check<S: ReferenceField<Scalar = f64>>(name: &str, field: &S) {
let Some(chi) = field.expected_euler() else {
panic!("{name} declares no chi, so it cannot be in this test");
};
for samples in [17u32, 25, 33] {
let (measured, triangles) = chi_at(field, samples);
assert!(triangles > 0, "{name} at {samples}^3 meshed nothing");
assert_eq!(
measured, chi,
"{name} at {samples}^3: chi {measured}, prescribed {chi}"
);
}
}
check("sphere", &Sphere::<f64>::canonical());
check("torus", &Torus::<f64>::canonical());
check("ball_drilled_g1", &ball_drilled_g1::<f64>());
check("graph_theta_g2", &graph_theta_g2::<f64>());
check("ball_drilled_g2", &ball_drilled_g2::<f64>());
check("ball_drilled_g3", &ball_drilled_g3::<f64>());
check("graph_k4_g3", &graph_k4_g3::<f64>());
check("graph_cube_g5", &graph_cube_g5::<f64>());
}
#[test]
fn graph_cube_g5_is_perfect_at_7_and_11_and_absent_at_9() {
let field = graph_cube_g5::<f64>();
assert_eq!(chi_at(&field, 7).0, -8, "7^3");
assert_eq!(chi_at(&field, 9).1, 0, "9^3 must mesh nothing");
assert_eq!(chi_at(&field, 11).0, -8, "11^3");
}
#[test]
fn default_is_canonical() {
assert_eq!(Sphere::<f32>::default(), Sphere::canonical());
assert_eq!(Torus::<f32>::default(), Torus::canonical());
assert_eq!(BoxExact::<f32>::default(), BoxExact::canonical());
assert_eq!(ThinPlate::<f32>::default(), ThinPlate::canonical());
assert_eq!(Gyroid::<f32>::default(), Gyroid::canonical());
assert_eq!(FbmTerrain::<f32>::default(), FbmTerrain::canonical());
}
#[test]
fn difference_gradient_follows_the_active_operand() {
let f = csg_difference::<f64>();
let g = f.gradient([-0.9, -0.5, 0.0]);
assert_eq!(g, BoxExact::<f64>::canonical().gradient([-0.9, -0.5, 0.0]));
let p = [0.85, 0.85, 0.85];
let g = f.gradient(p);
let sphere_grad = Sphere::<f64> {
center: [0.6; 3],
radius: 0.75,
}
.gradient(p);
assert_eq!(g, [-sphere_grad[0], -sphere_grad[1], -sphere_grad[2]]);
}
#[test]
fn union_is_the_set_union_and_never_overestimates() {
use super::{Sphere, Union};
let a = Sphere {
center: [-0.4, 0.0, 0.0],
radius: 0.7_f64,
};
let b = Sphere {
center: [0.4, 0.0, 0.0],
radius: 0.7_f64,
};
let u = Union { a, b };
let mut checked = 0usize;
let n = 24;
for i in 0..n {
for j in 0..n {
for k in 0..n {
let p = [
-1.5 + 3.0 * f64::from(i) / f64::from(n - 1),
-1.5 + 3.0 * f64::from(j) / f64::from(n - 1),
-1.5 + 3.0 * f64::from(k) / f64::from(n - 1),
];
let (fa, fb, fu) = (a.sample(p), b.sample(p), u.sample(p));
assert_eq!(
fu <= 0.0,
fa <= 0.0 || fb <= 0.0,
"union disagrees with `inside a or inside b` at {p:?}"
);
assert!(
fu <= fa + 1e-12 && fu <= fb + 1e-12,
"union overestimates distance at {p:?}: {fu} > min({fa}, {fb})"
);
checked += 1;
}
}
}
assert_eq!(checked, 24 * 24 * 24);
}
#[test]
fn the_blend_radius_adds_material_monotonically() {
use super::{SmoothUnion, Sphere, Union};
let (a, b) = (
Sphere {
center: [-0.55, 0.0, 0.0],
radius: 0.5_f64,
},
Sphere {
center: [0.55, 0.0, 0.0],
radius: 0.5_f64,
},
);
let seam = [0.0, 0.0, 0.0];
let hard = Union { a, b }.sample(seam);
let zero = SmoothUnion { a, b, k: 0.0 }.sample(seam);
assert_eq!(zero, hard, "k = 0 must degenerate to a hard union");
let mut previous = hard;
for step in 1..=6 {
let k = 0.1 * f64::from(step);
let value = SmoothUnion { a, b, k }.sample(seam);
assert!(
value < previous,
"k = {k} did not add material at the seam: {value} >= {previous}"
);
previous = value;
}
std::println!("measured: seam value {hard:.4} (hard) -> {previous:.4} at k = 0.6");
let blended = SmoothUnion { a, b, k: 0.3 };
for i in 0..20 {
for j in 0..20 {
let p = [
-1.5 + 3.0 * f64::from(i) / 19.0,
-1.5 + 3.0 * f64::from(j) / 19.0,
0.0,
];
assert!(
blended.sample(p) <= Union { a, b }.sample(p) + 1e-12,
"the blend removed material at {p:?}"
);
}
}
}
#[test]
fn every_field_meets_the_bound_it_declares() {
use super::FieldBound;
const N: u32 = 12;
let offset = 0.031_7_f64;
crate::for_each_reference_field!(f64, |name, field| {
let (lo, hi) = field.domain();
let bound = field.bound();
let mut worst_low = f64::INFINITY;
let mut worst_high: f64 = 0.0;
let mut samples = 0usize;
for i in 0..N {
for j in 0..N {
for k in 0..N {
let t = |v: u32, a: f64, b: f64| {
a + (b - a) * (f64::from(v) + 0.5 + offset) / f64::from(N)
};
let p = [t(i, lo[0], hi[0]), t(j, lo[1], hi[1]), t(k, lo[2], hi[2])];
let g = field.gradient(p);
let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
if !len.is_finite() {
continue;
}
worst_low = worst_low.min(len);
worst_high = worst_high.max(len);
samples += 1;
}
}
}
assert!(samples > 100, "{name}: only {samples} usable samples");
match bound {
FieldBound::Exact => {
assert!(
worst_high < 1.20,
"{name} declares Exact but |∇f| reaches {worst_high:.4}"
);
assert!(
worst_low > 0.30,
"{name} declares Exact but |∇f| falls to {worst_low:.4}"
);
}
FieldBound::Lipschitz { l } => {
assert!(
worst_high <= l * 1.05,
"{name} declares Lipschitz l = {l} but |∇f| reaches {worst_high:.4}"
);
}
FieldBound::Underestimate { q } => {
assert!((0.0..=1.0).contains(&q), "{name}: q = {q} is not in (0, 1]");
assert!(
worst_high < 1.20,
"{name} declares Underestimate but |∇f| reaches {worst_high:.4}"
);
}
FieldBound::Unbounded => {}
}
std::println!("measured: {name:<16} {bound:?} |∇f| in [{worst_low:.3}, {worst_high:.3}]");
});
}
#[test]
fn the_csg_field_does_not_claim_to_be_a_distance() {
use super::FieldBound;
let f = super::csg_difference::<f64>();
assert_ne!(
f.bound(),
FieldBound::Exact,
"csg_difference is max(box, -sphere); max of two exact distances is not \
an exact distance, and near a concave seam it overestimates"
);
assert!(!f.bound().is_exact());
assert_eq!(f.bound().lipschitz(), Some(1.0));
}
#[test]
fn min_and_max_are_both_inexact_and_the_asymmetry_is_by_region() {
use super::{Intersection, Sphere, Union};
let a = Sphere {
center: [-0.35, 0.0, 0.0],
radius: 0.8_f64,
};
let b = Sphere {
center: [0.35, 0.0, 0.0],
radius: 0.8_f64,
};
let union = Union { a, b };
let inter = Intersection { a, b };
let mut union_outside = 0.0f64;
let mut union_inside = 0.0f64;
let mut inter_outside = 0.0f64;
let mut inter_inside = 0.0f64;
let n = 40;
for i in 0..n {
for j in 0..n {
for k in 0..n {
let t = |v: i32| -1.6 + 3.2 * (f64::from(v) + 0.5) / f64::from(n);
let p = [t(i), t(j), t(k)];
let (fa, fb) = (a.sample(p), b.sample(p));
let u = union.sample(p);
if fa > 0.0 && fb > 0.0 {
union_outside = union_outside.max((u - fa.min(fb)).abs());
} else {
let truth = fa.max(fb);
union_inside = union_inside.max((truth - u).abs());
}
let x = inter.sample(p);
if fa < 0.0 && fb < 0.0 {
inter_inside = inter_inside.max((x - fa.max(fb)).abs());
} else {
let truth = fa.min(fb);
inter_outside = inter_outside.max((truth - x).abs());
}
}
}
}
std::println!(
"measured: union outside {union_outside:.3e} inside {union_inside:.3e}\n\
measured: inter inside {inter_inside:.3e} outside {inter_outside:.3e}"
);
assert!(
union_outside < 1e-12,
"min is not exact outside the union: {union_outside:e}"
);
assert!(
inter_inside < 1e-12,
"max is not exact inside the intersection: {inter_inside:e}"
);
assert!(
union_inside > 1e-3,
"min was expected to lose accuracy inside the union"
);
assert!(
inter_outside > 1e-3,
"max was expected to lose accuracy outside the intersection"
);
let ratio = union_inside / inter_outside;
assert!(
(0.5..2.0).contains(&ratio),
"one operator is much worse than the other ({ratio:.3}), which would make \
the ticket's 'strictly weaker' prediction right after all"
);
}
#[test]
fn composing_exact_fields_keeps_the_constant_and_loses_exactness() {
use super::{BoundedSdf, Difference, FieldBound, Intersection, SmoothUnion, Sphere, Union};
let a = Sphere::<f64>::canonical();
let b = Sphere {
center: [0.5; 3],
radius: 0.6_f64,
};
assert_eq!(a.value_bound(), FieldBound::Exact);
for (name, bound) in [
("union", Union { a, b }.value_bound()),
("intersection", Intersection { a, b }.value_bound()),
("difference", Difference { a, b }.value_bound()),
("smooth_union", SmoothUnion { a, b, k: 0.2 }.value_bound()),
] {
assert_eq!(
bound,
FieldBound::Lipschitz { l: 1.0 },
"{name} should keep the constant and lose exactness"
);
assert!(!bound.is_exact(), "{name} must not claim to be a distance");
assert_eq!(bound.lipschitz(), Some(1.0), "{name} lost its step size");
}
let noisy = super::Gyroid::<f64>::canonical();
assert!(matches!(
Union { a, b: noisy }.value_bound(),
FieldBound::Lipschitz { .. }
));
assert_eq!(
Union {
a,
b: super::FbmTerrain::<f64>::canonical()
}
.value_bound(),
FieldBound::Unbounded
);
}