use alloc::vec::Vec;
use super::{Brush, BrushOp, BrushStack, Capsule, smooth_min};
use crate::fields::{BoxExact, Sphere};
use crate::{Real, Sdf};
#[derive(Clone, Copy, Debug)]
enum Shape {
Sphere(Sphere<f64>),
Cube(BoxExact<f64>),
Capsule(Capsule<f64>),
}
impl Sdf for Shape {
type Scalar = f64;
fn sample(&self, p: [f64; 3]) -> f64 {
match self {
Self::Sphere(s) => s.sample(p),
Self::Cube(b) => b.sample(p),
Self::Capsule(c) => c.sample(p),
}
}
}
fn eight(op_of: impl Fn(usize) -> BrushOp) -> Vec<Brush<Shape>> {
let centres = [
[0.30, 0.10, -0.20],
[-0.25, 0.35, 0.15],
[0.05, -0.30, 0.25],
[-0.15, -0.10, -0.35],
[0.40, 0.25, 0.05],
[-0.35, 0.05, 0.30],
[0.20, -0.40, -0.10],
[-0.05, 0.20, -0.30],
];
centres
.iter()
.enumerate()
.map(|(i, c)| {
let shape = match i % 3 {
0 => Shape::Sphere(Sphere {
center: *c,
radius: 0.30 + 0.02 * i as f64,
}),
1 => Shape::Cube(BoxExact {
center: *c,
half_extents: [0.22 + 0.01 * i as f64; 3],
}),
_ => Shape::Capsule(Capsule {
a: *c,
b: [c[0] + 0.25, c[1] - 0.15, c[2] + 0.1],
radius: 0.16,
}),
};
Brush {
shape,
op: op_of(i),
}
})
.collect()
}
fn probes() -> Vec<[f64; 3]> {
let mut out = Vec::new();
for z in 0..4 {
for y in 0..4 {
for x in 0..4 {
out.push([
-0.6 + 0.4 * f64::from(x),
-0.6 + 0.4 * f64::from(y),
-0.6 + 0.4 * f64::from(z),
]);
}
}
}
out
}
fn signature(brushes: &[Brush<Shape>], probes: &[[f64; 3]]) -> Vec<u64> {
let stack = BrushStack {
base: BoxExact::<f64>::canonical(),
brushes,
};
probes.iter().map(|p| stack.sample(*p).to_bits()).collect()
}
fn permutations(n: usize) -> Vec<Vec<usize>> {
let mut out = Vec::new();
let mut items: Vec<usize> = (0..n).collect();
let mut counters = alloc::vec![0usize; n];
out.push(items.clone());
let mut i = 0;
while i < n {
if counters[i] < i {
if i % 2 == 0 {
items.swap(0, i);
} else {
items.swap(counters[i], i);
}
out.push(items.clone());
counters[i] += 1;
i = 0;
} else {
counters[i] = 0;
i += 1;
}
}
out
}
fn distinct_results(brushes: &[Brush<Shape>]) -> usize {
let probes = probes();
let orders = permutations(brushes.len());
let mut seen: Vec<Vec<u64>> = Vec::new();
for order in &orders {
let permuted: Vec<Brush<Shape>> = order.iter().map(|&i| brushes[i]).collect();
let sig = signature(&permuted, &probes);
if let Err(at) = seen.binary_search(&sig) {
seen.insert(at, sig);
}
}
seen.len()
}
#[test]
fn there_really_are_40320_orderings() {
let orders = permutations(8);
assert_eq!(orders.len(), 40_320);
let mut sorted = orders.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), 40_320, "permutations must be distinct");
}
#[test]
fn eight_additions_commute_exactly_over_all_orderings() {
let brushes = eight(|_| BrushOp::Add);
let distinct = distinct_results(&brushes);
std::println!(
"measured: G-003 eight Add brushes over 40,320 orderings -> {distinct} distinct result(s)"
);
assert_eq!(
distinct, 1,
"additions must commute, or concurrent editing dies here"
);
}
#[test]
fn eight_subtractions_commute_exactly_over_all_orderings() {
let brushes = eight(|_| BrushOp::Subtract);
let distinct = distinct_results(&brushes);
std::println!(
"measured: G-003 eight Subtract brushes over 40,320 orderings -> {distinct} distinct result(s)"
);
assert_eq!(distinct, 1);
}
#[test]
fn mixing_adds_and_subtracts_does_not_commute() {
let brushes = eight(|i| {
if i % 2 == 0 {
BrushOp::Add
} else {
BrushOp::Subtract
}
});
let distinct = distinct_results(&brushes);
std::println!(
"measured: G-003 four Add + four Subtract over 40,320 orderings -> {distinct} distinct results"
);
assert!(
distinct > 1,
"if these commuted the fixture would not be overlapping enough to test anything"
);
assert!(
!BrushOp::Add.commutes_with(BrushOp::Subtract),
"the API must not claim they commute"
);
}
#[test]
fn smooth_union_does_not_commute() {
let brushes = eight(|_| BrushOp::SmoothAdd { k: 0.15 });
let distinct = distinct_results(&brushes);
std::println!(
"measured: G-003 eight SmoothAdd(k=0.15) over 40,320 orderings -> {distinct} distinct results"
);
assert!(
distinct > 1,
"smooth-min is not associative; this should differ"
);
assert!(!BrushOp::SmoothAdd { k: 0.15 }.commutes_with(BrushOp::SmoothAdd { k: 0.15 }));
}
#[test]
fn smooth_min_fails_reordering_in_two_separate_ways() {
let cases: [(f64, f64, f64); 4] = [
(0.3, -0.2, 0.25),
(1.0, 1.0, 0.5),
(-0.75, 0.4, 0.1),
(0.05, 0.06, 0.5),
];
let mut worst_ulp = 0i64;
for (a, b, k) in cases {
let forward = smooth_min(a, b, k);
let reverse = smooth_min(b, a, k);
assert!(
(forward - reverse).abs() <= 4.0 * f64::EPSILON * forward.abs().max(1.0),
"smooth_min({a}, {b}, {k}) differs by more than rounding: {forward} vs {reverse}"
);
let gap = (forward.to_bits() as i64 - reverse.to_bits() as i64).abs();
worst_ulp = worst_ulp.max(gap);
}
std::println!(
"measured: G-003 smooth-min commutativity -- worst disagreement {worst_ulp} ulp when the arguments are swapped"
);
assert!(
worst_ulp > 0,
"if swapping is bit-exact, the module docs should say so plainly"
);
let (a, b, c, k): (f64, f64, f64, f64) = (-0.15, 0.2, 0.2, 0.5);
let left = smooth_min(smooth_min(a, b, k), c, k);
let right = smooth_min(a, smooth_min(b, c, k), k);
assert_ne!(
left.to_bits(),
right.to_bits(),
"smooth-min is not associative; if this passes the claim above is wrong"
);
let gap = (left - right).abs();
std::println!(
"measured: G-003 smooth-min associativity gap -> {left} vs {right}, {gap:.3e} apart"
);
assert!(
gap > 1e-3,
"the fixture has drifted into the saturated region where smooth-min is just min"
);
}
#[test]
fn a_zero_join_width_is_an_ordinary_min() {
for (a, b) in [(0.3f64, -0.2f64), (1.0, 1.0), (-0.75, 0.4)] {
assert_eq!(smooth_min(a, b, 0.0).to_bits(), a.min(b).to_bits());
assert_eq!(smooth_min(a, b, -1.0).to_bits(), a.min(b).to_bits());
}
}
#[test]
fn the_capsule_is_an_exact_distance_field() {
let c = Capsule {
a: [-0.4, 0.0, 0.0],
b: [0.4, 0.0, 0.0],
radius: 0.25,
};
assert!((c.sample([0.0, 0.0, 0.0]) + 0.25).abs() < 1e-15);
assert!((c.sample([0.0, 0.25, 0.0])).abs() < 1e-15);
assert!((c.sample([0.9, 0.0, 0.0]) - 0.25).abs() < 1e-15);
let eps = 1e-6;
for p in [[0.0, 0.5, 0.1], [0.7, 0.2, -0.3], [-0.6, -0.4, 0.2]] {
let mut grad = [0.0f64; 3];
for axis in 0..3 {
let mut lo = p;
let mut hi = p;
lo[axis] -= eps;
hi[axis] += eps;
grad[axis] = (c.sample(hi) - c.sample(lo)) / (2.0 * eps);
}
let len = (grad[0] * grad[0] + grad[1] * grad[1] + grad[2] * grad[2]).sqrt();
assert!((len - 1.0).abs() < 1e-6, "|grad| = {len} at {p:?}");
}
}
#[test]
fn a_degenerate_capsule_is_a_sphere() {
let c = Capsule {
a: [0.1, 0.2, 0.3],
b: [0.1, 0.2, 0.3],
radius: 0.4,
};
let s = Sphere {
center: [0.1, 0.2, 0.3],
radius: 0.4,
};
for p in [[0.0; 3], [1.0, 0.0, 0.0], [-0.5, 0.5, 0.25]] {
assert!((c.sample(p) - s.sample(p)).abs() < 1e-15, "at {p:?}");
}
}
#[test]
fn a_stack_applies_its_brushes() {
let inside = [0.0, 0.0, 0.0];
let base = BoxExact::<f64>::canonical();
assert!(
base.sample(inside) < 0.0,
"the origin starts inside the box"
);
let carve = [Brush::subtract(Shape::Sphere(Sphere {
center: [0.0; 3],
radius: 0.5,
}))];
let stack = BrushStack {
base,
brushes: &carve,
};
assert!(
stack.sample(inside) > 0.0,
"the carve should have removed it"
);
let refill = [
carve[0],
Brush::add(Shape::Sphere(Sphere {
center: [0.0; 3],
radius: 0.5,
})),
];
let stack = BrushStack {
base,
brushes: &refill,
};
assert!(
stack.sample(inside) < 0.0,
"adding it back should restore it"
);
}