1#![allow(clippy::needless_range_loop, clippy::type_complexity)]
2#![allow(dead_code)]
9
10use std::collections::{BinaryHeap, HashMap};
11
12fn dist_sq(a: [f64; 3], b: [f64; 3]) -> f64 {
15 (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)
16}
17
18pub struct SpatialHash3D<T: Clone> {
25 pub cell_size: f64,
27 cells: HashMap<(i32, i32, i32), Vec<(usize, T)>>,
28 pub n_items: usize,
30}
31
32impl<T: Clone> SpatialHash3D<T> {
33 pub fn new(cell_size: f64) -> Self {
35 Self {
36 cell_size,
37 cells: HashMap::new(),
38 n_items: 0,
39 }
40 }
41
42 pub fn cell_coord(&self, x: f64) -> i32 {
44 (x / self.cell_size).floor() as i32
45 }
46
47 pub fn cell_key(&self, pos: [f64; 3]) -> (i32, i32, i32) {
49 (
50 self.cell_coord(pos[0]),
51 self.cell_coord(pos[1]),
52 self.cell_coord(pos[2]),
53 )
54 }
55
56 pub fn insert(&mut self, id: usize, pos: [f64; 3], data: T) -> (i32, i32, i32) {
58 let key = self.cell_key(pos);
59 self.cells.entry(key).or_default().push((id, data));
60 self.n_items += 1;
61 key
62 }
63
64 pub fn remove(&mut self, id: usize, pos: [f64; 3]) {
66 let key = self.cell_key(pos);
67 if let Some(bucket) = self.cells.get_mut(&key) {
68 let before = bucket.len();
69 bucket.retain(|(item_id, _)| *item_id != id);
70 let removed = before - bucket.len();
71 self.n_items = self.n_items.saturating_sub(removed);
72 if bucket.is_empty() {
73 self.cells.remove(&key);
74 }
75 }
76 }
77
78 pub fn query_radius(&self, center: [f64; 3], radius: f64) -> Vec<(usize, &T)> {
82 let r_cells = (radius / self.cell_size).ceil() as i32;
83 let cx = self.cell_coord(center[0]);
84 let cy = self.cell_coord(center[1]);
85 let cz = self.cell_coord(center[2]);
86
87 let mut result = Vec::new();
88 for dx in -r_cells..=r_cells {
89 for dy in -r_cells..=r_cells {
90 for dz in -r_cells..=r_cells {
91 let key = (cx + dx, cy + dy, cz + dz);
92 if let Some(bucket) = self.cells.get(&key) {
93 for (id, data) in bucket {
94 result.push((*id, data));
95 }
96 }
97 }
98 }
99 }
100 result
101 }
102
103 pub fn query_cell(&self, key: (i32, i32, i32)) -> Vec<(usize, &T)> {
105 match self.cells.get(&key) {
106 Some(bucket) => bucket.iter().map(|(id, data)| (*id, data)).collect(),
107 None => Vec::new(),
108 }
109 }
110
111 pub fn clear(&mut self) {
113 self.cells.clear();
114 self.n_items = 0;
115 }
116
117 pub fn len(&self) -> usize {
119 self.n_items
120 }
121
122 pub fn is_empty(&self) -> bool {
124 self.n_items == 0
125 }
126
127 pub fn n_cells(&self) -> usize {
129 self.cells.len()
130 }
131}
132
133pub struct SpatialHashPos3D<T: Clone> {
137 pub cell_size: f64,
139 cells: HashMap<(i32, i32, i32), Vec<(usize, [f64; 3], T)>>,
140 pub n_items: usize,
142}
143
144impl<T: Clone> SpatialHashPos3D<T> {
145 pub fn new(cell_size: f64) -> Self {
147 Self {
148 cell_size,
149 cells: HashMap::new(),
150 n_items: 0,
151 }
152 }
153
154 pub fn cell_coord(&self, x: f64) -> i32 {
156 (x / self.cell_size).floor() as i32
157 }
158
159 pub fn cell_key(&self, pos: [f64; 3]) -> (i32, i32, i32) {
161 (
162 self.cell_coord(pos[0]),
163 self.cell_coord(pos[1]),
164 self.cell_coord(pos[2]),
165 )
166 }
167
168 pub fn insert(&mut self, id: usize, pos: [f64; 3], data: T) -> (i32, i32, i32) {
170 let key = self.cell_key(pos);
171 self.cells.entry(key).or_default().push((id, pos, data));
172 self.n_items += 1;
173 key
174 }
175
176 pub fn remove(&mut self, id: usize, pos: [f64; 3]) {
178 let key = self.cell_key(pos);
179 if let Some(bucket) = self.cells.get_mut(&key) {
180 let before = bucket.len();
181 bucket.retain(|(item_id, _, _)| *item_id != id);
182 let removed = before - bucket.len();
183 self.n_items = self.n_items.saturating_sub(removed);
184 if bucket.is_empty() {
185 self.cells.remove(&key);
186 }
187 }
188 }
189
190 pub fn query_radius(&self, center: [f64; 3], radius: f64) -> Vec<(usize, &T)> {
192 let r_cells = (radius / self.cell_size).ceil() as i32;
193 let cx = self.cell_coord(center[0]);
194 let cy = self.cell_coord(center[1]);
195 let cz = self.cell_coord(center[2]);
196 let r2 = radius * radius;
197 let mut result = Vec::new();
198
199 for dx in -r_cells..=r_cells {
200 for dy in -r_cells..=r_cells {
201 for dz in -r_cells..=r_cells {
202 let key = (cx + dx, cy + dy, cz + dz);
203 if let Some(bucket) = self.cells.get(&key) {
204 for (id, pos, data) in bucket {
205 let d2 = dist_sq(*pos, center);
206 if d2 <= r2 {
207 result.push((*id, data));
208 }
209 }
210 }
211 }
212 }
213 }
214 result
215 }
216
217 pub fn query_aabb(&self, min: [f64; 3], max: [f64; 3]) -> Vec<(usize, &T)> {
219 let cmin = [
220 self.cell_coord(min[0]),
221 self.cell_coord(min[1]),
222 self.cell_coord(min[2]),
223 ];
224 let cmax = [
225 self.cell_coord(max[0]),
226 self.cell_coord(max[1]),
227 self.cell_coord(max[2]),
228 ];
229 let mut result = Vec::new();
230 for cx in cmin[0]..=cmax[0] {
231 for cy in cmin[1]..=cmax[1] {
232 for cz in cmin[2]..=cmax[2] {
233 if let Some(bucket) = self.cells.get(&(cx, cy, cz)) {
234 for (id, pos, data) in bucket {
235 if pos[0] >= min[0]
236 && pos[0] <= max[0]
237 && pos[1] >= min[1]
238 && pos[1] <= max[1]
239 && pos[2] >= min[2]
240 && pos[2] <= max[2]
241 {
242 result.push((*id, data));
243 }
244 }
245 }
246 }
247 }
248 }
249 result
250 }
251
252 pub fn k_nearest(&self, center: [f64; 3], k: usize) -> Vec<(usize, f64, &T)> {
258 if k == 0 || self.n_items == 0 {
259 return Vec::new();
260 }
261
262 let mut all: Vec<(usize, f64, &T)> = Vec::new();
264 for bucket in self.cells.values() {
265 for (id, pos, data) in bucket {
266 let d2 = dist_sq(*pos, center);
267 all.push((*id, d2, data));
268 }
269 }
270 all.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
271 all.truncate(k);
272 all.iter()
274 .map(|(id, d2, data)| (*id, d2.sqrt(), *data))
275 .collect()
276 }
277
278 pub fn query_cell(&self, key: (i32, i32, i32)) -> Vec<(usize, &T)> {
280 match self.cells.get(&key) {
281 Some(bucket) => bucket.iter().map(|(id, _, data)| (*id, data)).collect(),
282 None => Vec::new(),
283 }
284 }
285
286 pub fn clear(&mut self) {
288 self.cells.clear();
289 self.n_items = 0;
290 }
291
292 pub fn len(&self) -> usize {
294 self.n_items
295 }
296
297 pub fn is_empty(&self) -> bool {
299 self.n_items == 0
300 }
301
302 pub fn n_cells(&self) -> usize {
304 self.cells.len()
305 }
306
307 pub fn statistics(&self) -> SpatialHashStats {
309 let n_cells = self.cells.len();
310 if n_cells == 0 {
311 return SpatialHashStats {
312 n_items: 0,
313 n_occupied_cells: 0,
314 min_bucket_size: 0,
315 max_bucket_size: 0,
316 avg_bucket_size: 0.0,
317 load_factor: 0.0,
318 };
319 }
320 let sizes: Vec<usize> = self.cells.values().map(|b| b.len()).collect();
321 let min_b = *sizes.iter().min().unwrap_or(&0);
322 let max_b = *sizes.iter().max().unwrap_or(&0);
323 let sum: usize = sizes.iter().sum();
324 SpatialHashStats {
325 n_items: self.n_items,
326 n_occupied_cells: n_cells,
327 min_bucket_size: min_b,
328 max_bucket_size: max_b,
329 avg_bucket_size: sum as f64 / n_cells as f64,
330 load_factor: sum as f64 / n_cells as f64,
331 }
332 }
333
334 pub fn update_position(&mut self, id: usize, old_pos: [f64; 3], new_pos: [f64; 3], data: T) {
336 self.remove(id, old_pos);
337 self.insert(id, new_pos, data);
338 }
339}
340
341#[derive(Debug, Clone)]
343pub struct SpatialHashStats {
344 pub n_items: usize,
346 pub n_occupied_cells: usize,
348 pub min_bucket_size: usize,
350 pub max_bucket_size: usize,
352 pub avg_bucket_size: f64,
354 pub load_factor: f64,
356}
357
358#[derive(Debug, Clone, Copy)]
362pub struct RayGridStep {
363 pub cell: (i32, i32, i32),
365 pub t_enter: f64,
367 pub t_exit: f64,
369}
370
371pub fn ray_traverse_grid(
376 origin: [f64; 3],
377 direction: [f64; 3],
378 cell_size: f64,
379 max_t: f64,
380 max_steps: usize,
381) -> Vec<RayGridStep> {
382 let mut result = Vec::new();
383 if cell_size <= 0.0 {
384 return result;
385 }
386
387 let dir_len =
389 (direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2])
390 .sqrt();
391 if dir_len < 1e-15 {
392 return result;
393 }
394 let dir = [
395 direction[0] / dir_len,
396 direction[1] / dir_len,
397 direction[2] / dir_len,
398 ];
399
400 let mut cell = [
402 (origin[0] / cell_size).floor() as i32,
403 (origin[1] / cell_size).floor() as i32,
404 (origin[2] / cell_size).floor() as i32,
405 ];
406
407 let mut step = [0i32; 3];
409 let mut t_max = [0.0f64; 3]; let mut t_delta = [f64::INFINITY; 3];
411
412 for i in 0..3 {
413 if dir[i] > 0.0 {
414 step[i] = 1;
415 let next_boundary = (cell[i] as f64 + 1.0) * cell_size;
416 t_max[i] = (next_boundary - origin[i]) / dir[i];
417 t_delta[i] = cell_size / dir[i];
418 } else if dir[i] < 0.0 {
419 step[i] = -1;
420 let next_boundary = cell[i] as f64 * cell_size;
421 t_max[i] = (next_boundary - origin[i]) / dir[i];
422 t_delta[i] = cell_size / (-dir[i]);
423 } else {
424 step[i] = 0;
425 t_max[i] = f64::INFINITY;
426 t_delta[i] = f64::INFINITY;
427 }
428 }
429
430 let mut t = 0.0;
431 for _ in 0..max_steps {
432 let min_t = t_max[0].min(t_max[1]).min(t_max[2]);
434 let t_exit = min_t.min(max_t);
435
436 result.push(RayGridStep {
437 cell: (cell[0], cell[1], cell[2]),
438 t_enter: t,
439 t_exit,
440 });
441
442 if min_t >= max_t {
443 break;
444 }
445
446 if t_max[0] <= t_max[1] && t_max[0] <= t_max[2] {
448 cell[0] += step[0];
449 t = t_max[0];
450 t_max[0] += t_delta[0];
451 } else if t_max[1] <= t_max[2] {
452 cell[1] += step[1];
453 t = t_max[1];
454 t_max[1] += t_delta[1];
455 } else {
456 cell[2] += step[2];
457 t = t_max[2];
458 t_max[2] += t_delta[2];
459 }
460 }
461
462 result
463}
464
465pub struct LooseOctree<T: Clone> {
469 pub center: [f64; 3],
471 pub half_size: f64,
473 pub depth: u32,
475 pub items: Vec<(usize, [f64; 3], T)>,
477 pub children: Option<Box<[LooseOctree<T>; 8]>>,
479}
480
481impl<T: Clone> LooseOctree<T> {
482 pub fn new(center: [f64; 3], half_size: f64, max_depth: u32) -> Self {
484 Self {
485 center,
486 half_size,
487 depth: max_depth,
488 items: Vec::new(),
489 children: None,
490 }
491 }
492
493 pub fn child_index(&self, pos: [f64; 3]) -> usize {
495 let mut idx = 0usize;
496 if pos[0] >= self.center[0] {
497 idx |= 1;
498 }
499 if pos[1] >= self.center[1] {
500 idx |= 2;
501 }
502 if pos[2] >= self.center[2] {
503 idx |= 4;
504 }
505 idx
506 }
507
508 fn child_center(&self, i: usize) -> [f64; 3] {
510 let q = self.half_size * 0.5;
511 [
512 self.center[0] + if i & 1 != 0 { q } else { -q },
513 self.center[1] + if i & 2 != 0 { q } else { -q },
514 self.center[2] + if i & 4 != 0 { q } else { -q },
515 ]
516 }
517
518 fn subdivide(&mut self) {
520 if self.children.is_some() {
521 return;
522 }
523 let child_depth = self.depth.saturating_sub(1);
524 let child_half = self.half_size * 0.5;
525 let children = std::array::from_fn(|i| {
526 LooseOctree::new(self.child_center(i), child_half, child_depth)
527 });
528 self.children = Some(Box::new(children));
529 }
530
531 pub fn insert(&mut self, id: usize, pos: [f64; 3], data: T, max_items: usize) {
533 self.items.push((id, pos, data.clone()));
534
535 if self.items.len() > max_items && self.depth > 0 {
536 self.subdivide();
537 let items: Vec<_> = self.items.drain(..).collect();
538 for (iid, ipos, idata) in items {
539 let ci = self.child_index(ipos);
540 if let Some(children) = self.children.as_mut() {
541 children[ci].insert(iid, ipos, idata, max_items);
542 }
543 }
544 }
545 }
546
547 pub fn query_sphere(&self, center: [f64; 3], radius: f64) -> Vec<(usize, &T)> {
549 let loose = self.half_size * 2.0;
550 let mut closest2 = 0.0f64;
551 for i in 0..3 {
552 let lo = self.center[i] - loose;
553 let hi = self.center[i] + loose;
554 if center[i] < lo {
555 closest2 += (center[i] - lo).powi(2);
556 } else if center[i] > hi {
557 closest2 += (center[i] - hi).powi(2);
558 }
559 }
560 if closest2 > radius * radius {
561 return Vec::new();
562 }
563
564 let r2 = radius * radius;
565 let mut result = Vec::new();
566
567 for (id, pos, data) in &self.items {
568 let d2 = dist_sq(*pos, center);
569 if d2 <= r2 {
570 result.push((*id, data));
571 }
572 }
573
574 if let Some(children) = &self.children {
575 for child in children.iter() {
576 result.extend(child.query_sphere(center, radius));
577 }
578 }
579
580 result
581 }
582
583 pub fn query_aabb(&self, min: [f64; 3], max: [f64; 3]) -> Vec<(usize, &T)> {
585 let loose = self.half_size * 2.0;
587 for i in 0..3 {
588 if self.center[i] - loose > max[i] || self.center[i] + loose < min[i] {
589 return Vec::new();
590 }
591 }
592
593 let mut result = Vec::new();
594 for (id, pos, data) in &self.items {
595 if pos[0] >= min[0]
596 && pos[0] <= max[0]
597 && pos[1] >= min[1]
598 && pos[1] <= max[1]
599 && pos[2] >= min[2]
600 && pos[2] <= max[2]
601 {
602 result.push((*id, data));
603 }
604 }
605
606 if let Some(children) = &self.children {
607 for child in children.iter() {
608 result.extend(child.query_aabb(min, max));
609 }
610 }
611
612 result
613 }
614
615 pub fn item_count(&self) -> usize {
617 let mut count = self.items.len();
618 if let Some(children) = &self.children {
619 for child in children.iter() {
620 count += child.item_count();
621 }
622 }
623 count
624 }
625
626 pub fn max_depth_reached(&self) -> u32 {
628 if let Some(children) = &self.children {
629 let child_max = children
630 .iter()
631 .map(|c| c.max_depth_reached())
632 .max()
633 .unwrap_or(0);
634 child_max + 1
635 } else {
636 0
637 }
638 }
639
640 pub fn node_count(&self) -> usize {
642 let mut count = 1;
643 if let Some(children) = &self.children {
644 for child in children.iter() {
645 count += child.node_count();
646 }
647 }
648 count
649 }
650
651 pub fn k_nearest(&self, center: [f64; 3], k: usize) -> Vec<(usize, f64, &T)> {
653 let mut heap: BinaryHeap<KnnEntry<&T>> = BinaryHeap::new();
655 self.knn_recurse(center, k, &mut heap);
656
657 let mut result: Vec<(usize, f64, &T)> =
658 heap.into_iter().map(|e| (e.id, e.dist, e.data)).collect();
659 result.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
660 result
661 }
662
663 fn knn_recurse<'a>(
664 &'a self,
665 center: [f64; 3],
666 k: usize,
667 heap: &mut BinaryHeap<KnnEntry<&'a T>>,
668 ) {
669 for (id, pos, data) in &self.items {
671 let d = dist_sq(*pos, center).sqrt();
672 if heap.len() < k {
673 heap.push(KnnEntry {
674 dist: d,
675 id: *id,
676 data,
677 });
678 } else if let Some(top) = heap.peek()
679 && d < top.dist
680 {
681 heap.pop();
682 heap.push(KnnEntry {
683 dist: d,
684 id: *id,
685 data,
686 });
687 }
688 }
689
690 if let Some(children) = &self.children {
692 for child in children.iter() {
693 let loose = child.half_size * 2.0;
695 let mut min_d2 = 0.0f64;
696 for i in 0..3 {
697 let lo = child.center[i] - loose;
698 let hi = child.center[i] + loose;
699 if center[i] < lo {
700 min_d2 += (center[i] - lo).powi(2);
701 } else if center[i] > hi {
702 min_d2 += (center[i] - hi).powi(2);
703 }
704 }
705 let min_d = min_d2.sqrt();
706 let should_search =
707 heap.len() < k || heap.peek().is_some_and(|top| min_d < top.dist);
708 if should_search {
709 child.knn_recurse(center, k, heap);
710 }
711 }
712 }
713 }
714}
715
716struct KnnEntry<D> {
718 dist: f64,
719 id: usize,
720 data: D,
721}
722
723impl<D> PartialEq for KnnEntry<D> {
724 fn eq(&self, other: &Self) -> bool {
725 self.dist == other.dist
726 }
727}
728
729impl<D> Eq for KnnEntry<D> {}
730
731impl<D> PartialOrd for KnnEntry<D> {
732 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
733 Some(self.cmp(other))
734 }
735}
736
737impl<D> Ord for KnnEntry<D> {
738 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
739 self.dist
740 .partial_cmp(&other.dist)
741 .unwrap_or(std::cmp::Ordering::Equal)
742 }
743}
744
745pub struct ParallelSpatialHash<T: Clone> {
753 inner: SpatialHashPos3D<T>,
754}
755
756impl<T: Clone> ParallelSpatialHash<T> {
757 pub fn build(entries: &[(usize, [f64; 3], T)], cell_size: f64) -> Self {
759 let mut inner = SpatialHashPos3D::new(cell_size);
760 for (id, pos, data) in entries {
761 inner.insert(*id, *pos, data.clone());
762 }
763 Self { inner }
764 }
765
766 pub fn query_radius(&self, center: [f64; 3], radius: f64) -> Vec<(usize, &T)> {
768 self.inner.query_radius(center, radius)
769 }
770
771 pub fn query_aabb(&self, min: [f64; 3], max: [f64; 3]) -> Vec<(usize, &T)> {
773 self.inner.query_aabb(min, max)
774 }
775
776 pub fn len(&self) -> usize {
778 self.inner.len()
779 }
780
781 pub fn is_empty(&self) -> bool {
783 self.inner.is_empty()
784 }
785
786 pub fn n_cells(&self) -> usize {
788 self.inner.n_cells()
789 }
790}
791
792pub fn merge_spatial_hashes<T: Clone>(
798 a: &SpatialHashPos3D<T>,
799 b: &SpatialHashPos3D<T>,
800) -> SpatialHashPos3D<T> {
801 assert!(
802 (a.cell_size - b.cell_size).abs() < 1e-12,
803 "merge requires identical cell sizes"
804 );
805 let mut merged = SpatialHashPos3D::new(a.cell_size);
806
807 for (key, bucket) in &a.cells {
809 for (id, pos, data) in bucket {
810 merged
812 .cells
813 .entry(*key)
814 .or_default()
815 .push((*id, *pos, data.clone()));
816 merged.n_items += 1;
817 }
818 }
819
820 for (key, bucket) in &b.cells {
822 for (id, pos, data) in bucket {
823 merged
824 .cells
825 .entry(*key)
826 .or_default()
827 .push((*id, *pos, data.clone()));
828 merged.n_items += 1;
829 }
830 }
831
832 merged
833}
834
835pub struct PersistentSpatialHash<T: Clone> {
842 hash: SpatialHashPos3D<T>,
844 last_positions: HashMap<usize, [f64; 3]>,
846 frame: u64,
848}
849
850impl<T: Clone> PersistentSpatialHash<T> {
851 pub fn new(cell_size: f64) -> Self {
853 Self {
854 hash: SpatialHashPos3D::new(cell_size),
855 last_positions: HashMap::new(),
856 frame: 0,
857 }
858 }
859
860 pub fn insert_or_update(&mut self, id: usize, pos: [f64; 3], data: T) {
864 if let Some(&old_pos) = self.last_positions.get(&id) {
865 self.hash.remove(id, old_pos);
866 }
867 self.hash.insert(id, pos, data);
868 self.last_positions.insert(id, pos);
869 }
870
871 pub fn remove(&mut self, id: usize) {
873 if let Some(&pos) = self.last_positions.get(&id) {
874 self.hash.remove(id, pos);
875 self.last_positions.remove(&id);
876 }
877 }
878
879 pub fn query_radius(&self, center: [f64; 3], radius: f64) -> Vec<(usize, &T)> {
881 self.hash.query_radius(center, radius)
882 }
883
884 pub fn query_aabb(&self, min: [f64; 3], max: [f64; 3]) -> Vec<(usize, &T)> {
886 self.hash.query_aabb(min, max)
887 }
888
889 pub fn len(&self) -> usize {
891 self.hash.len()
892 }
893
894 pub fn is_empty(&self) -> bool {
896 self.hash.is_empty()
897 }
898
899 pub fn advance_frame(&mut self) {
901 self.frame += 1;
902 }
903
904 pub fn current_frame(&self) -> u64 {
906 self.frame
907 }
908
909 pub fn statistics(&self) -> SpatialHashStats {
911 self.hash.statistics()
912 }
913}
914
915pub struct GpuSpatialHashLayout<T: Clone> {
922 cell_size: f32,
923 cell_keys: Vec<(i32, i32, i32)>,
925 cell_offsets: Vec<(usize, usize)>,
927 items: Vec<(usize, [f32; 3], T)>,
929}
930
931impl<T: Clone> GpuSpatialHashLayout<T> {
932 pub fn build(entries: &[(usize, [f32; 3], T)], cell_size: f32) -> Self {
934 let mut buckets: HashMap<(i32, i32, i32), Vec<(usize, [f32; 3], T)>> = HashMap::new();
936 for (id, pos, data) in entries {
937 let key = (
938 (pos[0] / cell_size).floor() as i32,
939 (pos[1] / cell_size).floor() as i32,
940 (pos[2] / cell_size).floor() as i32,
941 );
942 buckets
943 .entry(key)
944 .or_default()
945 .push((*id, *pos, data.clone()));
946 }
947
948 let mut cell_keys: Vec<(i32, i32, i32)> = buckets.keys().copied().collect();
950 cell_keys.sort_unstable();
951
952 let mut items = Vec::new();
953 let mut cell_offsets = Vec::new();
954
955 for &key in &cell_keys {
956 let start = items.len();
957 for entry in &buckets[&key] {
958 items.push(entry.clone());
959 }
960 cell_offsets.push((start, items.len()));
961 }
962
963 Self {
964 cell_size,
965 cell_keys,
966 cell_offsets,
967 items,
968 }
969 }
970
971 pub fn n_items(&self) -> usize {
973 self.items.len()
974 }
975
976 pub fn n_cells(&self) -> usize {
978 self.cell_keys.len()
979 }
980
981 pub fn query_cell_flat(&self, cx: i32, cy: i32, cz: i32) -> Vec<(usize, &T)> {
983 let key = (cx, cy, cz);
984 if let Ok(pos) = self.cell_keys.binary_search(&key) {
985 let (start, end) = self.cell_offsets[pos];
986 self.items[start..end]
987 .iter()
988 .map(|(id, _, data)| (*id, data))
989 .collect()
990 } else {
991 Vec::new()
992 }
993 }
994
995 pub fn to_flat_f32_buffer(&self) -> Vec<f32> {
999 let mut buf = Vec::with_capacity(self.items.len() * 6);
1000 for (item_idx, (_, pos, _)) in self.items.iter().enumerate() {
1001 let key = self
1003 .cell_keys
1004 .iter()
1005 .enumerate()
1006 .find(|(ci, _)| {
1007 let (start, end) = self.cell_offsets[*ci];
1008 item_idx >= start && item_idx < end
1009 })
1010 .map(|(_, &k)| k)
1011 .unwrap_or((0, 0, 0));
1012 buf.push(pos[0]);
1013 buf.push(pos[1]);
1014 buf.push(pos[2]);
1015 buf.push(key.0 as f32);
1016 buf.push(key.1 as f32);
1017 buf.push(key.2 as f32);
1018 }
1019 buf
1020 }
1021
1022 pub fn cell_size(&self) -> f32 {
1024 self.cell_size
1025 }
1026}
1027
1028#[cfg(test)]
1031mod tests {
1032 use super::*;
1033
1034 #[test]
1037 fn spatial_hash_basic_insert_query() {
1038 let mut sh: SpatialHash3D<&str> = SpatialHash3D::new(1.0);
1039 sh.insert(0, [0.5, 0.5, 0.5], "a");
1040 assert_eq!(sh.len(), 1);
1041 assert_eq!(sh.n_cells(), 1);
1042 let items = sh.query_cell((0, 0, 0));
1043 assert_eq!(items.len(), 1);
1044 assert_eq!(items[0].0, 0);
1045 }
1046
1047 #[test]
1048 fn spatial_hash_remove() {
1049 let mut sh: SpatialHash3D<i32> = SpatialHash3D::new(1.0);
1050 sh.insert(0, [0.0, 0.0, 0.0], 42);
1051 sh.insert(1, [0.5, 0.0, 0.0], 7);
1052 sh.remove(0, [0.0, 0.0, 0.0]);
1053 assert_eq!(sh.len(), 1);
1054 }
1055
1056 #[test]
1057 fn spatial_hash_cell_key() {
1058 let sh: SpatialHash3D<()> = SpatialHash3D::new(2.0);
1059 assert_eq!(sh.cell_key([0.5, 0.5, 0.5]), (0, 0, 0));
1060 assert_eq!(sh.cell_key([2.5, 4.5, -0.5]), (1, 2, -1));
1061 }
1062
1063 #[test]
1066 fn spatial_hash_insert_query_radius() {
1067 let mut sh: SpatialHashPos3D<&str> = SpatialHashPos3D::new(1.0);
1068 sh.insert(0, [0.0, 0.0, 0.0], "origin");
1069 sh.insert(1, [0.5, 0.0, 0.0], "near");
1070 sh.insert(2, [10.0, 0.0, 0.0], "far");
1071
1072 let results = sh.query_radius([0.0, 0.0, 0.0], 1.0);
1073 let ids: Vec<usize> = results.iter().map(|(id, _)| *id).collect();
1074 assert!(ids.contains(&0), "origin should be found");
1075 assert!(ids.contains(&1), "near should be found");
1076 assert!(!ids.contains(&2), "far should NOT be found");
1077 }
1078
1079 #[test]
1080 fn spatial_hash_clear_len_zero() {
1081 let mut sh: SpatialHashPos3D<i32> = SpatialHashPos3D::new(1.0);
1082 sh.insert(0, [0.0, 0.0, 0.0], 42);
1083 sh.insert(1, [1.0, 1.0, 1.0], 7);
1084 assert_eq!(sh.len(), 2);
1085 sh.clear();
1086 assert_eq!(sh.len(), 0);
1087 assert!(sh.is_empty());
1088 }
1089
1090 #[test]
1091 fn spatial_hash_query_radius_excludes_outside() {
1092 let mut sh: SpatialHashPos3D<u32> = SpatialHashPos3D::new(2.0);
1093 let radius = 1.5_f64;
1094 sh.insert(0, [0.0, 0.0, 0.0], 0);
1095 sh.insert(1, [radius + 0.1, 0.0, 0.0], 1);
1096 sh.insert(2, [radius - 0.1, 0.0, 0.0], 2);
1097
1098 let results = sh.query_radius([0.0, 0.0, 0.0], radius);
1099 let ids: Vec<usize> = results.iter().map(|(id, _)| *id).collect();
1100 assert!(ids.contains(&0));
1101 assert!(ids.contains(&2));
1102 assert!(
1103 !ids.contains(&1),
1104 "item just outside radius must be excluded"
1105 );
1106 }
1107
1108 #[test]
1109 fn spatial_hash_query_aabb() {
1110 let mut sh: SpatialHashPos3D<&str> = SpatialHashPos3D::new(1.0);
1111 sh.insert(0, [0.5, 0.5, 0.5], "inside");
1112 sh.insert(1, [1.5, 0.5, 0.5], "inside2");
1113 sh.insert(2, [5.0, 5.0, 5.0], "outside");
1114
1115 let results = sh.query_aabb([0.0, 0.0, 0.0], [2.0, 1.0, 1.0]);
1116 let ids: Vec<usize> = results.iter().map(|(id, _)| *id).collect();
1117 assert!(ids.contains(&0));
1118 assert!(ids.contains(&1));
1119 assert!(!ids.contains(&2));
1120 }
1121
1122 #[test]
1123 fn spatial_hash_k_nearest() {
1124 let mut sh: SpatialHashPos3D<&str> = SpatialHashPos3D::new(1.0);
1125 sh.insert(0, [0.0, 0.0, 0.0], "a");
1126 sh.insert(1, [1.0, 0.0, 0.0], "b");
1127 sh.insert(2, [2.0, 0.0, 0.0], "c");
1128 sh.insert(3, [10.0, 0.0, 0.0], "d");
1129
1130 let nearest = sh.k_nearest([0.0, 0.0, 0.0], 2);
1131 assert_eq!(nearest.len(), 2);
1132 assert_eq!(nearest[0].0, 0, "closest should be item 0");
1133 assert_eq!(nearest[1].0, 1, "second closest should be item 1");
1134 }
1135
1136 #[test]
1137 fn spatial_hash_k_nearest_more_than_items() {
1138 let mut sh: SpatialHashPos3D<i32> = SpatialHashPos3D::new(1.0);
1139 sh.insert(0, [0.0, 0.0, 0.0], 1);
1140
1141 let nearest = sh.k_nearest([0.0, 0.0, 0.0], 5);
1142 assert_eq!(nearest.len(), 1, "should return all items when k > n_items");
1143 }
1144
1145 #[test]
1146 fn spatial_hash_statistics() {
1147 let mut sh: SpatialHashPos3D<i32> = SpatialHashPos3D::new(1.0);
1148 sh.insert(0, [0.0, 0.0, 0.0], 1);
1149 sh.insert(1, [0.5, 0.0, 0.0], 2);
1150 sh.insert(2, [5.0, 5.0, 5.0], 3);
1151
1152 let stats = sh.statistics();
1153 assert_eq!(stats.n_items, 3);
1154 assert_eq!(stats.n_occupied_cells, 2);
1155 assert_eq!(stats.max_bucket_size, 2);
1156 assert_eq!(stats.min_bucket_size, 1);
1157 }
1158
1159 #[test]
1160 fn spatial_hash_statistics_empty() {
1161 let sh: SpatialHashPos3D<i32> = SpatialHashPos3D::new(1.0);
1162 let stats = sh.statistics();
1163 assert_eq!(stats.n_items, 0);
1164 assert_eq!(stats.n_occupied_cells, 0);
1165 }
1166
1167 #[test]
1168 fn spatial_hash_update_position() {
1169 let mut sh: SpatialHashPos3D<&str> = SpatialHashPos3D::new(1.0);
1170 sh.insert(0, [0.0, 0.0, 0.0], "a");
1171 sh.update_position(0, [0.0, 0.0, 0.0], [5.0, 5.0, 5.0], "a");
1172 assert_eq!(sh.len(), 1);
1173
1174 let at_origin = sh.query_radius([0.0, 0.0, 0.0], 0.5);
1176 assert!(at_origin.is_empty(), "item should no longer be at origin");
1177
1178 let at_new = sh.query_radius([5.0, 5.0, 5.0], 0.5);
1180 assert_eq!(at_new.len(), 1);
1181 }
1182
1183 #[test]
1186 fn ray_traverse_along_x_axis() {
1187 let steps = ray_traverse_grid([0.5, 0.5, 0.5], [1.0, 0.0, 0.0], 1.0, 3.0, 100);
1188 assert!(!steps.is_empty());
1189 let cells: Vec<(i32, i32, i32)> = steps.iter().map(|s| s.cell).collect();
1191 assert!(cells.contains(&(0, 0, 0)));
1192 assert!(cells.contains(&(1, 0, 0)));
1193 assert!(cells.contains(&(2, 0, 0)));
1194 }
1195
1196 #[test]
1197 fn ray_traverse_diagonal() {
1198 let steps = ray_traverse_grid([0.5, 0.5, 0.5], [1.0, 1.0, 0.0], 1.0, 3.0, 100);
1199 assert!(!steps.is_empty());
1200 assert_eq!(steps[0].cell, (0, 0, 0));
1202 }
1203
1204 #[test]
1205 fn ray_traverse_zero_direction() {
1206 let steps = ray_traverse_grid([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 1.0, 10.0, 100);
1207 assert!(steps.is_empty(), "zero direction should produce no steps");
1208 }
1209
1210 #[test]
1211 fn ray_traverse_negative_direction() {
1212 let steps = ray_traverse_grid([2.5, 0.5, 0.5], [-1.0, 0.0, 0.0], 1.0, 3.0, 100);
1213 assert!(!steps.is_empty());
1214 let cells: Vec<(i32, i32, i32)> = steps.iter().map(|s| s.cell).collect();
1215 assert!(cells.contains(&(2, 0, 0)));
1216 assert!(cells.contains(&(1, 0, 0)));
1217 assert!(cells.contains(&(0, 0, 0)));
1218 }
1219
1220 #[test]
1221 fn ray_traverse_max_steps_limit() {
1222 let steps = ray_traverse_grid([0.5, 0.5, 0.5], [1.0, 0.0, 0.0], 1.0, 1000.0, 5);
1223 assert!(steps.len() <= 5, "should respect max_steps limit");
1224 }
1225
1226 #[test]
1229 fn loose_octree_insert_query_sphere() {
1230 let mut tree: LooseOctree<&str> = LooseOctree::new([0.0; 3], 16.0, 4);
1231 tree.insert(0, [1.0, 0.0, 0.0], "a", 2);
1232 tree.insert(1, [0.0, 1.0, 0.0], "b", 2);
1233 tree.insert(2, [100.0, 0.0, 0.0], "far", 2);
1234
1235 let results = tree.query_sphere([0.0, 0.0, 0.0], 5.0);
1236 let ids: Vec<usize> = results.iter().map(|(id, _)| *id).collect();
1237 assert!(ids.contains(&0), "item 0 should be found");
1238 assert!(ids.contains(&1), "item 1 should be found");
1239 assert!(!ids.contains(&2), "far item must not appear");
1240 }
1241
1242 #[test]
1243 fn loose_octree_item_count() {
1244 let mut tree: LooseOctree<i32> = LooseOctree::new([0.0; 3], 32.0, 3);
1245 for i in 0..5_usize {
1246 tree.insert(i, [i as f64, 0.0, 0.0], i as i32, 2);
1247 }
1248 assert!(
1249 tree.item_count() >= 5,
1250 "item_count should be at least 5, got {}",
1251 tree.item_count()
1252 );
1253 }
1254
1255 #[test]
1256 fn loose_octree_query_aabb() {
1257 let mut tree: LooseOctree<&str> = LooseOctree::new([0.0; 3], 16.0, 4);
1258 tree.insert(0, [1.0, 1.0, 1.0], "inside", 2);
1259 tree.insert(1, [-1.0, -1.0, -1.0], "inside2", 2);
1260 tree.insert(2, [50.0, 50.0, 50.0], "outside", 2);
1261
1262 let results = tree.query_aabb([-2.0, -2.0, -2.0], [2.0, 2.0, 2.0]);
1263 let ids: Vec<usize> = results.iter().map(|(id, _)| *id).collect();
1264 assert!(ids.contains(&0));
1265 assert!(ids.contains(&1));
1266 assert!(!ids.contains(&2));
1267 }
1268
1269 #[test]
1270 fn loose_octree_k_nearest() {
1271 let mut tree: LooseOctree<&str> = LooseOctree::new([0.0; 3], 32.0, 4);
1272 tree.insert(0, [0.0, 0.0, 0.0], "origin", 2);
1273 tree.insert(1, [1.0, 0.0, 0.0], "near", 2);
1274 tree.insert(2, [5.0, 0.0, 0.0], "mid", 2);
1275 tree.insert(3, [100.0, 0.0, 0.0], "far", 2);
1276
1277 let nearest = tree.k_nearest([0.0, 0.0, 0.0], 2);
1278 assert_eq!(nearest.len(), 2);
1279 assert_eq!(nearest[0].0, 0, "closest should be item 0");
1280 assert_eq!(nearest[1].0, 1, "second closest should be item 1");
1281 }
1282
1283 #[test]
1284 fn loose_octree_max_depth() {
1285 let mut tree: LooseOctree<i32> = LooseOctree::new([0.0; 3], 32.0, 4);
1286 for i in 0..10_usize {
1288 tree.insert(i, [i as f64 * 0.1, 0.0, 0.0], i as i32, 2);
1289 }
1290 let depth = tree.max_depth_reached();
1291 assert!(depth > 0, "should have subdivided at least once");
1292 }
1293
1294 #[test]
1295 fn loose_octree_node_count() {
1296 let tree: LooseOctree<i32> = LooseOctree::new([0.0; 3], 32.0, 4);
1297 assert_eq!(tree.node_count(), 1, "empty tree has one root node");
1298 }
1299
1300 #[test]
1301 fn loose_octree_node_count_after_subdivision() {
1302 let mut tree: LooseOctree<i32> = LooseOctree::new([0.0; 3], 32.0, 4);
1303 for i in 0..10_usize {
1304 tree.insert(i, [i as f64, 0.0, 0.0], i as i32, 2);
1305 }
1306 assert!(tree.node_count() > 1, "should have created child nodes");
1307 }
1308
1309 #[test]
1312 fn parallel_spatial_hash_build_query() {
1313 let entries: Vec<(usize, [f64; 3], i32)> = (0..8)
1314 .map(|i| (i, [i as f64, 0.0, 0.0], i as i32))
1315 .collect();
1316 let sh = ParallelSpatialHash::build(&entries, 2.0);
1317 let results = sh.query_radius([0.0, 0.0, 0.0], 2.5);
1318 let ids: Vec<usize> = results.iter().map(|(id, _)| *id).collect();
1319 assert!(ids.contains(&0));
1320 assert!(ids.contains(&1));
1321 assert!(ids.contains(&2));
1322 }
1323
1324 #[test]
1325 fn parallel_spatial_hash_len() {
1326 let entries: Vec<(usize, [f64; 3], &str)> =
1327 vec![(0, [0.0, 0.0, 0.0], "a"), (1, [1.0, 0.0, 0.0], "b")];
1328 let sh = ParallelSpatialHash::build(&entries, 1.0);
1329 assert_eq!(sh.len(), 2);
1330 assert!(!sh.is_empty());
1331 }
1332
1333 #[test]
1334 fn parallel_spatial_hash_empty() {
1335 let entries: Vec<(usize, [f64; 3], i32)> = Vec::new();
1336 let sh = ParallelSpatialHash::build(&entries, 1.0);
1337 assert!(sh.is_empty());
1338 assert_eq!(sh.len(), 0);
1339 }
1340
1341 #[test]
1344 fn spatial_hash_merge_basic() {
1345 let mut a: SpatialHashPos3D<i32> = SpatialHashPos3D::new(1.0);
1346 a.insert(0, [0.0, 0.0, 0.0], 10);
1347 a.insert(1, [1.0, 0.0, 0.0], 20);
1348
1349 let mut b: SpatialHashPos3D<i32> = SpatialHashPos3D::new(1.0);
1350 b.insert(2, [5.0, 0.0, 0.0], 30);
1351 b.insert(3, [6.0, 0.0, 0.0], 40);
1352
1353 let merged = merge_spatial_hashes(&a, &b);
1354 assert_eq!(merged.len(), 4);
1355 }
1356
1357 #[test]
1358 fn spatial_hash_merge_query_all() {
1359 let mut a: SpatialHashPos3D<&str> = SpatialHashPos3D::new(1.0);
1360 a.insert(0, [0.0, 0.0, 0.0], "a");
1361
1362 let mut b: SpatialHashPos3D<&str> = SpatialHashPos3D::new(1.0);
1363 b.insert(1, [10.0, 0.0, 0.0], "b");
1364
1365 let merged = merge_spatial_hashes(&a, &b);
1366 let r0 = merged.query_radius([0.0, 0.0, 0.0], 0.5);
1367 let r1 = merged.query_radius([10.0, 0.0, 0.0], 0.5);
1368 assert_eq!(r0.len(), 1);
1369 assert_eq!(r1.len(), 1);
1370 }
1371
1372 #[test]
1375 fn persistent_hash_update_positions() {
1376 let mut ph: PersistentSpatialHash<i32> = PersistentSpatialHash::new(1.0);
1377 ph.insert_or_update(0, [0.0, 0.0, 0.0], 100);
1378 ph.insert_or_update(1, [5.0, 0.0, 0.0], 200);
1379
1380 let r = ph.query_radius([0.0, 0.0, 0.0], 0.5);
1382 assert_eq!(r.len(), 1);
1383 assert_eq!(r[0].0, 0);
1384
1385 ph.insert_or_update(0, [5.5, 0.0, 0.0], 100);
1387
1388 let r2 = ph.query_radius([0.0, 0.0, 0.0], 0.5);
1389 assert!(r2.is_empty(), "item 0 should have moved");
1390 }
1391
1392 #[test]
1393 fn persistent_hash_remove() {
1394 let mut ph: PersistentSpatialHash<i32> = PersistentSpatialHash::new(1.0);
1395 ph.insert_or_update(0, [0.0, 0.0, 0.0], 1);
1396 ph.insert_or_update(1, [0.5, 0.0, 0.0], 2);
1397 ph.remove(0);
1398 assert_eq!(ph.len(), 1);
1399 }
1400
1401 #[test]
1402 fn persistent_hash_frame_advance() {
1403 let mut ph: PersistentSpatialHash<i32> = PersistentSpatialHash::new(1.0);
1404 ph.insert_or_update(0, [0.0, 0.0, 0.0], 42);
1405 ph.advance_frame();
1406 assert_eq!(ph.current_frame(), 1);
1407 let r = ph.query_radius([0.0, 0.0, 0.0], 0.5);
1409 assert_eq!(r.len(), 1);
1410 }
1411
1412 #[test]
1415 fn gpu_layout_build_basic() {
1416 let entries: Vec<(usize, [f32; 3], u32)> = vec![
1417 (0, [0.0, 0.0, 0.0], 10),
1418 (1, [1.0, 0.0, 0.0], 20),
1419 (2, [2.0, 0.0, 0.0], 30),
1420 ];
1421 let layout = GpuSpatialHashLayout::build(&entries, 1.0);
1422 assert_eq!(layout.n_items(), 3);
1423 }
1424
1425 #[test]
1426 fn gpu_layout_query_cell() {
1427 let entries: Vec<(usize, [f32; 3], u32)> = vec![
1428 (0, [0.5, 0.0, 0.0], 1),
1429 (1, [0.7, 0.0, 0.0], 2),
1430 (2, [5.0, 0.0, 0.0], 3),
1431 ];
1432 let layout = GpuSpatialHashLayout::build(&entries, 1.0);
1433 let cell_items = layout.query_cell_flat(0, 0, 0);
1434 assert_eq!(cell_items.len(), 2, "two items in cell (0,0,0)");
1435 }
1436
1437 #[test]
1438 fn gpu_layout_flat_buffer_format() {
1439 let entries: Vec<(usize, [f32; 3], u32)> = vec![(0, [1.0, 2.0, 3.0], 99)];
1440 let layout = GpuSpatialHashLayout::build(&entries, 1.0);
1441 let buf = layout.to_flat_f32_buffer();
1442 assert!(!buf.is_empty());
1444 assert_eq!(buf[0], 1.0_f32);
1445 assert_eq!(buf[1], 2.0_f32);
1446 assert_eq!(buf[2], 3.0_f32);
1447 }
1448}