1use std::collections::{HashMap, HashSet};
4
5use crate::ids::EntityHandle;
6use crate::spatial::{Aabb3, Bounds, CellCoord3, GridSpec, Position3};
7
8const MAX_DENSE_DEDUP_SLOTS: usize = 262_144;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct CellOccupancy {
13 pub cell: CellCoord3,
15 pub entities: usize,
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub enum CellQueryStrategy {
22 #[default]
24 Grid,
25 OccupiedCells,
27}
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct CellQueryStats {
32 pub strategy: CellQueryStrategy,
34 pub grid_cells_probed: usize,
36 pub occupied_cells_scanned: usize,
38 pub matched_cells: usize,
40 pub candidate_handles: usize,
42}
43
44#[derive(Clone, Debug, Default)]
46pub struct CellQueryScratch {
47 seen_dense: Vec<u64>,
48 seen_collisions: Vec<u32>,
49 seen_sparse: HashSet<EntityHandle>,
50 query_epoch: u32,
51 handles: Vec<EntityHandle>,
52 matching_cells: Vec<CellCoord3>,
53 stats: CellQueryStats,
54}
55
56impl CellQueryScratch {
57 pub fn clear(&mut self) {
59 self.begin_query();
60 self.handles.clear();
61 self.matching_cells.clear();
62 self.stats = CellQueryStats::default();
63 }
64
65 pub fn handles(&self) -> &[EntityHandle] {
67 &self.handles
68 }
69
70 pub fn len(&self) -> usize {
72 self.handles.len()
73 }
74
75 pub fn is_empty(&self) -> bool {
77 self.handles.is_empty()
78 }
79
80 pub const fn stats(&self) -> CellQueryStats {
82 self.stats
83 }
84
85 pub fn handle_capacity(&self) -> usize {
87 self.handles.capacity()
88 }
89
90 pub fn dedup_capacity(&self) -> usize {
92 self.seen_dense
93 .capacity()
94 .saturating_add(self.seen_collisions.capacity())
95 .saturating_add(self.seen_sparse.capacity())
96 }
97
98 pub fn matching_cell_capacity(&self) -> usize {
100 self.matching_cells.capacity()
101 }
102
103 fn begin_query(&mut self) {
104 self.query_epoch = self.query_epoch.wrapping_add(1);
105 if self.query_epoch == 0 {
106 self.seen_dense.fill(0);
107 self.seen_collisions.fill(0);
108 self.query_epoch = 1;
109 }
110 self.seen_sparse.clear();
111 }
112
113 fn insert_seen(&mut self, handle: EntityHandle) -> bool {
114 let Ok(index) = usize::try_from(handle.index()) else {
115 return self.seen_sparse.insert(handle);
116 };
117 if index >= MAX_DENSE_DEDUP_SLOTS {
118 return self.seen_sparse.insert(handle);
119 }
120 if index >= self.seen_dense.len() {
121 self.seen_dense.resize(index + 1, 0);
122 self.seen_collisions.resize(index + 1, 0);
123 }
124 let marker = (u64::from(self.query_epoch) << 32) | u64::from(handle.generation());
125 let previous = self.seen_dense[index];
126 let previous_epoch = u32::try_from(previous >> 32).expect("marker epoch fits u32");
127 if previous_epoch != self.query_epoch {
128 self.seen_dense[index] = marker;
129 true
130 } else if self.seen_collisions[index] == self.query_epoch {
131 self.seen_sparse.insert(handle)
132 } else if previous == marker {
133 false
134 } else {
135 self.seen_collisions[index] = self.query_epoch;
136 let previous_generation =
137 u32::try_from(previous & u64::from(u32::MAX)).expect("marker generation fits u32");
138 self.seen_sparse
139 .insert(EntityHandle::new(handle.index(), previous_generation));
140 self.seen_sparse.insert(handle)
141 }
142 }
143}
144
145#[derive(Clone, Debug)]
147pub struct CellIndex {
148 grid: GridSpec,
149 cells: HashMap<CellCoord3, Vec<EntityHandle>>,
150 entity_cells: HashMap<EntityHandle, Vec<CellCoord3>>,
151}
152
153impl CellIndex {
154 pub fn new(grid: GridSpec) -> Self {
156 Self {
157 grid,
158 cells: HashMap::new(),
159 entity_cells: HashMap::new(),
160 }
161 }
162
163 pub const fn grid(&self) -> GridSpec {
165 self.grid
166 }
167
168 pub fn upsert(&mut self, handle: EntityHandle, position: Position3, bounds: Bounds) {
170 self.remove(handle);
171 let cells = self.grid.cells_for_bounds(position, bounds);
172 for cell in &cells {
173 self.cells.entry(*cell).or_default().push(handle);
174 }
175 self.entity_cells.insert(handle, cells);
176 }
177
178 pub fn remove(&mut self, handle: EntityHandle) -> bool {
180 let Some(cells) = self.entity_cells.remove(&handle) else {
181 return false;
182 };
183
184 for cell in cells {
185 let remove_cell = if let Some(handles) = self.cells.get_mut(&cell) {
186 handles.retain(|candidate| *candidate != handle);
187 handles.is_empty()
188 } else {
189 false
190 };
191 if remove_cell {
192 self.cells.remove(&cell);
193 }
194 }
195
196 true
197 }
198
199 pub fn query_aabb(&self, aabb: Aabb3) -> Vec<EntityHandle> {
201 let mut scratch = CellQueryScratch::default();
202 self.query_aabb_into(aabb, &mut scratch);
203 scratch.handles
204 }
205
206 pub fn query_aabb_into<'a>(
208 &self,
209 aabb: Aabb3,
210 scratch: &'a mut CellQueryScratch,
211 ) -> &'a [EntityHandle] {
212 scratch.clear();
213 let min = self.grid.cell_at(aabb.min);
214 let max = self.grid.cell_at(aabb.max);
215
216 let grid_cells = query_cell_volume(min, max);
217 if grid_cells <= self.cells.len() {
218 scratch.stats.strategy = CellQueryStrategy::Grid;
219 scratch.stats.grid_cells_probed = grid_cells;
220 for x in min.x..=max.x {
221 for y in min.y..=max.y {
222 for z in min.z..=max.z {
223 self.collect_cell(CellCoord3::new(x, y, z), scratch);
224 }
225 }
226 }
227 } else {
228 scratch.stats.strategy = CellQueryStrategy::OccupiedCells;
229 scratch.stats.occupied_cells_scanned = self.cells.len();
230 scratch.matching_cells.extend(
231 self.cells
232 .keys()
233 .copied()
234 .filter(|cell| cell_in_range(*cell, min, max)),
235 );
236 scratch.matching_cells.sort_unstable();
237 for index in 0..scratch.matching_cells.len() {
238 self.collect_cell(scratch.matching_cells[index], scratch);
239 }
240 }
241
242 scratch.stats.candidate_handles = scratch.handles.len();
243
244 scratch.handles()
245 }
246
247 pub fn query_sphere(&self, center: Position3, radius: f32) -> Vec<EntityHandle> {
249 self.query_aabb(Bounds::Sphere { radius }.to_aabb(center))
250 }
251
252 pub fn query_sphere_into<'a>(
254 &self,
255 center: Position3,
256 radius: f32,
257 scratch: &'a mut CellQueryScratch,
258 ) -> &'a [EntityHandle] {
259 self.query_aabb_into(Bounds::Sphere { radius }.to_aabb(center), scratch)
260 }
261
262 fn collect_cell(&self, cell: CellCoord3, scratch: &mut CellQueryScratch) {
263 if let Some(handles) = self.cells.get(&cell) {
264 scratch.stats.matched_cells = scratch.stats.matched_cells.saturating_add(1);
265 for handle in handles {
266 if scratch.insert_seen(*handle) {
267 scratch.handles.push(*handle);
268 }
269 }
270 }
271 }
272
273 pub fn handles_in_cell(&self, cell: CellCoord3) -> Vec<EntityHandle> {
275 self.cells.get(&cell).cloned().unwrap_or_default()
276 }
277
278 pub fn handles_in_cell_slice(&self, cell: CellCoord3) -> &[EntityHandle] {
280 self.cells.get(&cell).map_or(&[], Vec::as_slice)
281 }
282
283 pub fn cells_for_handle(&self, handle: EntityHandle) -> Option<&[CellCoord3]> {
285 self.entity_cells.get(&handle).map(Vec::as_slice)
286 }
287
288 pub fn entity_count(&self) -> usize {
290 self.entity_cells.len()
291 }
292
293 pub fn occupied_cell_count(&self) -> usize {
295 self.cells.len()
296 }
297
298 pub fn cell_occupancy(&self) -> Vec<CellOccupancy> {
300 let mut cells = self
301 .cells
302 .iter()
303 .map(|(cell, handles)| CellOccupancy {
304 cell: *cell,
305 entities: handles.len(),
306 })
307 .collect::<Vec<_>>();
308 cells.sort_by_key(|occupancy| occupancy.cell);
309 cells
310 }
311}
312
313fn query_cell_volume(min: CellCoord3, max: CellCoord3) -> usize {
314 fn axis_cells(min: i32, max: i32) -> usize {
315 if max < min {
316 return 0;
317 }
318 usize::try_from(i64::from(max) - i64::from(min) + 1).unwrap_or(usize::MAX)
319 }
320
321 axis_cells(min.x, max.x)
322 .saturating_mul(axis_cells(min.y, max.y))
323 .saturating_mul(axis_cells(min.z, max.z))
324}
325
326const fn cell_in_range(cell: CellCoord3, min: CellCoord3, max: CellCoord3) -> bool {
327 cell.x >= min.x
328 && cell.x <= max.x
329 && cell.y >= min.y
330 && cell.y <= max.y
331 && cell.z >= min.z
332 && cell.z <= max.z
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn index_exposes_handles_by_cell() {
341 let grid = GridSpec::new(10.0).expect("valid grid");
342 let mut index = CellIndex::new(grid);
343 let handle = EntityHandle::new(1, 0);
344 index.upsert(handle, Position3::new(1.0, 2.0, 3.0), Bounds::Point);
345 let cell = grid.cell_at(Position3::new(1.0, 2.0, 3.0));
346
347 assert_eq!(index.handles_in_cell(cell), vec![handle]);
348 assert_eq!(index.handles_in_cell_slice(cell), &[handle]);
349 assert!(
350 index
351 .handles_in_cell_slice(CellCoord3::new(99, 99, 99))
352 .is_empty()
353 );
354 assert_eq!(index.cells_for_handle(handle), Some([cell].as_slice()));
355 }
356
357 #[test]
358 fn scratch_query_deduplicates_and_reuses_storage() {
359 let grid = GridSpec::new(10.0).expect("valid grid");
360 let mut index = CellIndex::new(grid);
361 let handle = EntityHandle::new(1, 0);
362 index.upsert(
363 handle,
364 Position3::new(9.0, 0.0, 0.0),
365 Bounds::Sphere { radius: 2.0 },
366 );
367 let mut scratch = CellQueryScratch::default();
368
369 let first = index.query_aabb_into(
370 Bounds::Sphere { radius: 4.0 }.to_aabb(Position3::new(10.0, 0.0, 0.0)),
371 &mut scratch,
372 );
373 assert_eq!(first, &[handle]);
374 assert_eq!(scratch.len(), 1);
375 assert_eq!(scratch.stats().strategy, CellQueryStrategy::Grid);
376 assert_eq!(scratch.stats().candidate_handles, 1);
377 assert!(scratch.handle_capacity() >= 1);
378 assert!(scratch.dedup_capacity() >= 1);
379
380 let second = index.query_aabb_into(
381 Bounds::Point.to_aabb(Position3::new(100.0, 0.0, 0.0)),
382 &mut scratch,
383 );
384 assert!(second.is_empty());
385 assert!(scratch.is_empty());
386 }
387
388 #[test]
389 fn dense_dedup_preserves_generations_and_bounds_sparse_handles() {
390 let grid = GridSpec::new(10.0).expect("valid grid");
391 let mut index = CellIndex::new(grid);
392 let old = EntityHandle::new(7, 1);
393 let current = EntityHandle::new(7, 2);
394 let sparse = EntityHandle::new(u32::MAX, 0);
395 let spanning = Bounds::Sphere { radius: 2.0 };
396 index.upsert(old, Position3::new(9.0, 0.0, 0.0), spanning);
397 index.upsert(current, Position3::new(9.0, 0.0, 0.0), spanning);
398 index.upsert(sparse, Position3::new(9.0, 0.0, 0.0), Bounds::Point);
399 let mut scratch = CellQueryScratch::default();
400
401 let handles = index.query_aabb_into(
402 spanning.to_aabb(Position3::new(10.0, 0.0, 0.0)),
403 &mut scratch,
404 );
405
406 assert_eq!(handles, &[old, current, sparse]);
407 assert!(scratch.dedup_capacity() < MAX_DENSE_DEDUP_SLOTS);
408 }
409
410 #[test]
411 fn sparse_large_query_scans_occupied_cells_deterministically() {
412 let grid = GridSpec::new(10.0).expect("valid grid");
413 let mut index = CellIndex::new(grid);
414 let high = EntityHandle::new(2, 0);
415 let low = EntityHandle::new(1, 0);
416 index.upsert(high, Position3::new(95.0, 0.0, 0.0), Bounds::Point);
417 index.upsert(low, Position3::new(-95.0, 0.0, 0.0), Bounds::Point);
418 let mut scratch = CellQueryScratch::default();
419
420 let handles = index.query_aabb_into(
421 Aabb3::new(
422 Position3::new(-100.0, -100.0, -100.0),
423 Position3::new(100.0, 100.0, 100.0),
424 ),
425 &mut scratch,
426 );
427
428 assert_eq!(handles, &[low, high]);
429 assert_eq!(scratch.stats().strategy, CellQueryStrategy::OccupiedCells);
430 assert_eq!(scratch.stats().occupied_cells_scanned, 2);
431 assert_eq!(scratch.stats().matched_cells, 2);
432 assert_eq!(scratch.stats().candidate_handles, 2);
433 assert!(scratch.matching_cell_capacity() >= 2);
434 }
435}