#![allow(clippy::float_cmp)]
use alloc::vec::Vec;
use super::{Edit, PaintStack, Splat, ramp, shade};
use crate::Sdf;
use crate::brush::{Brush, BrushStack};
use crate::fields::{BoxExact, Sphere};
type Log = Edit<Sphere<f64>, Sphere<f64>, f64>;
type Log32 = Edit<Sphere<f32>, Sphere<f32>, f32>;
fn wall() -> BoxExact<f64> {
BoxExact {
center: [0.0; 3],
half_extents: [2.0, 2.0, 0.25],
}
}
const RED: [f64; 4] = [1.0, 0.0, 0.0, 1.0];
const GREY: [f64; 4] = [0.5, 0.5, 0.5, 1.0];
fn spray() -> Splat<Sphere<f64>, f64> {
Splat {
shape: Sphere {
center: [0.0, 0.0, -0.25],
radius: 0.75,
},
color: RED,
softness: 0.1,
depth: 0.05,
}
}
fn hole() -> Brush<Sphere<f64>> {
Brush::subtract(Sphere {
center: [1.2, 0.0, 0.0],
radius: 0.5,
})
}
const SURVIVING: [[f64; 3]; 5] = [
[0.00, 0.00, -0.25],
[0.20, 0.10, -0.25],
[-0.30, 0.25, -0.25],
[0.15, -0.40, -0.25],
[-0.45, -0.20, -0.25],
];
#[test]
fn paint_on_the_surviving_wall_does_not_move_when_a_hole_is_carved() {
let before_log: [Log; 1] = [Edit::Spray(spray())];
let after_log = [Edit::Spray(spray()), Edit::Carve(hole())];
let before = PaintStack {
base: wall(),
edits: &before_log,
background: GREY,
};
let after = PaintStack {
base: wall(),
edits: &after_log,
background: GREY,
};
for p in SURVIVING {
let b = before.color_at(p);
let a = after.color_at(p);
assert_eq!(
b.map(f64::to_bits),
a.map(f64::to_bits),
"colour moved at {p:?}: {b:?} -> {a:?}"
);
assert!(b[0] > 0.9, "expected red at {p:?}, got {b:?}");
}
}
#[test]
fn the_interior_a_carve_exposes_is_bare() {
let log = [
Edit::Spray(spray()),
Edit::Carve(Brush::subtract(Sphere {
center: [0.0, 0.0, 0.0],
radius: 0.35,
})),
];
let world = PaintStack {
base: wall(),
edits: &log,
background: GREY,
};
for z in [-0.10, 0.0, 0.10] {
let p = [0.35, 0.0, z];
let c = world.color_at(p);
assert_eq!(c, GREY, "newly exposed interior is painted at {p:?}");
}
}
#[test]
fn paint_stops_at_the_edge_of_the_spray() {
let log: [Log; 1] = [Edit::Spray(spray())];
let world = PaintStack {
base: wall(),
edits: &log,
background: GREY,
};
assert_eq!(world.color_at([1.0, 0.0, -0.25]), GREY);
}
#[test]
fn a_shape_that_reaches_through_a_wall_paints_both_faces() {
let reaching: [Log; 1] = [Edit::Spray(spray())];
let both = PaintStack {
base: wall(),
edits: &reaching,
background: GREY,
};
assert!(
both.color_at([0.0, 0.0, 0.25])[0] > 0.9,
"a shape spanning the wall paints the far face"
);
let shallow: [Log; 1] = [Edit::Spray(Splat {
shape: Sphere {
center: [0.0, 0.0, -0.25],
radius: 0.3,
},
..spray()
})];
let one_face = PaintStack {
base: wall(),
edits: &shallow,
background: GREY,
};
assert!(
one_face.color_at([0.0, 0.0, -0.25])[0] > 0.9,
"front is painted"
);
assert_eq!(
one_face.color_at([0.0, 0.0, 0.25]),
GREY,
"back face is out of the shape's reach"
);
}
#[test]
fn a_log_without_sprays_samples_bit_identically_to_a_brush_stack() {
let brushes = [
hole(),
Brush::add(Sphere {
center: [-1.0, 0.5, 0.0],
radius: 0.4,
}),
Brush::smooth_add(
Sphere {
center: [0.8, -0.6, 0.1],
radius: 0.3,
},
0.15,
),
];
let edits: Vec<Edit<Sphere<f64>, Sphere<f64>, f64>> =
brushes.iter().copied().map(Edit::Carve).collect();
let stack = BrushStack {
base: wall(),
brushes: &brushes,
};
let painted = PaintStack {
base: wall(),
edits: &edits,
background: GREY,
};
for i in 0..40 {
let t = f64::from(i) * 0.1 - 2.0;
let p = [t, t * 0.5 - 0.3, t * 0.25];
assert_eq!(
stack.sample(p).to_bits(),
painted.sample(p).to_bits(),
"field diverged at {p:?}"
);
}
}
#[test]
fn sprays_do_not_move_the_surface() {
let carve_only = [Edit::<Sphere<f64>, Sphere<f64>, f64>::Carve(hole())];
let with_spray = [Edit::Spray(spray()), Edit::Carve(hole())];
let a = PaintStack {
base: wall(),
edits: &carve_only,
background: GREY,
};
let b = PaintStack {
base: wall(),
edits: &with_spray,
background: GREY,
};
for i in 0..40 {
let t = f64::from(i) * 0.1 - 2.0;
let p = [t, t * 0.5 - 0.3, t * 0.25];
assert_eq!(a.sample(p).to_bits(), b.sample(p).to_bits());
}
}
#[test]
fn sprays_layer_in_log_order() {
let blue = Splat {
color: [0.0, 0.0, 1.0, 1.0],
..spray()
};
let red_then_blue: [Log; 2] = [Edit::Spray(spray()), Edit::Spray(blue)];
let blue_then_red: [Log; 2] = [Edit::Spray(blue), Edit::Spray(spray())];
let p = [0.0, 0.0, -0.25];
let first = PaintStack {
base: wall(),
edits: &red_then_blue,
background: GREY,
}
.color_at(p);
let second = PaintStack {
base: wall(),
edits: &blue_then_red,
background: GREY,
}
.color_at(p);
assert!(
first[2] > 0.9 && first[0] < 0.1,
"blue should be on top: {first:?}"
);
assert!(
second[0] > 0.9 && second[2] < 0.1,
"red should be on top: {second:?}"
);
}
#[test]
fn a_half_covered_splat_blends_and_leaves_alpha_alone() {
let half = Splat {
color: [1.0, 0.0, 0.0, 0.5],
..spray()
};
let log: [Log; 1] = [Edit::Spray(half)];
let c = PaintStack {
base: wall(),
edits: &log,
background: GREY,
}
.color_at([0.0, 0.0, -0.25]);
assert!((c[0] - 0.75).abs() < 1e-12, "half of the way to red: {c:?}");
assert_eq!(c[3], GREY[3], "alpha is the background's");
}
#[test]
fn a_zero_width_ramp_is_a_step() {
assert_eq!(ramp(-1.0_f64, 0.0), 1.0);
assert_eq!(ramp(0.0_f64, 0.0), 1.0);
assert_eq!(ramp(1e-300_f64, 0.0), 0.0);
assert_eq!(ramp(1.0_f64, -1.0), 0.0);
}
#[test]
fn a_positive_width_ramp_is_linear_and_clamped() {
assert_eq!(ramp(-1.0_f64, 2.0), 1.0);
assert_eq!(ramp(0.0_f64, 2.0), 1.0);
assert_eq!(ramp(1.0_f64, 2.0), 0.5);
assert_eq!(ramp(2.0_f64, 2.0), 0.0);
assert_eq!(ramp(3.0_f64, 2.0), 0.0);
}
#[test]
fn shade_refills_without_releasing_capacity() {
let log: [Log; 1] = [Edit::Spray(spray())];
let world = PaintStack {
base: wall(),
edits: &log,
background: GREY,
};
let positions: Vec<[f64; 3]> = SURVIVING.into_iter().collect();
let mut out = Vec::new();
shade(&positions, &world, &mut out);
assert_eq!(out.len(), positions.len());
let capacity = out.capacity();
let first = out.clone();
shade(&positions, &world, &mut out);
assert_eq!(out, first, "shade is not deterministic");
assert_eq!(out.capacity(), capacity, "shade reallocated");
shade(&[], &world, &mut out);
assert!(out.is_empty());
assert_eq!(out.capacity(), capacity, "shade released capacity");
}
#[test]
fn shade_agrees_with_color_at() {
let log = [Edit::Spray(spray()), Edit::Carve(hole())];
let world = PaintStack {
base: wall(),
edits: &log,
background: GREY,
};
let positions: Vec<[f64; 3]> = SURVIVING.into_iter().collect();
let mut out = Vec::new();
shade(&positions, &world, &mut out);
for (p, c) in positions.iter().zip(&out) {
assert_eq!(world.color_at(*p).map(f64::to_bits), c.map(f64::to_bits));
}
}
#[test]
fn it_works_in_f32() {
let log: [Log32; 1] = [Edit::Spray(Splat {
shape: Sphere {
center: [0.0_f32, 0.0, -0.25],
radius: 0.75,
},
color: [1.0_f32, 0.0, 0.0, 1.0],
softness: 0.1,
depth: 0.05,
})];
let world = PaintStack {
base: BoxExact {
center: [0.0_f32; 3],
half_extents: [2.0, 2.0, 0.25],
},
edits: &log,
background: [0.5_f32, 0.5, 0.5, 1.0],
};
assert!(world.color_at([0.0, 0.0, -0.25])[0] > 0.9);
assert_eq!(world.color_at([1.5, 0.0, -0.25]), [0.5, 0.5, 0.5, 1.0]);
}