1use crate::authoring::model::field::Extent;
5use crate::authoring::primitives::space::{Boundary, axis_delta};
6
7#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct HashGrid {
11 pub grid_w: u32,
13 pub grid_h: u32,
15 pub cell_w: f32,
17 pub cell_h: f32,
19}
20
21pub const MAX_INDEX_CELLS: u64 = 1 << 22;
27
28impl HashGrid {
29 pub fn new(extent: Extent, cell_size: f32) -> Self {
39 let cell_size = if cell_size > 0.0 { cell_size } else { 1.0 };
40 let mut grid_w = (extent.w / cell_size).floor().max(1.0) as u32;
41 let mut grid_h = (extent.h / cell_size).floor().max(1.0) as u32;
42
43 if u64::from(grid_w) * u64::from(grid_h) > MAX_INDEX_CELLS {
44 let scale = (f64::from(grid_w) * f64::from(grid_h) / MAX_INDEX_CELLS as f64).sqrt();
47 grid_w = ((f64::from(grid_w) / scale).floor() as u32).max(1);
48 grid_h = ((f64::from(grid_h) / scale).floor() as u32).max(1);
49 }
50
51 Self {
52 grid_w,
53 grid_h,
54 cell_w: extent.w / grid_w as f32,
55 cell_h: extent.h / grid_h as f32,
56 }
57 }
58
59 pub fn num_cells(&self) -> u32 {
61 self.grid_w * self.grid_h
62 }
63}
64
65pub struct SpatialHash {
67 cell_size: f32,
69 cell_w: f32,
71 cell_h: f32,
72 cell_w_inv: f32,
73 cell_h_inv: f32,
74 grid_w: u32,
75 grid_h: u32,
76 world_w: f32,
77 world_h: f32,
78 agent_cells: Vec<u32>,
80 sorted_agents: Vec<u32>,
82 cell_start: Vec<u32>,
84}
85
86impl std::fmt::Debug for SpatialHash {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("SpatialHash")
90 .field("cell_size", &self.cell_size)
91 .field("grid_w", &self.grid_w)
92 .field("grid_h", &self.grid_h)
93 .field("world_w", &self.world_w)
94 .field("world_h", &self.world_h)
95 .field("agent_count", &self.agent_cells.len())
96 .finish_non_exhaustive()
97 }
98}
99
100impl SpatialHash {
101 pub fn new(cell_size: f32, world_w: f32, world_h: f32) -> Self {
103 let grid = HashGrid::new(Extent { w: world_w, h: world_h }, cell_size);
106 let (grid_w, grid_h) = (grid.grid_w, grid.grid_h);
107 let (cell_w, cell_h) = (grid.cell_w, grid.cell_h);
108 let num_cells = grid_w * grid_h;
109
110 Self {
111 cell_size,
112 cell_w,
113 cell_h,
114 cell_w_inv: 1.0 / cell_w,
115 cell_h_inv: 1.0 / cell_h,
116 grid_w,
117 grid_h,
118 world_w,
119 world_h,
120 agent_cells: Vec::new(),
121 sorted_agents: Vec::new(),
122 cell_start: vec![0; num_cells as usize + 1],
123 }
124 }
125
126 #[inline]
130 pub fn cell_index(&self, x: f32, y: f32) -> u32 {
131 let cx = ((x * self.cell_w_inv).floor() as i32).rem_euclid(self.grid_w as i32) as u32;
132 let cy = ((y * self.cell_h_inv).floor() as i32).rem_euclid(self.grid_h as i32) as u32;
133 cy * self.grid_w + cx
134 }
135
136 pub fn build(&mut self, pos_x: &[f32], pos_y: &[f32]) {
138 self.build_where(pos_x, pos_y, |_| true);
139 }
140
141 pub fn build_where(&mut self, pos_x: &[f32], pos_y: &[f32], include: impl Fn(usize) -> bool) {
145 const NO_CELL: u32 = u32::MAX;
147
148 let num_agents = pos_x.len() as u32;
149 let num_cells = self.grid_w * self.grid_h;
150 self.agent_cells.clear();
151 self.sorted_agents.clear();
152 self.cell_start.clear();
153 self.agent_cells.reserve(num_agents as usize);
154 self.sorted_agents.resize(num_agents as usize, 0);
155 self.cell_start.resize((num_cells + 1) as usize, 0);
156
157 for i in 0..num_agents {
159 let cell = if include(i as usize) {
160 self.cell_index(pos_x[i as usize], pos_y[i as usize])
161 } else {
162 NO_CELL
163 };
164 self.agent_cells.push(cell);
165 if cell != NO_CELL {
166 self.cell_start[cell as usize + 1] += 1;
167 }
168 }
169
170 for i in 1..=num_cells {
172 self.cell_start[i as usize] += self.cell_start[i as usize - 1];
173 }
174
175 let mut write_pos = self.cell_start.clone();
177 for i in 0..num_agents {
178 let cell = self.agent_cells[i as usize];
179 if cell == NO_CELL {
180 continue;
181 }
182 let pos = write_pos[cell as usize];
183 self.sorted_agents[pos as usize] = i;
184 write_pos[cell as usize] += 1;
185 }
186 }
187
188 pub fn query_radius(&self, x: f32, y: f32, r: f32, pos_x: &[f32], pos_y: &[f32], result: &mut Vec<u32>) {
192 result.clear();
193 self.for_each_within(x, y, r, pos_x, pos_y, |agent_idx, _dx, _dy, _d2| {
194 result.push(agent_idx);
195 });
196 }
197
198 pub fn for_each_within<F: FnMut(u32, f32, f32, f32)>(
204 &self,
205 x: f32,
206 y: f32,
207 r: f32,
208 pos_x: &[f32],
209 pos_y: &[f32],
210 mut f: F,
211 ) {
212 let r2 = r * r;
213 let cell_radius_x = (r / self.cell_w).ceil() as i32;
214 let cell_radius_y = (r / self.cell_h).ceil() as i32;
215 let cell_x = ((x * self.cell_w_inv).floor() as i32).rem_euclid(self.grid_w as i32);
216 let cell_y = ((y * self.cell_h_inv).floor() as i32).rem_euclid(self.grid_h as i32);
217 let (y_lo, y_hi) = if 2 * cell_radius_y + 1 > self.grid_h as i32 {
219 (0, self.grid_h as i32 - 1)
220 } else {
221 (cell_y - cell_radius_y, cell_y + cell_radius_y)
222 };
223 let (x_lo, x_hi) = if 2 * cell_radius_x + 1 > self.grid_w as i32 {
224 (0, self.grid_w as i32 - 1)
225 } else {
226 (cell_x - cell_radius_x, cell_x + cell_radius_x)
227 };
228
229 for grid_y in y_lo..=y_hi {
230 let wrapped_y = grid_y.rem_euclid(self.grid_h as i32) as u32;
231 for grid_x in x_lo..=x_hi {
232 let wrapped_x = grid_x.rem_euclid(self.grid_w as i32) as u32;
233 let cell_index = wrapped_y * self.grid_w + wrapped_x;
234 let start = self.cell_start[cell_index as usize] as usize;
235 let end = self.cell_start[cell_index as usize + 1] as usize;
236 for &agent_idx in &self.sorted_agents[start..end] {
237 let dx = axis_delta(x, pos_x[agent_idx as usize], self.world_w, Boundary::Torus);
238 let dy = axis_delta(y, pos_y[agent_idx as usize], self.world_h, Boundary::Torus);
239 let d2 = dx * dx + dy * dy;
240 if d2 <= r2 {
241 f(agent_idx, dx, dy, d2);
242 }
243 }
244 }
245 }
246 }
247
248 pub fn cell_size_is(&self, cell_size: f32) -> bool {
250 (self.cell_size - cell_size).abs() <= f32::EPSILON
251 }
252
253 pub fn world_is(&self, world_w: f32, world_h: f32) -> bool {
255 (self.world_w - world_w).abs() <= f32::EPSILON && (self.world_h - world_h).abs() <= f32::EPSILON
256 }
257
258 pub fn grid_dims(&self) -> (u32, u32) {
260 (self.grid_w, self.grid_h)
261 }
262
263 pub fn cell_extents(&self) -> (f32, f32) {
265 (self.cell_w, self.cell_h)
266 }
267
268 pub fn buckets(&self) -> (&[u32], &[u32]) {
271 (&self.cell_start, &self.sorted_agents)
272 }
273
274 pub fn rebuild_with_cell_size(&mut self, new_cell_size: f32, pos_x: &[f32], pos_y: &[f32]) {
276 if (new_cell_size - self.cell_size).abs() > f32::EPSILON {
277 *self = Self::new(new_cell_size, self.world_w, self.world_h);
278 self.build(pos_x, pos_y);
279 }
280 }
281
282 pub fn heap_bytes(&self) -> usize {
284 self.agent_cells.capacity() * 4 + self.sorted_agents.capacity() * 4 + self.cell_start.capacity() * 4
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::authoring::primitives::rng::xorshift64;
292
293 #[test]
295 fn the_index_grid_is_capped() {
296 let extent = Extent {
297 w: 10_000.0,
298 h: 10_000.0,
299 };
300 let grid = HashGrid::new(extent, 1.0);
301 assert!(
302 u64::from(grid.grid_w) * u64::from(grid.grid_h) <= MAX_INDEX_CELLS,
303 "{}x{} is over the cap",
304 grid.grid_w,
305 grid.grid_h
306 );
307 assert!((grid.cell_w * grid.grid_w as f32 - extent.w).abs() < 1e-3);
309 assert!((grid.cell_h * grid.grid_h as f32 - extent.h).abs() < 1e-3);
310 }
311
312 #[test]
314 fn the_sort_and_the_shared_geometry_agree() {
315 for (w, h, cell) in [
316 (1000.0, 1000.0, 50.0),
317 (1000.0, 1000.0, 47.0),
318 (10_000.0, 10_000.0, 1.0),
319 ] {
320 let hash = SpatialHash::new(cell, w, h);
321 let grid = HashGrid::new(Extent { w, h }, cell);
322 assert_eq!(hash.grid_dims(), (grid.grid_w, grid.grid_h), "dims at cell {cell}");
323 assert_eq!(
324 hash.cell_extents(),
325 (grid.cell_w, grid.cell_h),
326 "extents at cell {cell}"
327 );
328 }
329 }
330
331 #[test]
332 fn build_and_query_finds_all_close_agents() {
333 let pos_x = vec![0.0, 1.0, -2.0, 50.0];
335 let pos_y = vec![0.0, 2.0, -1.0, 50.0];
336 let mut sh = SpatialHash::new(10.0, 100.0, 100.0);
337 sh.build(&pos_x, &pos_y);
338
339 let mut result = Vec::new();
340 sh.query_radius(0.0, 0.0, 5.0, &pos_x, &pos_y, &mut result);
341
342 result.sort();
343 assert_eq!(result, vec![0, 1, 2]);
344 }
345
346 #[test]
347 fn toroidal_query_finds_wrapped_agent() {
348 let pos_x = vec![1.0, 99.0];
350 let pos_y = vec![1.0, 99.0];
351 let mut sh = SpatialHash::new(10.0, 100.0, 100.0);
352 sh.build(&pos_x, &pos_y);
353
354 let mut result = Vec::new();
355 sh.query_radius(1.0, 1.0, 5.0, &pos_x, &pos_y, &mut result);
356
357 result.sort();
358 assert_eq!(result, vec![0, 1]);
359 }
360
361 fn axis_distance(a: f32, b: f32, world: f32) -> f32 {
363 let d = (a - b).abs();
364 d.min(world - d)
365 }
366
367 #[test]
368 fn matches_brute_force_with_non_divisor_cell_size() {
369 let (world_w, world_h, r) = (1_000.0_f32, 730.0_f32, 47.0_f32);
371 let mut seed = 0x1234_5678_9ABC_DEF0_u64;
372 let mut unit = || {
373 seed = xorshift64(seed);
374 (seed >> 40) as f32 / 16_777_216.0
375 };
376
377 let mut pos_x = Vec::new();
378 let mut pos_y = Vec::new();
379 for _ in 0..500 {
380 pos_x.push(unit() * world_w);
381 pos_y.push(unit() * world_h);
382 }
383
384 let mut sh = SpatialHash::new(r, world_w, world_h);
385 sh.build(&pos_x, &pos_y);
386
387 let mut result = Vec::new();
388 for i in 0..pos_x.len() {
389 sh.query_radius(pos_x[i], pos_y[i], r, &pos_x, &pos_y, &mut result);
390 result.sort();
391
392 let mut expected: Vec<u32> = (0..pos_x.len() as u32)
393 .filter(|&j| {
394 let dx = axis_distance(pos_x[j as usize], pos_x[i], world_w);
395 let dy = axis_distance(pos_y[j as usize], pos_y[i], world_h);
396 dx * dx + dy * dy <= r * r
397 })
398 .collect();
399 expected.sort();
400
401 assert_eq!(result, expected, "neighbors of agent {i} disagree with brute force");
402 }
403 }
404
405 #[test]
406 fn query_wider_than_grid_returns_each_agent_once() {
407 let pos_x = vec![0.0, 60.0, 150.0];
409 let pos_y = vec![0.0, 0.0, 0.0];
410 let mut sh = SpatialHash::new(100.0, 300.0, 300.0);
411 sh.build(&pos_x, &pos_y);
412
413 let mut result = Vec::new();
414 sh.query_radius(0.0, 0.0, 100.0, &pos_x, &pos_y, &mut result);
415
416 result.sort();
417 assert_eq!(result, vec![0, 1], "agent 2 is 150 away, and nothing may repeat");
418 }
419
420 #[test]
421 fn single_cell_grid_returns_each_agent_once() {
422 let pos_x = vec![10.0, 20.0, 60.0];
424 let pos_y = vec![10.0, 20.0, 60.0];
425 let mut sh = SpatialHash::new(200.0, 100.0, 100.0);
426 sh.build(&pos_x, &pos_y);
427
428 let mut result = Vec::new();
429 sh.query_radius(10.0, 10.0, 200.0, &pos_x, &pos_y, &mut result);
430
431 result.sort();
432 assert_eq!(result, vec![0, 1, 2], "one cell means one visit per agent");
433 }
434}