Skip to main content

oxihuman_core/
spatial_hash_2d.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! 2D spatial hashing for broad-phase point queries.
5
6#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10/// A 2D spatial hash grid.
11#[allow(dead_code)]
12#[derive(Debug, Clone)]
13pub struct SpatialHash2D {
14    pub cell_size: f32,
15    cells: HashMap<(i32, i32), Vec<usize>>,
16    positions: Vec<[f32; 2]>,
17}
18
19#[allow(dead_code)]
20impl SpatialHash2D {
21    pub fn new(cell_size: f32) -> Self {
22        Self {
23            cell_size: cell_size.max(1e-6),
24            cells: HashMap::new(),
25            positions: Vec::new(),
26        }
27    }
28
29    fn cell_key(&self, x: f32, y: f32) -> (i32, i32) {
30        let cx = (x / self.cell_size).floor() as i32;
31        let cy = (y / self.cell_size).floor() as i32;
32        (cx, cy)
33    }
34
35    /// Insert a point and return its index.
36    pub fn insert(&mut self, pos: [f32; 2]) -> usize {
37        let id = self.positions.len();
38        self.positions.push(pos);
39        let key = self.cell_key(pos[0], pos[1]);
40        self.cells.entry(key).or_default().push(id);
41        id
42    }
43
44    /// Query all point indices in the same or neighboring cells (within radius).
45    pub fn query_radius(&self, x: f32, y: f32, radius: f32) -> Vec<usize> {
46        let r_cells = (radius / self.cell_size).ceil() as i32 + 1;
47        let cx = (x / self.cell_size).floor() as i32;
48        let cy = (y / self.cell_size).floor() as i32;
49        let mut result = Vec::new();
50        for dx in -r_cells..=r_cells {
51            for dy in -r_cells..=r_cells {
52                if let Some(ids) = self.cells.get(&(cx + dx, cy + dy)) {
53                    for &id in ids {
54                        let p = self.positions[id];
55                        let ddx = p[0] - x;
56                        let ddy = p[1] - y;
57                        if ddx * ddx + ddy * ddy <= radius * radius {
58                            result.push(id);
59                        }
60                    }
61                }
62            }
63        }
64        result
65    }
66
67    /// Query all points in a rectangle.
68    pub fn query_aabb(&self, min: [f32; 2], max: [f32; 2]) -> Vec<usize> {
69        let c_min = self.cell_key(min[0], min[1]);
70        let c_max = self.cell_key(max[0], max[1]);
71        let mut result = Vec::new();
72        for cx in c_min.0..=c_max.0 {
73            for cy in c_min.1..=c_max.1 {
74                if let Some(ids) = self.cells.get(&(cx, cy)) {
75                    for &id in ids {
76                        let p = self.positions[id];
77                        if p[0] >= min[0] && p[0] <= max[0] && p[1] >= min[1] && p[1] <= max[1] {
78                            result.push(id);
79                        }
80                    }
81                }
82            }
83        }
84        result
85    }
86
87    /// Number of inserted points.
88    pub fn point_count(&self) -> usize {
89        self.positions.len()
90    }
91
92    /// Number of non-empty cells.
93    pub fn cell_count(&self) -> usize {
94        self.cells.len()
95    }
96
97    /// Clear all data.
98    pub fn clear(&mut self) {
99        self.cells.clear();
100        self.positions.clear();
101    }
102
103    /// Get position by index.
104    pub fn get(&self, id: usize) -> Option<[f32; 2]> {
105        self.positions.get(id).copied()
106    }
107
108    /// Rebuild the grid from existing positions.
109    pub fn rebuild(&mut self) {
110        self.cells.clear();
111        for (id, pos) in self.positions.iter().enumerate() {
112            let key = self.cell_key(pos[0], pos[1]);
113            self.cells.entry(key).or_default().push(id);
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn insert_and_count() {
124        let mut h = SpatialHash2D::new(1.0);
125        h.insert([0.5, 0.5]);
126        h.insert([1.5, 1.5]);
127        assert_eq!(h.point_count(), 2);
128    }
129
130    #[test]
131    fn query_radius_finds_nearby() {
132        let mut h = SpatialHash2D::new(1.0);
133        let id0 = h.insert([0.0, 0.0]);
134        let _id1 = h.insert([10.0, 10.0]);
135        let results = h.query_radius(0.0, 0.0, 0.5);
136        assert!(results.contains(&id0));
137    }
138
139    #[test]
140    fn query_radius_excludes_far() {
141        let mut h = SpatialHash2D::new(1.0);
142        let _id0 = h.insert([0.0, 0.0]);
143        let id1 = h.insert([10.0, 10.0]);
144        let results = h.query_radius(0.0, 0.0, 0.5);
145        assert!(!results.contains(&id1));
146    }
147
148    #[test]
149    fn query_aabb_finds_inside() {
150        let mut h = SpatialHash2D::new(1.0);
151        let id = h.insert([1.5, 1.5]);
152        let results = h.query_aabb([0.0, 0.0], [2.0, 2.0]);
153        assert!(results.contains(&id));
154    }
155
156    #[test]
157    fn query_aabb_excludes_outside() {
158        let mut h = SpatialHash2D::new(1.0);
159        let _id = h.insert([5.0, 5.0]);
160        let results = h.query_aabb([0.0, 0.0], [2.0, 2.0]);
161        assert!(results.is_empty());
162    }
163
164    #[test]
165    fn cell_count_nonzero() {
166        let mut h = SpatialHash2D::new(1.0);
167        h.insert([0.0, 0.0]);
168        h.insert([5.0, 5.0]);
169        assert!(h.cell_count() >= 2);
170    }
171
172    #[test]
173    fn clear_resets_count() {
174        let mut h = SpatialHash2D::new(1.0);
175        h.insert([1.0, 1.0]);
176        h.clear();
177        assert_eq!(h.point_count(), 0);
178        assert_eq!(h.cell_count(), 0);
179    }
180
181    #[test]
182    fn get_by_id() {
183        let mut h = SpatialHash2D::new(1.0);
184        use std::f32::consts::PI;
185        let id = h.insert([PI, 2.71]);
186        let p = h.get(id).expect("should succeed");
187        assert!((p[0] - PI).abs() < 1e-5);
188    }
189
190    #[test]
191    fn rebuild_preserves_query() {
192        let mut h = SpatialHash2D::new(1.0);
193        let id = h.insert([0.5, 0.5]);
194        h.rebuild();
195        let results = h.query_radius(0.5, 0.5, 0.1);
196        assert!(results.contains(&id));
197    }
198
199    #[test]
200    fn all_in_radius_one() {
201        let mut h = SpatialHash2D::new(0.5);
202        let ids: Vec<usize> = (0..5).map(|i| h.insert([i as f32 * 0.1, 0.0])).collect();
203        let results = h.query_radius(0.2, 0.0, 0.5);
204        // should find all 5
205        for id in &ids {
206            assert!(results.contains(id), "missing id {id}");
207        }
208    }
209}