use alloc::vec::Vec;
use he3d_math::Vec3;
use he3d_math::scalar::floor;
use crate::aabb::Aabb;
pub const DEFAULT_CELL_SIZE: f32 = 2.0;
const MAX_CELLS_PER_BODY: u64 = 64;
const CELL_LIMIT: f32 = 1_048_576.0;
#[derive(Clone, Copy, Debug)]
struct Entry {
body: u32,
next: i32,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct SpatialGrid {
inv_cell_size: f32,
buckets: Vec<i32>,
mask: usize,
entries: Vec<Entry>,
oversized: Vec<u32>,
oversized_flag: Vec<bool>,
}
impl SpatialGrid {
pub(crate) fn new(cell_size: f32) -> Self {
let cell_size = if cell_size.is_finite() && cell_size > 0.0 {
cell_size
} else {
DEFAULT_CELL_SIZE
};
Self {
inv_cell_size: 1.0 / cell_size,
buckets: Vec::new(),
mask: 0,
entries: Vec::new(),
oversized: Vec::new(),
oversized_flag: Vec::new(),
}
}
pub(crate) fn rebuild(&mut self, aabbs: &[Aabb]) {
let wanted = aabbs.len().saturating_mul(2).next_power_of_two().max(64);
if self.buckets.len() == wanted {
self.buckets.fill(-1);
} else {
self.buckets = alloc::vec![-1; wanted];
self.mask = wanted - 1;
}
self.entries.clear();
self.oversized.clear();
self.oversized_flag.clear();
self.oversized_flag.resize(aabbs.len(), false);
for (index, aabb) in aabbs.iter().enumerate() {
let Ok(index) = u32::try_from(index) else {
break;
};
let low = self.cell_of(aabb.min);
let high = self.cell_of(aabb.max);
let span = cell_span(low, high);
if span > MAX_CELLS_PER_BODY {
self.oversized.push(index);
self.oversized_flag[index as usize] = true;
continue;
}
for z in low.z..=high.z {
for y in low.y..=high.y {
for x in low.x..=high.x {
let bucket = bucket_index(x, y, z, self.mask);
self.entries.push(Entry {
body: index,
next: self.buckets[bucket],
});
self.buckets[bucket] = i32::try_from(self.entries.len() - 1).unwrap_or(-1);
}
}
}
}
}
fn cell_of(&self, point: Vec3) -> Cell {
Cell {
x: cell_coord(point.x, self.inv_cell_size),
y: cell_coord(point.y, self.inv_cell_size),
z: cell_coord(point.z, self.inv_cell_size),
}
}
pub(crate) fn for_each_candidate(&self, aabbs: &[Aabb], mut emit: impl FnMut(u32, u32)) {
for (index, aabb) in aabbs.iter().enumerate() {
if self.oversized_flag.get(index).copied().unwrap_or(false) {
continue;
}
let Ok(index) = u32::try_from(index) else {
break;
};
let low = self.cell_of(aabb.min);
let high = self.cell_of(aabb.max);
for z in low.z..=high.z {
for y in low.y..=high.y {
for x in low.x..=high.x {
let cell = Cell { x, y, z };
let mut cursor = self.buckets[bucket_index(x, y, z, self.mask)];
while cursor >= 0 {
let entry = self.entries[usize::try_from(cursor).unwrap_or(0)];
cursor = entry.next;
if entry.body <= index {
continue;
}
let other = &aabbs[entry.body as usize];
if !aabb.overlaps(other) {
continue;
}
let overlap_min = aabb.min.max(other.min);
if self.cell_of(overlap_min) != cell {
continue;
}
emit(index, entry.body);
}
}
}
}
}
for &index in &self.oversized {
for other_index in 0..aabbs.len() {
let Ok(other_index) = u32::try_from(other_index) else {
break;
};
if other_index == index {
continue;
}
if self.oversized_flag[other_index as usize] && other_index < index {
continue;
}
let (low, high) = if index < other_index {
(index, other_index)
} else {
(other_index, index)
};
if aabbs[low as usize].overlaps(&aabbs[high as usize]) {
emit(low, high);
}
}
}
}
#[cfg(test)]
pub(crate) fn oversized_count(&self) -> usize {
self.oversized.len()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Cell {
x: i32,
y: i32,
z: i32,
}
fn cell_span(low: Cell, high: Cell) -> u64 {
let span = |a: i32, b: i32| u64::try_from(i64::from(b) - i64::from(a) + 1).unwrap_or(0);
span(low.x, high.x)
.saturating_mul(span(low.y, high.y))
.saturating_mul(span(low.z, high.z))
}
#[expect(clippy::cast_possible_truncation)]
fn cell_coord(value: f32, inv_cell_size: f32) -> i32 {
let scaled = value * inv_cell_size;
if !scaled.is_finite() {
return 0;
}
floor(scaled).clamp(-CELL_LIMIT, CELL_LIMIT) as i32
}
fn bucket_index(x: i32, y: i32, z: i32, mask: usize) -> usize {
let mix = |value: i32, salt: u32| u32::from_ne_bytes(value.to_ne_bytes()).wrapping_mul(salt);
let hashed = mix(x, 73_856_093) ^ mix(y, 19_349_663) ^ mix(z, 83_492_791);
usize::try_from(hashed).unwrap_or(0) & mask
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss)]
mod tests {
use super::{DEFAULT_CELL_SIZE, SpatialGrid, cell_coord};
use crate::aabb::Aabb;
use alloc::vec::Vec;
use he3d_math::Vec3;
fn box_at(x: f32, half: f32) -> Aabb {
Aabb::from_center_half_extents(Vec3::new(x, 0.0, 0.0), Vec3::splat(half))
}
fn pairs(aabbs: &[Aabb]) -> Vec<(u32, u32)> {
let mut grid = SpatialGrid::new(DEFAULT_CELL_SIZE);
grid.rebuild(aabbs);
let mut found = Vec::new();
grid.for_each_candidate(aabbs, |a, b| found.push((a, b)));
found.sort_unstable();
found
}
#[test]
fn overlapping_boxes_are_reported_exactly_once() {
let aabbs = [box_at(0.0, 1.0), box_at(1.0, 1.0)];
assert_eq!(pairs(&aabbs), alloc::vec![(0, 1)]);
}
#[test]
fn a_pair_sharing_many_cells_is_still_reported_once() {
let aabbs = [box_at(0.0, 5.0), box_at(0.0, 5.0)];
assert_eq!(pairs(&aabbs), alloc::vec![(0, 1)]);
}
#[test]
fn separated_boxes_are_not_reported() {
let aabbs = [box_at(0.0, 0.5), box_at(100.0, 0.5)];
assert_eq!(pairs(&aabbs), [] as [(u32, u32); 0]);
}
#[test]
fn touching_boxes_are_reported() {
let aabbs = [box_at(0.0, 1.0), box_at(2.0, 1.0)];
assert_eq!(pairs(&aabbs), alloc::vec![(0, 1)]);
}
#[test]
fn three_overlapping_boxes_yield_every_pair_once() {
let aabbs = [box_at(0.0, 1.0), box_at(0.5, 1.0), box_at(1.0, 1.0)];
assert_eq!(pairs(&aabbs), alloc::vec![(0, 1), (0, 2), (1, 2)]);
}
#[test]
fn a_huge_box_falls_back_to_the_oversized_list() {
let aabbs = [box_at(0.0, 1000.0), box_at(10.0, 0.5), box_at(5000.0, 0.5)];
let mut grid = SpatialGrid::new(DEFAULT_CELL_SIZE);
grid.rebuild(&aabbs);
assert_eq!(grid.oversized_count(), 1);
assert_eq!(pairs(&aabbs), alloc::vec![(0, 1)]);
}
#[test]
fn two_huge_boxes_pair_once() {
let aabbs = [box_at(0.0, 1000.0), box_at(100.0, 1000.0)];
assert_eq!(pairs(&aabbs), alloc::vec![(0, 1)]);
}
#[test]
fn cell_coordinates_are_clamped_not_wrapped() {
let far = cell_coord(1e30, 0.5);
let opposite = cell_coord(-1e30, 0.5);
assert_ne!(far, opposite);
assert!(far > 0);
assert!(opposite < 0);
assert_eq!(cell_coord(f32::NAN, 0.5), 0);
}
#[test]
fn an_empty_grid_reports_nothing() {
assert_eq!(pairs(&[]), [] as [(u32, u32); 0]);
}
#[test]
fn many_scattered_boxes_report_only_real_overlaps() {
let mut aabbs = Vec::new();
for index in 0..200 {
aabbs.push(box_at(index as f32 * 4.0, 0.5));
}
assert_eq!(pairs(&aabbs), [] as [(u32, u32); 0]);
}
}