Files
OneCAD/crates/geom/src/bvh_tests.rs
T

131 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
/// A small deterministic generator (the tests need no crate for randomness).
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> f64 {
self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
(self.0 >> 11) as f64 / (1u64 << 53) as f64
}
fn range(&mut self, lo: f64, hi: f64) -> f64 {
lo + (hi - lo) * self.next()
}
fn vec(&mut self, r: f64) -> Vec3 {
Vec3::new(self.range(-r, r), self.range(-r, r), self.range(-r, r))
}
}
/// A grid of `n` × `n` squares of two triangles on z = 0, and `extra` random triangles above.
fn mesh(n: u32, extra: usize, rng: &mut Lcg) -> Mesh {
let mut positions: Vec<Vec3> = (0..=n).flat_map(|j| (0..=n).map(move |i| Vec3::new(f64::from(i), f64::from(j), 0.0))).collect();
let id = |i: u32, j: u32| j * (n + 1) + i;
let mut triangles = Vec::new();
for j in 0..n {
for i in 0..n {
triangles.push([id(i, j), id(i + 1, j), id(i + 1, j + 1)]);
triangles.push([id(i, j), id(i + 1, j + 1), id(i, j + 1)]);
}
}
for _ in 0..extra {
let c = rng.vec(f64::from(n)) + Vec3::new(0.0, 0.0, f64::from(n));
let k = positions.len() as u32;
positions.extend([c + rng.vec(1.5), c + rng.vec(1.5), c + rng.vec(1.5)]);
triangles.push([k, k + 1, k + 2]);
}
let edges =
(0..n).map(|i| (0..=n).map(|j| Vec3::new(f64::from(i), f64::from(j), 0.0)).collect()).chain([vec![Vec3::new(0.5, 0.5, 2.0)]]).collect();
Mesh { tri_face: vec![0; triangles.len()], positions, triangles, edges, ..Default::default() }
}
#[test]
fn indexed_raycast_answers_as_the_scan_does() {
let mut rng = Lcg(7);
let m = mesh(12, 400, &mut rng);
let index = MeshIndex::new(&m);
let mut hits = 0;
for k in 0..4000 {
// Every few rays go straight down through grid vertices and edges, where two or more
// triangles tie.
let (o, d) = if k % 4 == 0 {
let p = Vec3::new(f64::from(k % 13), f64::from((k / 13) % 13) * 0.5, 50.0);
(p, Vec3::new(0.0, 0.0, -1.0))
} else {
let o = rng.vec(40.0) + Vec3::new(6.0, 6.0, 0.0);
let target = Vec3::new(rng.range(-1.0, 13.0), rng.range(-1.0, 13.0), rng.range(-2.0, 14.0));
let Some(d) = (target - o).normalized() else { continue };
(o, d)
};
let want = m.raycast(o, d);
assert_eq!(index.raycast(&m, o, d), want, "ray {k} from {o:?} along {d:?}");
hits += usize::from(want.is_some());
}
assert!(hits > 1000, "the rays should mostly hit ({hits})");
}
#[test]
fn indexed_raycast_of_another_mesh_falls_back_to_the_scan() {
let mut rng = Lcg(3);
let (a, b) = (mesh(2, 0, &mut rng), mesh(5, 0, &mut rng));
let index = MeshIndex::new(&a);
let (o, d) = (Vec3::new(4.5, 4.2, 9.0), Vec3::new(0.0, 0.0, -1.0));
assert!(b.raycast(o, d).is_some());
assert_eq!(index.raycast(&b, o, d), b.raycast(o, d));
}
/// Distance from point `p` to the ray from `o` along unit `d` (from its start on), and how far along.
fn ray_point(o: Vec3, d: Vec3, p: Vec3) -> (f64, f64) {
let t = (p - o).dot(d).max(0.0);
((o + d * t).dist(p), t)
}
#[test]
fn segments_near_the_ray_are_all_reported() {
let mut rng = Lcg(11);
let m = mesh(16, 0, &mut rng);
let index = MeshIndex::new(&m);
for k in 0..500 {
let o = rng.vec(30.0) + Vec3::new(8.0, 8.0, 30.0);
let target = Vec3::new(rng.range(0.0, 16.0), rng.range(0.0, 16.0), 0.0);
let Some(d) = (target - o).normalized() else { continue };
let (a, b) = if k % 2 == 0 { (0.3, 0.0) } else { (0.0, 0.01) };
let mut got = std::collections::HashSet::new();
index.segments_near(o, d, a, b, |e, s| {
assert!(got.insert((e, s)), "segment {e}/{s} reported twice");
});
for (ei, e) in m.edges.iter().enumerate() {
for (si, w) in e.windows(2).enumerate() {
// Sampled along the segment: any point within reach must have its segment reported.
for f in 0..=8 {
let p = w[0] + (w[1] - w[0]) * (f64::from(f) / 8.0);
let (dist, t) = ray_point(o, d, p);
if dist <= a + b * t {
assert!(got.contains(&(ei, si)), "ray {k}: segment {ei}/{si} is within reach but was not reported");
}
}
}
}
// The one-point edge stands as its own segment.
let last = m.edges.len() - 1;
let (dist, t) = ray_point(o, d, Vec3::new(0.5, 0.5, 2.0));
if dist <= a + b * t {
assert!(got.contains(&(last, 0)));
}
}
}
#[test]
fn hostile_boxes_never_panic() {
let nan = Vec3::new(f64::NAN, 0.0, 0.0);
let boxes = [Aabb3::EMPTY, Aabb3 { min: nan, max: nan }, Aabb3 { min: Vec3::ZERO, max: Vec3::new(1.0, 1.0, 1.0) }];
let bvh = Bvh::new(&boxes);
assert_eq!(bvh.nearest(Vec3::new(0.5, 0.5, 5.0), Vec3::new(0.0, 0.0, -1.0), |i| Some(f64::from(i))), Some((2.0, 2)));
assert_eq!(bvh.nearest(Vec3::ZERO, Vec3::new(f64::NAN, 0.0, 0.0), |_| Some(1.0)), None);
let empty = Bvh::new(&[]);
assert!(empty.nearest(Vec3::ZERO, Vec3::new(0.0, 0.0, 1.0), |_| Some(1.0)).is_none());
empty.visit(|_| true, |_| panic!("an empty hierarchy has no items"));
let m = Mesh { positions: vec![Vec3::ZERO], triangles: vec![[0, 5, 9]], ..Default::default() };
assert!(MeshIndex::new(&m).raycast(&m, Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, 0.0, -1.0)).is_none());
MeshIndex::new(&m).segments_near(Vec3::ZERO, Vec3::new(0.0, 0.0, 1.0), f64::NAN, 0.0, |_, _| panic!("no segments"));
}