use cvkg_core::Rect;
use std::collections::HashMap;
const MAX_CELL_SPAN: i32 = 1000;
pub struct SpatialHash<T> {
cells: HashMap<(i32, i32), Vec<T>>,
cell_size: f32,
registration_count: usize,
}
impl<T: Clone> SpatialHash<T> {
pub fn new(cell_size: f32) -> Option<Self> {
if cell_size <= 0.0 {
return None;
}
Some(Self {
cells: HashMap::new(),
cell_size,
registration_count: 0,
})
}
pub fn insert(&mut self, rect: Rect, item: T) {
let (min_cx, min_cy, max_cx, max_cy) = self.cells_for_rect(rect);
for cx in min_cx..=max_cx {
for cy in min_cy..=max_cy {
self.cells.entry((cx, cy)).or_default().push(item.clone());
self.registration_count += 1;
}
}
}
pub fn query(&self, rect: Rect) -> Vec<T> {
let (min_cx, min_cy, max_cx, max_cy) = self.cells_for_rect(rect);
let mut results = Vec::new();
for cx in min_cx..=max_cx {
for cy in min_cy..=max_cy {
if let Some(cell) = self.cells.get(&(cx, cy)) {
results.extend_from_slice(cell);
}
}
}
results
}
pub fn clear(&mut self) {
self.cells.clear();
self.registration_count = 0;
}
pub fn len(&self) -> usize {
self.registration_count
}
pub fn is_empty(&self) -> bool {
self.registration_count == 0
}
fn cells_for_rect(&self, rect: Rect) -> (i32, i32, i32, i32) {
let min_cx = (rect.x / self.cell_size)
.floor()
.clamp(i32::MIN as f32, i32::MAX as f32) as i32;
let min_cy = (rect.y / self.cell_size)
.floor()
.clamp(i32::MIN as f32, i32::MAX as f32) as i32;
let max_cx = ((rect.x + rect.width) / self.cell_size)
.floor()
.clamp(i32::MIN as f32, i32::MAX as f32) as i32;
let max_cy = ((rect.y + rect.height) / self.cell_size)
.floor()
.clamp(i32::MIN as f32, i32::MAX as f32) as i32;
let max_cx = max_cx.min(min_cx.saturating_add(MAX_CELL_SPAN));
let max_cy = max_cy.min(min_cy.saturating_add(MAX_CELL_SPAN));
(min_cx, min_cy, max_cx, max_cy)
}
}
#[cfg(test)]
mod tests {
use super::*;
use cvkg_core::Rect;
fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
#[test]
fn test_insert_and_query() {
let mut sh: SpatialHash<u32> = SpatialHash::new(100.0).unwrap();
sh.insert(rect(0.0, 0.0, 50.0, 50.0), 1);
sh.insert(rect(200.0, 200.0, 50.0, 50.0), 2);
let results = sh.query(rect(0.0, 0.0, 60.0, 60.0));
assert!(results.contains(&1), "Should find item 1");
assert!(!results.contains(&2), "Should NOT find item 2");
}
#[test]
fn test_clear_resets_state() {
let mut sh: SpatialHash<u32> = SpatialHash::new(64.0).unwrap();
sh.insert(rect(0.0, 0.0, 10.0, 10.0), 42);
assert!(!sh.is_empty());
sh.clear();
assert!(sh.is_empty());
assert_eq!(sh.len(), 0);
let results = sh.query(rect(0.0, 0.0, 10.0, 10.0));
assert!(results.is_empty());
}
#[test]
fn test_multi_cell_item() {
let mut sh: SpatialHash<&str> = SpatialHash::new(100.0).unwrap();
sh.insert(rect(50.0, 0.0, 100.0, 50.0), "wide");
let left = sh.query(rect(0.0, 0.0, 60.0, 50.0));
let right = sh.query(rect(100.0, 0.0, 60.0, 50.0));
assert!(
left.contains(&"wide"),
"wide should appear in left cell query"
);
assert!(
right.contains(&"wide"),
"wide should appear in right cell query"
);
}
#[test]
fn test_negative_coordinates() {
let mut sh: SpatialHash<i32> = SpatialHash::new(50.0).unwrap();
sh.insert(rect(-100.0, -100.0, 20.0, 20.0), 99);
let results = sh.query(rect(-110.0, -110.0, 40.0, 40.0));
assert!(results.contains(&99), "Should handle negative coordinates");
}
}