use super::{transfer_budget::TransferBudget, transfer_math::Point};
const AXIS_CELLS: f64 = 2_097_152.;
pub(super) struct PointGrid {
cell: f64,
min: Point,
order: Vec<u32>,
cells: Vec<(u64, u32)>,
}
impl PointGrid {
pub fn build(points: &[Point], cell: f64, budget: &mut TransferBudget) -> Result<Self, String> {
if points.is_empty() || !cell.is_finite() || cell <= 0. {
return Err("Transfer point index needs points and a positive cell size".into());
}
let mut min = points[0];
let mut max = points[0];
for p in points {
for axis in 0..3 {
min[axis] = min[axis].min(p[axis]);
max[axis] = max[axis].max(p[axis]);
}
}
if (0..3).any(|axis| (max[axis] - min[axis]) / cell >= AXIS_CELLS - 2.) {
return Err("Transfer point extent exceeds the spatial index bound; crop the scan or raise the distance bound".into());
}
budget.reserve(points.len() * 16)?;
let levels = usize::BITS as usize - points.len().leading_zeros() as usize;
budget.charge(points.len() * (levels + 1))?;
let key_of = |p: &Point| pack(cell_coordinates(p, min, cell));
let mut keyed: Vec<(u64, u32)> = points
.iter()
.enumerate()
.map(|(i, p)| (key_of(p), i as u32))
.collect();
keyed.sort_unstable();
let mut cells = Vec::new();
let mut order = Vec::with_capacity(keyed.len());
for (position, (key, index)) in keyed.iter().enumerate() {
if cells.last().is_none_or(|(last, _)| last != key) {
cells.push((*key, position as u32));
}
order.push(*index);
}
Ok(Self { cell, min, order, cells })
}
pub fn within(
&self,
points: &[Point],
center: Point,
radius: f64,
out: &mut Vec<(u32, f64)>,
budget: &mut TransferBudget,
) -> Result<(), String> {
if !center.iter().all(|v| v.is_finite()) || !radius.is_finite() || radius < 0. || radius > 16. * self.cell {
return Err("Transfer point query needs a finite center and a radius within 16 index cells".into());
}
let low = cell_coordinates(&std::array::from_fn(|a| center[a] - radius), self.min, self.cell);
let high = cell_coordinates(&std::array::from_fn(|a| center[a] + radius), self.min, self.cell);
let r2 = radius * radius;
for x in low[0]..=high[0] {
for y in low[1]..=high[1] {
for z in low[2]..=high[2] {
budget.charge(1)?;
let key = pack([x, y, z]);
let Ok(slot) = self.cells.binary_search_by_key(&key, |(k, _)| *k) else { continue };
let start = self.cells[slot].1 as usize;
let end = self.cells.get(slot + 1).map_or(self.order.len(), |(_, s)| *s as usize);
budget.charge(end - start)?;
for &index in &self.order[start..end] {
let p = points[index as usize];
let d2: f64 = (0..3).map(|a| (p[a] - center[a]).powi(2)).sum();
if d2 <= r2 {
out.push((index, d2));
}
}
}
}
}
Ok(())
}
pub fn nearest(
&self,
points: &[Point],
center: Point,
radius: f64,
scratch: &mut Vec<(u32, f64)>,
budget: &mut TransferBudget,
) -> Result<Option<(u32, f64)>, String> {
scratch.clear();
self.within(points, center, radius, scratch, budget)?;
Ok(scratch
.iter()
.copied()
.fold(None, |best: Option<(u32, f64)>, (i, d2)| match best {
Some((bi, bd)) if bd < d2 || (bd == d2 && bi < i) => Some((bi, bd)),
_ => Some((i, d2)),
}))
}
}
fn cell_coordinates(p: &Point, min: Point, cell: f64) -> [u64; 3] {
std::array::from_fn(|axis| {
(((p[axis] - min[axis]) / cell).floor().max(0.) as u64).min(AXIS_CELLS as u64 - 1)
})
}
fn pack(c: [u64; 3]) -> u64 {
(c[0] << 42) | (c[1] << 21) | c[2]
}
#[cfg(test)]
mod tests {
use super::*;
fn cloud() -> Vec<Point> {
(0..1000)
.map(|i| {
let f = i as f64;
[(f * 0.37).rem_euclid(3.) - 1., (f * 0.91).rem_euclid(2.) + 5., (f * 0.13).rem_euclid(1.5) - 100.]
})
.collect()
}
#[test]
fn issue_4381_grid_queries_match_brute_force_and_charge_work() {
let points = cloud();
let mut budget = TransferBudget::new();
let grid = PointGrid::build(&points, 0.05, &mut budget).unwrap();
let before = budget.work;
for center in [[0., 5.5, -99.5], [-1., 5., -100.], [2.01, 7., -98.5], [0.3, 6.2, -99.]] {
for radius in [0., 0.01, 0.05, 0.12] {
let mut out = Vec::new();
grid.within(&points, center, radius, &mut out, &mut budget).unwrap();
out.sort_by_key(|(i, _)| *i);
let expected: Vec<(u32, f64)> = points
.iter()
.enumerate()
.filter_map(|(i, p)| {
let d2: f64 = (0..3).map(|a| (p[a] - center[a]).powi(2)).sum();
(d2 <= radius * radius).then_some((i as u32, d2))
})
.collect();
assert_eq!(out, expected, "{center:?} r={radius}");
let nearest = grid.nearest(&points, center, radius, &mut Vec::new(), &mut budget).unwrap();
let brute = expected.iter().copied().min_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
assert_eq!(nearest, brute);
}
}
assert!(budget.work < before, "every query charges the shared budget");
assert!(grid.within(&points, [0., 5.5, -99.5], 0.9, &mut Vec::new(), &mut budget).is_err(), "a radius beyond 16 cells is refused");
assert!(grid.within(&points, [f64::NAN, 5.5, -99.5], 0.01, &mut Vec::new(), &mut budget).is_err());
let mut exhausted = TransferBudget::new();
exhausted.work = 3;
assert!(grid.within(&points, [0., 5.5, -99.5], 0.05, &mut Vec::new(), &mut exhausted).unwrap_err().contains("budget"));
assert!(PointGrid::build(&points, 1e-9, &mut TransferBudget::new()).err().is_some_and(|e| e.contains("extent")));
assert!(PointGrid::build(&[], 0.1, &mut TransferBudget::new()).is_err());
}
}