oxihuman_core/
spatial_hash_2d.rs1#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10#[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 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 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 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 pub fn point_count(&self) -> usize {
89 self.positions.len()
90 }
91
92 pub fn cell_count(&self) -> usize {
94 self.cells.len()
95 }
96
97 pub fn clear(&mut self) {
99 self.cells.clear();
100 self.positions.clear();
101 }
102
103 pub fn get(&self, id: usize) -> Option<[f32; 2]> {
105 self.positions.get(id).copied()
106 }
107
108 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 for id in &ids {
206 assert!(results.contains(id), "missing id {id}");
207 }
208 }
209}