#![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.is_exact_distance() {
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.is_exact_distance());
}
#[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, 7);
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");
}
#[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);
}
#[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]]);
}