use super::{aabb_contains, Aabb, Bvh};
impl Bvh {
pub fn nearest_point_bounded(
&self,
point: [f64; 3],
radius: f64,
remaining: &mut usize,
ambiguity: f64,
candidates: &mut Vec<(u32, f64)>,
mut distance_squared: impl FnMut(u32) -> f64,
) -> Result<Option<(u32, f64)>, &'static str> {
if !point.iter().all(|v| v.is_finite()) || !radius.is_finite() || radius < 0.
|| !ambiguity.is_finite() || ambiguity < 0. || !(radius + ambiguity).is_finite() {
return Err("BVH query requires a finite point and nonnegative radius");
}
candidates.clear();
if self.root == u32::MAX { return Ok(None); }
let mut pending = [0_u32; 64];
pending[0] = self.root;
let mut count = 1;
let mut best: Option<(u32, u32, f64)> = None;
let mut limit = radius + ambiguity;
while count > 0 {
count -= 1;
let index = pending[count];
charge(remaining)?;
let node = &self.nodes[index as usize];
if !aabb_contains(point, &node.aabb, self.pad + limit) { continue; }
if node.tri != u32::MAX {
charge(remaining)?;
let d2 = distance_squared(node.tri);
if !d2.is_finite() || d2 < 0. {
return Err("BVH primitive distance must be finite and nonnegative");
}
if d2.sqrt() > limit { continue; }
candidates.push((node.tri, d2));
if best.is_none_or(|(_, old_index, old)| d2 < old || (d2 == old && index < old_index)) {
best = Some((node.tri, index, d2));
limit = d2.sqrt() + ambiguity;
}
} else {
if count + 2 > pending.len() { return Err("BVH query stack budget exceeded"); }
let left = box_distance(point, &self.nodes[node.left as usize].aabb);
let right = box_distance(point, &self.nodes[node.right as usize].aabb);
let (near, far) = if left <= right { (node.left, node.right) } else { (node.right, node.left) };
pending[count] = far;
pending[count + 1] = near;
count += 2;
}
}
*remaining = remaining.checked_sub(candidates.len()).ok_or("BVH query work budget exhausted")?;
candidates.retain(|(_, d2)| d2.sqrt() <= limit);
Ok(best.filter(|(_, _, d2)| d2.sqrt() <= radius).map(|(id, _, d2)| (id, d2)))
}
}
fn charge(remaining: &mut usize) -> Result<(), &'static str> {
*remaining = remaining.checked_sub(1).ok_or("BVH query work budget exhausted")?;
Ok(())
}
fn box_distance(point: [f64; 3], bounds: &Aabb) -> f64 {
(0..3).map(|axis| {
let d = (bounds.0[axis] - point[axis]).max(point[axis] - bounds.1[axis]).max(0.);
d * d
}).sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn d2(a: [f64; 3], b: [f64; 3]) -> f64 {
(0..3).map(|axis| (a[axis] - b[axis]).powi(2)).sum()
}
#[test]
fn transfer_4381_nearest_and_band_match_independent_distances() {
let points: Vec<_> = (0..125).map(|i| [f64::from(i % 5), f64::from((i / 5) % 5), f64::from(i / 25)]).collect();
let tree = Bvh::build(&points.iter().map(|p| [*p; 3]).collect::<Vec<_>>());
for query in [[0.1, 0.2, 0.3], [2.5, 2.5, 2.5], [-2., 1., 3.], [4., 4., 4.]] {
let mut order = Vec::new();
tree.point_candidates(query, 10., &mut order);
let mut expected = None;
for id in order {
let distance = d2(points[id as usize], query);
if expected.is_none_or(|(_, old)| distance < old) { expected = Some((id, distance)); }
}
let mut candidates = Vec::new();
let actual = tree.nearest_point_bounded(query, 10., &mut 1000, 0.4, &mut candidates,
|id| d2(points[id as usize], query)).unwrap();
assert_eq!(actual, expected, "nearest and original left-to-right tie precedence");
let nearest = expected.unwrap().1.sqrt();
let expected_band: Vec<_> = points.iter().enumerate().filter_map(|(id, point)| {
let distance = d2(*point, query);
(distance.sqrt() <= nearest + 0.4).then_some((id as u32, distance))
}).collect();
candidates.sort_by_key(|(id, _)| *id);
assert_eq!(candidates, expected_band, "no near-tie observation may be pruned");
}
}
#[test]
fn transfer_4381_nearest_refuses_budget_and_invalid_distance() {
let tree = Bvh::build(&[[[0.; 3]; 3], [[2.; 3]; 3]]);
assert!(tree.nearest_point_bounded([0.; 3], 1., &mut 0, 0., &mut Vec::new(), |_| 0.).is_err());
assert!(tree.nearest_point_bounded([0.; 3], 1., &mut 100, 0., &mut Vec::new(), |_| f64::NAN).is_err());
assert!(tree.nearest_point_bounded([0.; 3], 1., &mut 100, -1., &mut Vec::new(), |_| 0.).is_err());
assert_eq!(tree.nearest_point_bounded([8.; 3], 1., &mut 100, 0., &mut Vec::new(), |_| 108.).unwrap(), None);
assert_eq!(Bvh::build(&[]).nearest_point_bounded([0.; 3], 1., &mut 0, 0., &mut Vec::new(), |_| 0.).unwrap(), None);
}
}