1use std::collections::{HashMap, HashSet};
4
5use crate::ids::EntityHandle;
6use crate::spatial::{Aabb3, Bounds, CellCoord3, GridSpec, Position3};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct CellOccupancy {
11 pub cell: CellCoord3,
13 pub entities: usize,
15}
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19pub enum CellQueryStrategy {
20 #[default]
22 Grid,
23 OccupiedCells,
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub struct CellQueryStats {
30 pub strategy: CellQueryStrategy,
32 pub grid_cells_probed: usize,
34 pub occupied_cells_scanned: usize,
36 pub matched_cells: usize,
38 pub candidate_handles: usize,
40}
41
42#[derive(Clone, Debug, Default)]
44pub struct CellQueryScratch {
45 seen: HashSet<EntityHandle>,
46 handles: Vec<EntityHandle>,
47 matching_cells: Vec<CellCoord3>,
48 stats: CellQueryStats,
49}
50
51impl CellQueryScratch {
52 pub fn clear(&mut self) {
54 self.seen.clear();
55 self.handles.clear();
56 self.matching_cells.clear();
57 self.stats = CellQueryStats::default();
58 }
59
60 pub fn handles(&self) -> &[EntityHandle] {
62 &self.handles
63 }
64
65 pub fn len(&self) -> usize {
67 self.handles.len()
68 }
69
70 pub fn is_empty(&self) -> bool {
72 self.handles.is_empty()
73 }
74
75 pub const fn stats(&self) -> CellQueryStats {
77 self.stats
78 }
79
80 pub fn handle_capacity(&self) -> usize {
82 self.handles.capacity()
83 }
84
85 pub fn dedup_capacity(&self) -> usize {
87 self.seen.capacity()
88 }
89
90 pub fn matching_cell_capacity(&self) -> usize {
92 self.matching_cells.capacity()
93 }
94}
95
96#[derive(Clone, Debug)]
98pub struct CellIndex {
99 grid: GridSpec,
100 cells: HashMap<CellCoord3, Vec<EntityHandle>>,
101 entity_cells: HashMap<EntityHandle, Vec<CellCoord3>>,
102}
103
104impl CellIndex {
105 pub fn new(grid: GridSpec) -> Self {
107 Self {
108 grid,
109 cells: HashMap::new(),
110 entity_cells: HashMap::new(),
111 }
112 }
113
114 pub const fn grid(&self) -> GridSpec {
116 self.grid
117 }
118
119 pub fn upsert(&mut self, handle: EntityHandle, position: Position3, bounds: Bounds) {
121 self.remove(handle);
122 let cells = self.grid.cells_for_bounds(position, bounds);
123 for cell in &cells {
124 self.cells.entry(*cell).or_default().push(handle);
125 }
126 self.entity_cells.insert(handle, cells);
127 }
128
129 pub fn remove(&mut self, handle: EntityHandle) -> bool {
131 let Some(cells) = self.entity_cells.remove(&handle) else {
132 return false;
133 };
134
135 for cell in cells {
136 let remove_cell = if let Some(handles) = self.cells.get_mut(&cell) {
137 handles.retain(|candidate| *candidate != handle);
138 handles.is_empty()
139 } else {
140 false
141 };
142 if remove_cell {
143 self.cells.remove(&cell);
144 }
145 }
146
147 true
148 }
149
150 pub fn query_aabb(&self, aabb: Aabb3) -> Vec<EntityHandle> {
152 let mut scratch = CellQueryScratch::default();
153 self.query_aabb_into(aabb, &mut scratch);
154 scratch.handles
155 }
156
157 pub fn query_aabb_into<'a>(
159 &self,
160 aabb: Aabb3,
161 scratch: &'a mut CellQueryScratch,
162 ) -> &'a [EntityHandle] {
163 scratch.clear();
164 let min = self.grid.cell_at(aabb.min);
165 let max = self.grid.cell_at(aabb.max);
166
167 let grid_cells = query_cell_volume(min, max);
168 if grid_cells <= self.cells.len() {
169 scratch.stats.strategy = CellQueryStrategy::Grid;
170 scratch.stats.grid_cells_probed = grid_cells;
171 for x in min.x..=max.x {
172 for y in min.y..=max.y {
173 for z in min.z..=max.z {
174 self.collect_cell(CellCoord3::new(x, y, z), scratch);
175 }
176 }
177 }
178 } else {
179 scratch.stats.strategy = CellQueryStrategy::OccupiedCells;
180 scratch.stats.occupied_cells_scanned = self.cells.len();
181 scratch.matching_cells.extend(
182 self.cells
183 .keys()
184 .copied()
185 .filter(|cell| cell_in_range(*cell, min, max)),
186 );
187 scratch.matching_cells.sort_unstable();
188 for index in 0..scratch.matching_cells.len() {
189 self.collect_cell(scratch.matching_cells[index], scratch);
190 }
191 }
192
193 scratch.stats.candidate_handles = scratch.handles.len();
194
195 scratch.handles()
196 }
197
198 pub fn query_sphere(&self, center: Position3, radius: f32) -> Vec<EntityHandle> {
200 self.query_aabb(Bounds::Sphere { radius }.to_aabb(center))
201 }
202
203 pub fn query_sphere_into<'a>(
205 &self,
206 center: Position3,
207 radius: f32,
208 scratch: &'a mut CellQueryScratch,
209 ) -> &'a [EntityHandle] {
210 self.query_aabb_into(Bounds::Sphere { radius }.to_aabb(center), scratch)
211 }
212
213 fn collect_cell(&self, cell: CellCoord3, scratch: &mut CellQueryScratch) {
214 if let Some(handles) = self.cells.get(&cell) {
215 scratch.stats.matched_cells = scratch.stats.matched_cells.saturating_add(1);
216 for handle in handles {
217 if scratch.seen.insert(*handle) {
218 scratch.handles.push(*handle);
219 }
220 }
221 }
222 }
223
224 pub fn handles_in_cell(&self, cell: CellCoord3) -> Vec<EntityHandle> {
226 self.cells.get(&cell).cloned().unwrap_or_default()
227 }
228
229 pub fn handles_in_cell_slice(&self, cell: CellCoord3) -> &[EntityHandle] {
231 self.cells.get(&cell).map_or(&[], Vec::as_slice)
232 }
233
234 pub fn cells_for_handle(&self, handle: EntityHandle) -> Option<&[CellCoord3]> {
236 self.entity_cells.get(&handle).map(Vec::as_slice)
237 }
238
239 pub fn entity_count(&self) -> usize {
241 self.entity_cells.len()
242 }
243
244 pub fn occupied_cell_count(&self) -> usize {
246 self.cells.len()
247 }
248
249 pub fn cell_occupancy(&self) -> Vec<CellOccupancy> {
251 let mut cells = self
252 .cells
253 .iter()
254 .map(|(cell, handles)| CellOccupancy {
255 cell: *cell,
256 entities: handles.len(),
257 })
258 .collect::<Vec<_>>();
259 cells.sort_by_key(|occupancy| occupancy.cell);
260 cells
261 }
262}
263
264fn query_cell_volume(min: CellCoord3, max: CellCoord3) -> usize {
265 fn axis_cells(min: i32, max: i32) -> usize {
266 if max < min {
267 return 0;
268 }
269 usize::try_from(i64::from(max) - i64::from(min) + 1).unwrap_or(usize::MAX)
270 }
271
272 axis_cells(min.x, max.x)
273 .saturating_mul(axis_cells(min.y, max.y))
274 .saturating_mul(axis_cells(min.z, max.z))
275}
276
277const fn cell_in_range(cell: CellCoord3, min: CellCoord3, max: CellCoord3) -> bool {
278 cell.x >= min.x
279 && cell.x <= max.x
280 && cell.y >= min.y
281 && cell.y <= max.y
282 && cell.z >= min.z
283 && cell.z <= max.z
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn index_exposes_handles_by_cell() {
292 let grid = GridSpec::new(10.0).expect("valid grid");
293 let mut index = CellIndex::new(grid);
294 let handle = EntityHandle::new(1, 0);
295 index.upsert(handle, Position3::new(1.0, 2.0, 3.0), Bounds::Point);
296 let cell = grid.cell_at(Position3::new(1.0, 2.0, 3.0));
297
298 assert_eq!(index.handles_in_cell(cell), vec![handle]);
299 assert_eq!(index.handles_in_cell_slice(cell), &[handle]);
300 assert!(
301 index
302 .handles_in_cell_slice(CellCoord3::new(99, 99, 99))
303 .is_empty()
304 );
305 assert_eq!(index.cells_for_handle(handle), Some([cell].as_slice()));
306 }
307
308 #[test]
309 fn scratch_query_deduplicates_and_reuses_storage() {
310 let grid = GridSpec::new(10.0).expect("valid grid");
311 let mut index = CellIndex::new(grid);
312 let handle = EntityHandle::new(1, 0);
313 index.upsert(
314 handle,
315 Position3::new(9.0, 0.0, 0.0),
316 Bounds::Sphere { radius: 2.0 },
317 );
318 let mut scratch = CellQueryScratch::default();
319
320 let first = index.query_aabb_into(
321 Bounds::Sphere { radius: 4.0 }.to_aabb(Position3::new(10.0, 0.0, 0.0)),
322 &mut scratch,
323 );
324 assert_eq!(first, &[handle]);
325 assert_eq!(scratch.len(), 1);
326 assert_eq!(scratch.stats().strategy, CellQueryStrategy::Grid);
327 assert_eq!(scratch.stats().candidate_handles, 1);
328 assert!(scratch.handle_capacity() >= 1);
329 assert!(scratch.dedup_capacity() >= 1);
330
331 let second = index.query_aabb_into(
332 Bounds::Point.to_aabb(Position3::new(100.0, 0.0, 0.0)),
333 &mut scratch,
334 );
335 assert!(second.is_empty());
336 assert!(scratch.is_empty());
337 }
338
339 #[test]
340 fn sparse_large_query_scans_occupied_cells_deterministically() {
341 let grid = GridSpec::new(10.0).expect("valid grid");
342 let mut index = CellIndex::new(grid);
343 let high = EntityHandle::new(2, 0);
344 let low = EntityHandle::new(1, 0);
345 index.upsert(high, Position3::new(95.0, 0.0, 0.0), Bounds::Point);
346 index.upsert(low, Position3::new(-95.0, 0.0, 0.0), Bounds::Point);
347 let mut scratch = CellQueryScratch::default();
348
349 let handles = index.query_aabb_into(
350 Aabb3::new(
351 Position3::new(-100.0, -100.0, -100.0),
352 Position3::new(100.0, 100.0, 100.0),
353 ),
354 &mut scratch,
355 );
356
357 assert_eq!(handles, &[low, high]);
358 assert_eq!(scratch.stats().strategy, CellQueryStrategy::OccupiedCells);
359 assert_eq!(scratch.stats().occupied_cells_scanned, 2);
360 assert_eq!(scratch.stats().matched_cells, 2);
361 assert_eq!(scratch.stats().candidate_handles, 2);
362 assert!(scratch.matching_cell_capacity() >= 2);
363 }
364}