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, PartialEq, Eq)]
21pub enum CellIndexUpdate {
22 Inserted,
24 Unchanged,
26 Relocated,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct CellIndexUpdateReport {
33 pub update: CellIndexUpdate,
35 pub retained_cells: usize,
37 pub removed_cells: usize,
39 pub inserted_cells: usize,
41 pub incremental_diff: bool,
43}
44
45#[derive(Clone, Debug, Default)]
47pub struct CellIndexUpdateScratch {
48 cells: Vec<CellCoord3>,
49}
50
51impl CellIndexUpdateScratch {
52 pub fn retained_cell_capacity(&self) -> usize {
54 self.cells.capacity()
55 }
56
57 pub fn reclaim_retained_capacity(&mut self) {
59 self.cells.clear();
60 self.cells.shrink_to_fit();
61 }
62}
63
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66pub enum CellQueryStrategy {
67 #[default]
69 Grid,
70 OccupiedCells,
72}
73
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub struct CellQueryStats {
77 pub strategy: CellQueryStrategy,
79 pub grid_cells_probed: usize,
81 pub occupied_cells_scanned: usize,
83 pub matched_cells: usize,
85 pub candidate_handles: usize,
87}
88
89#[derive(Clone, Debug, Default)]
91pub struct CellQueryScratch {
92 seen_dense: Vec<u64>,
93 seen_collisions: Vec<u32>,
94 seen_sparse: HashSet<EntityHandle>,
95 query_epoch: u32,
96 handles: Vec<EntityHandle>,
97 matching_cells: Vec<CellCoord3>,
98 stats: CellQueryStats,
99}
100
101impl CellQueryScratch {
102 pub fn clear(&mut self) {
104 self.begin_query();
105 self.handles.clear();
106 self.matching_cells.clear();
107 self.stats = CellQueryStats::default();
108 }
109
110 pub fn handles(&self) -> &[EntityHandle] {
112 &self.handles
113 }
114
115 pub fn len(&self) -> usize {
117 self.handles.len()
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.handles.is_empty()
123 }
124
125 pub const fn stats(&self) -> CellQueryStats {
127 self.stats
128 }
129
130 pub fn handle_capacity(&self) -> usize {
132 self.handles.capacity()
133 }
134
135 pub fn dedup_capacity(&self) -> usize {
137 self.seen_dense
138 .capacity()
139 .saturating_add(self.seen_collisions.capacity())
140 .saturating_add(self.seen_sparse.capacity())
141 }
142
143 pub fn matching_cell_capacity(&self) -> usize {
145 self.matching_cells.capacity()
146 }
147
148 fn begin_query(&mut self) {
149 self.query_epoch = self.query_epoch.wrapping_add(1);
150 if self.query_epoch == 0 {
151 self.seen_dense.fill(0);
152 self.seen_collisions.fill(0);
153 self.query_epoch = 1;
154 }
155 self.seen_sparse.clear();
156 }
157
158 fn insert_seen(&mut self, handle: EntityHandle) -> bool {
159 let Ok(index) = usize::try_from(handle.index()) else {
160 return self.seen_sparse.insert(handle);
161 };
162 if index >= MAX_DENSE_DEDUP_SLOTS {
163 return self.seen_sparse.insert(handle);
164 }
165 if index >= self.seen_dense.len() {
166 self.seen_dense.resize(index + 1, 0);
167 self.seen_collisions.resize(index + 1, 0);
168 }
169 let marker = (u64::from(self.query_epoch) << 32) | u64::from(handle.generation());
170 let previous = self.seen_dense[index];
171 let previous_epoch = u32::try_from(previous >> 32).expect("marker epoch fits u32");
172 if previous_epoch != self.query_epoch {
173 self.seen_dense[index] = marker;
174 true
175 } else if self.seen_collisions[index] == self.query_epoch {
176 self.seen_sparse.insert(handle)
177 } else if previous == marker {
178 false
179 } else {
180 self.seen_collisions[index] = self.query_epoch;
181 let previous_generation =
182 u32::try_from(previous & u64::from(u32::MAX)).expect("marker generation fits u32");
183 self.seen_sparse
184 .insert(EntityHandle::new(handle.index(), previous_generation));
185 self.seen_sparse.insert(handle)
186 }
187 }
188}
189
190#[derive(Clone, Debug)]
192enum CellMembership {
193 Point(CellCoord3),
194 Multiple(Vec<CellCoord3>),
195}
196
197impl CellMembership {
198 fn as_slice(&self) -> &[CellCoord3] {
199 match self {
200 Self::Point(cell) => std::slice::from_ref(cell),
201 Self::Multiple(cells) => cells,
202 }
203 }
204
205 fn matches_range(&self, min: CellCoord3, max: CellCoord3) -> bool {
206 cells_match_range(self.as_slice(), min, max)
207 }
208}
209
210#[derive(Clone, Debug)]
212pub struct CellIndex {
213 grid: GridSpec,
214 cells: HashMap<CellCoord3, Vec<EntityHandle>>,
215 entity_cells: HashMap<EntityHandle, CellMembership>,
216}
217
218impl CellIndex {
219 pub fn new(grid: GridSpec) -> Self {
221 Self::with_capacity(grid, 0, 0)
222 }
223
224 pub fn with_capacity(
226 grid: GridSpec,
227 entity_capacity: usize,
228 occupied_cell_capacity: usize,
229 ) -> Self {
230 Self {
231 grid,
232 cells: HashMap::with_capacity(occupied_cell_capacity),
233 entity_cells: HashMap::with_capacity(entity_capacity),
234 }
235 }
236
237 pub fn reserve(&mut self, additional_entities: usize, additional_cells: usize) {
239 self.entity_cells.reserve(additional_entities);
240 self.cells.reserve(additional_cells);
241 }
242
243 pub fn reclaim_retained_capacity(&mut self) {
245 for handles in self.cells.values_mut() {
246 handles.shrink_to_fit();
247 }
248 for membership in self.entity_cells.values_mut() {
249 if let CellMembership::Multiple(cells) = membership {
250 cells.shrink_to_fit();
251 }
252 }
253 self.cells.shrink_to_fit();
254 self.entity_cells.shrink_to_fit();
255 }
256
257 pub fn entity_capacity(&self) -> usize {
259 self.entity_cells.capacity()
260 }
261
262 pub fn occupied_cell_capacity(&self) -> usize {
264 self.cells.capacity()
265 }
266
267 pub const fn grid(&self) -> GridSpec {
269 self.grid
270 }
271
272 pub fn upsert(&mut self, handle: EntityHandle, position: Position3, bounds: Bounds) {
274 self.upsert_tracked(handle, position, bounds);
275 }
276
277 pub fn upsert_tracked(
279 &mut self,
280 handle: EntityHandle,
281 position: Position3,
282 bounds: Bounds,
283 ) -> CellIndexUpdate {
284 let mut scratch = CellIndexUpdateScratch::default();
285 self.upsert_with_scratch(handle, position, bounds, &mut scratch)
286 .update
287 }
288
289 pub fn upsert_with_scratch(
291 &mut self,
292 handle: EntityHandle,
293 position: Position3,
294 bounds: Bounds,
295 scratch: &mut CellIndexUpdateScratch,
296 ) -> CellIndexUpdateReport {
297 if bounds == Bounds::Point {
298 return self.upsert_point(handle, self.grid.cell_at(position), scratch);
299 }
300
301 let aabb = bounds.to_aabb(position);
302 let min = self.grid.cell_at(aabb.min);
303 let max = self.grid.cell_at(aabb.max);
304 if let Some(current) = self.entity_cells.get(&handle)
305 && current.matches_range(min, max)
306 {
307 return CellIndexUpdateReport {
308 update: CellIndexUpdate::Unchanged,
309 retained_cells: current.as_slice().len(),
310 removed_cells: 0,
311 inserted_cells: 0,
312 incremental_diff: false,
313 };
314 }
315
316 scratch.cells.clear();
317 extend_cell_range(&mut scratch.cells, min, max);
318 let Some(previous) = self.entity_cells.remove(&handle) else {
319 for cell in &scratch.cells {
320 self.cells.entry(*cell).or_default().push(handle);
321 }
322 let inserted_cells = scratch.cells.len();
323 self.entity_cells.insert(
324 handle,
325 CellMembership::Multiple(std::mem::take(&mut scratch.cells)),
326 );
327 return CellIndexUpdateReport {
328 update: CellIndexUpdate::Inserted,
329 retained_cells: 0,
330 removed_cells: 0,
331 inserted_cells,
332 incremental_diff: false,
333 };
334 };
335
336 let incremental_diff = matches!(&previous, CellMembership::Multiple(_));
337 let (retained_cells, removed_cells, inserted_cells) =
338 self.apply_membership_diff(handle, previous.as_slice(), &scratch.cells);
339 let new_cells = std::mem::take(&mut scratch.cells);
340 if let CellMembership::Multiple(previous_cells) = previous {
341 scratch.cells = previous_cells;
342 }
343 self.entity_cells
344 .insert(handle, CellMembership::Multiple(new_cells));
345 CellIndexUpdateReport {
346 update: CellIndexUpdate::Relocated,
347 retained_cells,
348 removed_cells,
349 inserted_cells,
350 incremental_diff,
351 }
352 }
353
354 fn upsert_point(
355 &mut self,
356 handle: EntityHandle,
357 cell: CellCoord3,
358 scratch: &mut CellIndexUpdateScratch,
359 ) -> CellIndexUpdateReport {
360 let Some(previous) = self.entity_cells.remove(&handle) else {
361 self.cells.entry(cell).or_default().push(handle);
362 self.entity_cells
363 .insert(handle, CellMembership::Point(cell));
364 return CellIndexUpdateReport {
365 update: CellIndexUpdate::Inserted,
366 retained_cells: 0,
367 removed_cells: 0,
368 inserted_cells: 1,
369 incremental_diff: false,
370 };
371 };
372 if matches!(&previous, CellMembership::Point(current) if *current == cell) {
373 self.entity_cells.insert(handle, previous);
374 return CellIndexUpdateReport {
375 update: CellIndexUpdate::Unchanged,
376 retained_cells: 1,
377 removed_cells: 0,
378 inserted_cells: 0,
379 incremental_diff: false,
380 };
381 }
382
383 let target = [cell];
384 let (retained_cells, removed_cells, inserted_cells) =
385 self.apply_membership_diff(handle, previous.as_slice(), &target);
386 if let CellMembership::Multiple(previous_cells) = previous {
387 scratch.cells = previous_cells;
388 }
389 self.entity_cells
390 .insert(handle, CellMembership::Point(cell));
391 CellIndexUpdateReport {
392 update: CellIndexUpdate::Relocated,
393 retained_cells,
394 removed_cells,
395 inserted_cells,
396 incremental_diff: false,
397 }
398 }
399
400 fn apply_membership_diff(
401 &mut self,
402 handle: EntityHandle,
403 previous: &[CellCoord3],
404 next: &[CellCoord3],
405 ) -> (usize, usize, usize) {
406 let (mut old_index, mut new_index) = (0, 0);
407 let (mut retained, mut removed, mut inserted) = (0, 0, 0);
408 while old_index < previous.len() || new_index < next.len() {
409 match (previous.get(old_index), next.get(new_index)) {
410 (Some(old), Some(new)) if old == new => {
411 retained += 1;
412 old_index += 1;
413 new_index += 1;
414 }
415 (Some(old), Some(new)) if old < new => {
416 self.remove_handle_from_cell(*old, handle);
417 removed += 1;
418 old_index += 1;
419 }
420 (_, Some(new)) => {
421 self.cells.entry(*new).or_default().push(handle);
422 inserted += 1;
423 new_index += 1;
424 }
425 (Some(old), None) => {
426 self.remove_handle_from_cell(*old, handle);
427 removed += 1;
428 old_index += 1;
429 }
430 (None, None) => break,
431 }
432 }
433 (retained, removed, inserted)
434 }
435
436 pub fn remove(&mut self, handle: EntityHandle) -> bool {
438 let Some(cells) = self.entity_cells.remove(&handle) else {
439 return false;
440 };
441
442 for cell in cells.as_slice() {
443 self.remove_handle_from_cell(*cell, handle);
444 }
445
446 true
447 }
448
449 fn remove_handle_from_cell(&mut self, cell: CellCoord3, handle: EntityHandle) {
450 let remove_cell = if let Some(handles) = self.cells.get_mut(&cell) {
451 if let Some(index) = handles.iter().position(|candidate| *candidate == handle) {
452 handles.remove(index);
453 }
454 handles.is_empty()
455 } else {
456 false
457 };
458 if remove_cell {
459 self.cells.remove(&cell);
460 }
461 }
462
463 pub fn query_aabb(&self, aabb: Aabb3) -> Vec<EntityHandle> {
465 let mut scratch = CellQueryScratch::default();
466 self.query_aabb_into(aabb, &mut scratch);
467 scratch.handles
468 }
469
470 pub fn query_aabb_into<'a>(
472 &self,
473 aabb: Aabb3,
474 scratch: &'a mut CellQueryScratch,
475 ) -> &'a [EntityHandle] {
476 scratch.clear();
477 let min = self.grid.cell_at(aabb.min);
478 let max = self.grid.cell_at(aabb.max);
479
480 let grid_cells = query_cell_volume(min, max);
481 if grid_cells <= self.cells.len() {
482 scratch.stats.strategy = CellQueryStrategy::Grid;
483 scratch.stats.grid_cells_probed = grid_cells;
484 for x in min.x..=max.x {
485 for y in min.y..=max.y {
486 for z in min.z..=max.z {
487 self.collect_cell(CellCoord3::new(x, y, z), scratch);
488 }
489 }
490 }
491 } else {
492 scratch.stats.strategy = CellQueryStrategy::OccupiedCells;
493 scratch.stats.occupied_cells_scanned = self.cells.len();
494 scratch.matching_cells.extend(
495 self.cells
496 .keys()
497 .copied()
498 .filter(|cell| cell_in_range(*cell, min, max)),
499 );
500 scratch.matching_cells.sort_unstable();
501 for index in 0..scratch.matching_cells.len() {
502 self.collect_cell(scratch.matching_cells[index], scratch);
503 }
504 }
505
506 scratch.stats.candidate_handles = scratch.handles.len();
507
508 scratch.handles()
509 }
510
511 pub fn query_sphere(&self, center: Position3, radius: f32) -> Vec<EntityHandle> {
513 self.query_aabb(Bounds::Sphere { radius }.to_aabb(center))
514 }
515
516 pub fn query_sphere_into<'a>(
518 &self,
519 center: Position3,
520 radius: f32,
521 scratch: &'a mut CellQueryScratch,
522 ) -> &'a [EntityHandle] {
523 self.query_aabb_into(Bounds::Sphere { radius }.to_aabb(center), scratch)
524 }
525
526 fn collect_cell(&self, cell: CellCoord3, scratch: &mut CellQueryScratch) {
527 if let Some(handles) = self.cells.get(&cell) {
528 scratch.stats.matched_cells = scratch.stats.matched_cells.saturating_add(1);
529 for handle in handles {
530 if scratch.insert_seen(*handle) {
531 scratch.handles.push(*handle);
532 }
533 }
534 }
535 }
536
537 pub fn handles_in_cell(&self, cell: CellCoord3) -> Vec<EntityHandle> {
539 self.cells.get(&cell).cloned().unwrap_or_default()
540 }
541
542 pub fn handles_in_cell_slice(&self, cell: CellCoord3) -> &[EntityHandle] {
544 self.cells.get(&cell).map_or(&[], Vec::as_slice)
545 }
546
547 pub fn cells_for_handle(&self, handle: EntityHandle) -> Option<&[CellCoord3]> {
549 self.entity_cells.get(&handle).map(CellMembership::as_slice)
550 }
551
552 pub fn entity_count(&self) -> usize {
554 self.entity_cells.len()
555 }
556
557 pub fn point_membership_count(&self) -> usize {
559 self.entity_cells
560 .values()
561 .filter(|membership| matches!(membership, CellMembership::Point(_)))
562 .count()
563 }
564
565 pub fn occupied_cell_count(&self) -> usize {
567 self.cells.len()
568 }
569
570 pub fn cell_occupancy(&self) -> Vec<CellOccupancy> {
572 let mut cells = Vec::with_capacity(self.cells.len());
573 self.cell_occupancy_into(&mut cells);
574 cells
575 }
576
577 pub fn cell_occupancy_into(&self, out: &mut Vec<CellOccupancy>) {
579 out.clear();
580 out.extend(self.cells.iter().map(|(cell, handles)| CellOccupancy {
581 cell: *cell,
582 entities: handles.len(),
583 }));
584 out.sort_by_key(|occupancy| occupancy.cell);
585 }
586}
587
588fn cells_match_range(cells: &[CellCoord3], min: CellCoord3, max: CellCoord3) -> bool {
589 if cells.len() != query_cell_volume(min, max) {
590 return false;
591 }
592 let mut cells = cells.iter();
593 for x in min.x..=max.x {
594 for y in min.y..=max.y {
595 for z in min.z..=max.z {
596 if cells.next() != Some(&CellCoord3::new(x, y, z)) {
597 return false;
598 }
599 }
600 }
601 }
602 cells.next().is_none()
603}
604
605fn extend_cell_range(cells: &mut Vec<CellCoord3>, min: CellCoord3, max: CellCoord3) {
606 cells.reserve(query_cell_volume(min, max));
607 for x in min.x..=max.x {
608 for y in min.y..=max.y {
609 for z in min.z..=max.z {
610 cells.push(CellCoord3::new(x, y, z));
611 }
612 }
613 }
614}
615
616fn query_cell_volume(min: CellCoord3, max: CellCoord3) -> usize {
617 fn axis_cells(min: i32, max: i32) -> usize {
618 if max < min {
619 return 0;
620 }
621 usize::try_from(i64::from(max) - i64::from(min) + 1).unwrap_or(usize::MAX)
622 }
623
624 axis_cells(min.x, max.x)
625 .saturating_mul(axis_cells(min.y, max.y))
626 .saturating_mul(axis_cells(min.z, max.z))
627}
628
629const fn cell_in_range(cell: CellCoord3, min: CellCoord3, max: CellCoord3) -> bool {
630 cell.x >= min.x
631 && cell.x <= max.x
632 && cell.y >= min.y
633 && cell.y <= max.y
634 && cell.z >= min.z
635 && cell.z <= max.z
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
643 fn explicit_index_capacity_is_retained_and_grows_on_request() {
644 let grid = GridSpec::new(10.0).expect("valid grid");
645 let mut index = CellIndex::with_capacity(grid, 8, 4);
646
647 assert!(index.entity_capacity() >= 8);
648 assert!(index.occupied_cell_capacity() >= 4);
649 index.reserve(32, 16);
650 assert!(index.entity_capacity() >= 32);
651 assert!(index.occupied_cell_capacity() >= 16);
652
653 let handle = EntityHandle::new(1, 0);
654 index.upsert(handle, Position3::new(1.0, 2.0, 3.0), Bounds::Point);
655 assert_eq!(
656 index.query_sphere(Position3::new(1.0, 2.0, 3.0), 1.0),
657 vec![handle]
658 );
659 }
660
661 #[test]
662 fn reclaim_retained_capacity_preserves_queries_and_membership() {
663 let grid = GridSpec::new(10.0).expect("valid grid");
664 let mut index = CellIndex::with_capacity(grid, 64, 64);
665 let handle = EntityHandle::new(1, 0);
666 let position = Position3::new(9.0, 0.0, 0.0);
667 index.upsert(handle, position, Bounds::Sphere { radius: 2.0 });
668 let expected = index.query_sphere(position, 4.0);
669
670 index.reclaim_retained_capacity();
671
672 assert_eq!(index.query_sphere(position, 4.0), expected);
673 assert!(
674 !index
675 .cells_for_handle(handle)
676 .expect("membership")
677 .is_empty()
678 );
679 }
680
681 #[test]
682 fn tracked_upsert_skips_same_cell_point_updates() {
683 let grid = GridSpec::new(10.0).expect("valid grid");
684 let mut index = CellIndex::with_capacity(grid, 1, 2);
685 let handle = EntityHandle::new(1, 0);
686 let first_cell = grid.cell_at(Position3::new(1.0, 2.0, 3.0));
687 let second_cell = grid.cell_at(Position3::new(11.0, 2.0, 3.0));
688
689 assert_eq!(
690 index.upsert_tracked(handle, Position3::new(1.0, 2.0, 3.0), Bounds::Point),
691 CellIndexUpdate::Inserted
692 );
693 let entity_capacity = index.entity_capacity();
694 let cell_capacity = index.occupied_cell_capacity();
695 assert_eq!(
696 index.upsert_tracked(handle, Position3::new(9.0, 2.0, 3.0), Bounds::Point),
697 CellIndexUpdate::Unchanged
698 );
699 assert_eq!(index.handles_in_cell_slice(first_cell), &[handle]);
700 assert_eq!(index.entity_capacity(), entity_capacity);
701 assert_eq!(index.occupied_cell_capacity(), cell_capacity);
702
703 assert!(matches!(
704 index.entity_cells.get(&handle),
705 Some(CellMembership::Point(cell)) if *cell == first_cell
706 ));
707 assert_eq!(
708 index.upsert_tracked(handle, Position3::new(11.0, 2.0, 3.0), Bounds::Point),
709 CellIndexUpdate::Relocated
710 );
711 assert!(matches!(
712 index.entity_cells.get(&handle),
713 Some(CellMembership::Point(cell)) if *cell == second_cell
714 ));
715 assert_eq!(index.point_membership_count(), 1);
716 assert!(index.handles_in_cell_slice(first_cell).is_empty());
717 assert_eq!(index.handles_in_cell_slice(second_cell), &[handle]);
718 }
719
720 #[test]
721 fn tracked_upsert_skips_unchanged_multi_cell_bounds() {
722 let grid = GridSpec::new(10.0).expect("valid grid");
723 let mut index = CellIndex::new(grid);
724 let handle = EntityHandle::new(1, 0);
725 let bounds = Bounds::Sphere { radius: 2.0 };
726
727 assert_eq!(
728 index.upsert_tracked(handle, Position3::new(9.0, 0.0, 0.0), bounds),
729 CellIndexUpdate::Inserted
730 );
731 let retained_cells = index
732 .entity_cells
733 .get(&handle)
734 .expect("bounded entity has cells")
735 .as_slice()
736 .as_ptr();
737 assert_eq!(
738 index.upsert_tracked(handle, Position3::new(9.5, 0.0, 0.0), bounds),
739 CellIndexUpdate::Unchanged
740 );
741 assert_eq!(
742 index
743 .entity_cells
744 .get(&handle)
745 .expect("unchanged bounds retain cells")
746 .as_slice()
747 .as_ptr(),
748 retained_cells
749 );
750
751 let relocated_position = Position3::new(12.5, 0.0, 0.0);
752 assert_eq!(
753 index.upsert_tracked(handle, relocated_position, bounds),
754 CellIndexUpdate::Relocated
755 );
756 assert_eq!(
757 index.cells_for_handle(handle),
758 Some(grid.cells_for_bounds(relocated_position, bounds).as_slice())
759 );
760 }
761
762 #[test]
763 fn multi_cell_crossing_updates_only_membership_difference() {
764 let grid = GridSpec::new(32.0).expect("valid grid");
765 let bounds = Bounds::Sphere { radius: 20.0 };
766 let handle = EntityHandle::new(1, 0);
767 let start = Position3::new(16.0, 16.0, 16.0);
768 let moved = Position3::new(48.0, 16.0, 16.0);
769 let mut index = CellIndex::new(grid);
770 let mut scratch = CellIndexUpdateScratch::default();
771
772 let inserted = index.upsert_with_scratch(handle, start, bounds, &mut scratch);
773 assert_eq!(inserted.update, CellIndexUpdate::Inserted);
774 assert_eq!(inserted.inserted_cells, 27);
775
776 let relocated = index.upsert_with_scratch(handle, moved, bounds, &mut scratch);
777 assert_eq!(
778 relocated,
779 CellIndexUpdateReport {
780 update: CellIndexUpdate::Relocated,
781 retained_cells: 18,
782 removed_cells: 9,
783 inserted_cells: 9,
784 incremental_diff: true,
785 }
786 );
787 assert!(scratch.retained_cell_capacity() >= 27);
788
789 let returned = index.upsert_with_scratch(handle, start, bounds, &mut scratch);
790 assert_eq!(returned.retained_cells, 18);
791 assert_eq!(returned.removed_cells, 9);
792 assert_eq!(returned.inserted_cells, 9);
793 assert!(returned.incremental_diff);
794
795 let mut rebuilt = CellIndex::new(grid);
796 rebuilt.upsert(handle, start, bounds);
797 assert_eq!(
798 index.cells_for_handle(handle),
799 rebuilt.cells_for_handle(handle)
800 );
801 assert_eq!(
802 index.query_sphere(start, 64.0),
803 rebuilt.query_sphere(start, 64.0)
804 );
805 }
806
807 #[test]
808 fn index_exposes_handles_by_cell() {
809 let grid = GridSpec::new(10.0).expect("valid grid");
810 let mut index = CellIndex::new(grid);
811 let handle = EntityHandle::new(1, 0);
812 index.upsert(handle, Position3::new(1.0, 2.0, 3.0), Bounds::Point);
813 let cell = grid.cell_at(Position3::new(1.0, 2.0, 3.0));
814
815 assert_eq!(index.handles_in_cell(cell), vec![handle]);
816 assert_eq!(index.handles_in_cell_slice(cell), &[handle]);
817 assert!(
818 index
819 .handles_in_cell_slice(CellCoord3::new(99, 99, 99))
820 .is_empty()
821 );
822 assert_eq!(index.cells_for_handle(handle), Some([cell].as_slice()));
823 }
824
825 #[test]
826 fn occupancy_output_is_sorted_and_reuses_capacity() {
827 let grid = GridSpec::new(10.0).expect("valid grid");
828 let mut index = CellIndex::new(grid);
829 let left = EntityHandle::new(1, 0);
830 let right = EntityHandle::new(2, 0);
831 index.upsert(right, Position3::new(21.0, 0.0, 0.0), Bounds::Point);
832 index.upsert(left, Position3::new(-11.0, 0.0, 0.0), Bounds::Point);
833
834 let expected = index.cell_occupancy();
835 let mut occupancy = Vec::new();
836 index.cell_occupancy_into(&mut occupancy);
837 assert_eq!(occupancy, expected);
838 assert!(
839 occupancy
840 .windows(2)
841 .all(|cells| cells[0].cell < cells[1].cell)
842 );
843
844 let retained = occupancy.as_ptr();
845 index.cell_occupancy_into(&mut occupancy);
846 assert_eq!(occupancy, expected);
847 assert_eq!(occupancy.as_ptr(), retained);
848 }
849
850 #[test]
851 fn scratch_query_deduplicates_and_reuses_storage() {
852 let grid = GridSpec::new(10.0).expect("valid grid");
853 let mut index = CellIndex::new(grid);
854 let handle = EntityHandle::new(1, 0);
855 index.upsert(
856 handle,
857 Position3::new(9.0, 0.0, 0.0),
858 Bounds::Sphere { radius: 2.0 },
859 );
860 let mut scratch = CellQueryScratch::default();
861
862 let first = index.query_aabb_into(
863 Bounds::Sphere { radius: 4.0 }.to_aabb(Position3::new(10.0, 0.0, 0.0)),
864 &mut scratch,
865 );
866 assert_eq!(first, &[handle]);
867 assert_eq!(scratch.len(), 1);
868 assert_eq!(scratch.stats().strategy, CellQueryStrategy::Grid);
869 assert_eq!(scratch.stats().candidate_handles, 1);
870 assert!(scratch.handle_capacity() >= 1);
871 assert!(scratch.dedup_capacity() >= 1);
872
873 let second = index.query_aabb_into(
874 Bounds::Point.to_aabb(Position3::new(100.0, 0.0, 0.0)),
875 &mut scratch,
876 );
877 assert!(second.is_empty());
878 assert!(scratch.is_empty());
879 }
880
881 #[test]
882 fn dense_dedup_preserves_generations_and_bounds_sparse_handles() {
883 let grid = GridSpec::new(10.0).expect("valid grid");
884 let mut index = CellIndex::new(grid);
885 let old = EntityHandle::new(7, 1);
886 let current = EntityHandle::new(7, 2);
887 let sparse = EntityHandle::new(u32::MAX, 0);
888 let spanning = Bounds::Sphere { radius: 2.0 };
889 index.upsert(old, Position3::new(9.0, 0.0, 0.0), spanning);
890 index.upsert(current, Position3::new(9.0, 0.0, 0.0), spanning);
891 index.upsert(sparse, Position3::new(9.0, 0.0, 0.0), Bounds::Point);
892 let mut scratch = CellQueryScratch::default();
893
894 let handles = index.query_aabb_into(
895 spanning.to_aabb(Position3::new(10.0, 0.0, 0.0)),
896 &mut scratch,
897 );
898
899 assert_eq!(handles, &[old, current, sparse]);
900 assert!(scratch.dedup_capacity() < MAX_DENSE_DEDUP_SLOTS);
901 }
902
903 #[test]
904 fn sparse_large_query_scans_occupied_cells_deterministically() {
905 let grid = GridSpec::new(10.0).expect("valid grid");
906 let mut index = CellIndex::new(grid);
907 let high = EntityHandle::new(2, 0);
908 let low = EntityHandle::new(1, 0);
909 index.upsert(high, Position3::new(95.0, 0.0, 0.0), Bounds::Point);
910 index.upsert(low, Position3::new(-95.0, 0.0, 0.0), Bounds::Point);
911 let mut scratch = CellQueryScratch::default();
912
913 let handles = index.query_aabb_into(
914 Aabb3::new(
915 Position3::new(-100.0, -100.0, -100.0),
916 Position3::new(100.0, 100.0, 100.0),
917 ),
918 &mut scratch,
919 );
920
921 assert_eq!(handles, &[low, high]);
922 assert_eq!(scratch.stats().strategy, CellQueryStrategy::OccupiedCells);
923 assert_eq!(scratch.stats().occupied_cells_scanned, 2);
924 assert_eq!(scratch.stats().matched_cells, 2);
925 assert_eq!(scratch.stats().candidate_handles, 2);
926 assert!(scratch.matching_cell_capacity() >= 2);
927 }
928}