1pub const CELL_SIZE: i32 = 128;
5
6pub fn cell_coord(pos: i32) -> i32 {
10 pos.div_euclid(CELL_SIZE)
11}
12
13pub fn compute_cell_id(world_id: &str, region_id: &str, x: i32, y: i32) -> String {
15 let cx = cell_coord(x);
16 let cy = cell_coord(y);
17 format!("cell:{}:{}:{}:{}", world_id, region_id, cx, cy)
18}
19
20pub fn distance_squared(x1: i32, y1: i32, x2: i32, y2: i32) -> i64 {
22 let dx = (x2 as i64) - (x1 as i64);
23 let dy = (y2 as i64) - (y1 as i64);
24 dx.saturating_mul(dx).saturating_add(dy.saturating_mul(dy))
25}
26
27pub fn action_horizon_cells(cx: i32, cy: i32) -> [(i32, i32); 9] {
29 [
30 (cx - 1, cy - 1),
31 (cx, cy - 1),
32 (cx + 1, cy - 1),
33 (cx - 1, cy),
34 (cx, cy),
35 (cx + 1, cy),
36 (cx - 1, cy + 1),
37 (cx, cy + 1),
38 (cx + 1, cy + 1),
39 ]
40}
41
42pub fn compute_fov<F>(ox: i32, oy: i32, radius: i32, blocks_sight_fn: F) -> Vec<(i32, i32)>
45where
46 F: Fn(i32, i32) -> bool,
47{
48 let mut visible = Vec::with_capacity((radius as usize * 2 + 1).pow(2));
49 visible.push((ox, oy));
50
51 for octant in 0..8u8 {
52 cast_light(&mut visible, &blocks_sight_fn, ox, oy, radius, 1, 1.0, 0.0, octant);
53 }
54 visible
55}
56
57fn cast_light<F>(
58 visible: &mut Vec<(i32, i32)>,
59 blocks: &F,
60 ox: i32,
61 oy: i32,
62 radius: i32,
63 row: i32,
64 mut start_slope: f64,
65 end_slope: f64,
66 octant: u8,
67) where
68 F: Fn(i32, i32) -> bool,
69{
70 if start_slope < end_slope || row > radius {
71 return;
72 }
73
74 let mut prev_blocked = false;
75 let mut saved_start = start_slope;
76
77 for j in row..=radius {
78 let mut blocked = false;
79 let dy = -(j as f64);
80 let dx_f = dy * start_slope;
81 let dx_start = dx_f.round() as i32;
82
83 for dx in ((-j)..=dx_start).rev() {
84 let dx_f = dx as f64;
85 let slope_l = (dx_f + 0.5) / (dy - 0.5);
86 let slope_r = (dx_f - 0.5) / (dy + 0.5);
87
88 if slope_l < end_slope {
89 break;
90 }
91 if slope_r > start_slope {
92 continue;
93 }
94
95 let (mx, my) = transform_octant(dx, j, octant);
96 let ax = ox + mx;
97 let ay = oy + my;
98
99 if dx * dx + j * j <= radius * radius {
100 visible.push((ax, ay));
101 }
102
103 if blocks(ax, ay) {
104 if !prev_blocked {
105 saved_start = start_slope;
106 }
107 prev_blocked = true;
108 blocked = true;
109 start_slope = slope_r;
110 } else if prev_blocked {
111 prev_blocked = false;
112 cast_light(visible, blocks, ox, oy, radius, j + 1, saved_start, slope_l, octant);
113 }
114 }
115
116 if blocked {
117 return;
118 }
119 }
120}
121
122fn transform_octant(dx: i32, dy: i32, octant: u8) -> (i32, i32) {
123 match octant {
124 0 => (dx, -dy),
125 1 => (-dx, -dy),
126 2 => (dx, dy),
127 3 => (-dx, dy),
128 4 => (-dy, dx),
129 5 => (-dy, -dx),
130 6 => (dy, dx),
131 7 => (dy, -dx),
132 _ => (0, 0),
133 }
134}
135
136pub fn compute_observer_visibility_mask(fov_cells: &[(i32, i32)], meshlet_coords: &[(i32, i32)]) -> Vec<u32> {
144 let visible_set: rustc_hash::FxHashSet<(i32, i32)> = fov_cells.iter().copied().collect();
145 meshlet_coords
146 .iter()
147 .map(|coord| if visible_set.contains(coord) { 1 } else { 0 })
148 .collect()
149}
150
151pub fn fill_observer_visibility_mask(fov_cells: &[(i32, i32)], meshlet_coords: &[(i32, i32)], out: &mut [u32]) {
154 let visible_set: rustc_hash::FxHashSet<(i32, i32)> = fov_cells.iter().copied().collect();
155 for (i, coord) in meshlet_coords.iter().enumerate() {
156 if i < out.len() {
157 out[i] = if visible_set.contains(coord) { 1 } else { 0 };
158 }
159 }
160}
161
162struct AStarNode {
168 x: i32,
169 y: i32,
170 g: u32,
171 f: u32,
172}
173
174fn heuristic(x1: i32, y1: i32, x2: i32, y2: i32) -> u32 {
176 let dx = (x2 - x1).unsigned_abs();
177 let dy = (y2 - y1).unsigned_abs();
178 if dx > dy {
179 dx
180 } else {
181 dy
182 }
183}
184
185const DIRS: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
187
188pub fn astar<F>(
192 start_x: i32,
193 start_y: i32,
194 goal_x: i32,
195 goal_y: i32,
196 max_steps: u32,
197 cost_fn: F,
198) -> Option<Vec<(i32, i32)>>
199where
200 F: Fn(i32, i32) -> u32,
201{
202 use std::collections::BTreeMap;
203
204 if start_x == goal_x && start_y == goal_y {
205 return Some(Vec::new());
206 }
207
208 let key = |x: i32, y: i32| -> i64 { (x as i64) << 32 | (y as i64 & 0xFFFFFFFF) };
210
211 let mut g_score: BTreeMap<i64, u32> = BTreeMap::new();
213 let mut came_from: BTreeMap<i64, i64> = BTreeMap::new();
215
216 let mut open: Vec<AStarNode> = Vec::with_capacity(max_steps as usize);
219 let start_h = heuristic(start_x, start_y, goal_x, goal_y);
220 open.push(AStarNode {
221 x: start_x,
222 y: start_y,
223 g: 0,
224 f: start_h,
225 });
226 g_score.insert(key(start_x, start_y), 0);
227
228 let mut iterations: u32 = 0;
229
230 while let Some(best_idx) = find_min_f(&open) {
231 iterations += 1;
232 if iterations > max_steps {
233 return None; }
235
236 let current = open.swap_remove(best_idx);
237
238 if current.x == goal_x && current.y == goal_y {
239 let mut path = Vec::new();
241 let mut ck = key(goal_x, goal_y);
242 let sk = key(start_x, start_y);
243 while ck != sk {
244 let cx = (ck >> 32) as i32;
245 let cy = ck as i32;
246 path.push((cx, cy));
247 ck = match came_from.get(&ck) {
248 Some(&parent) => parent,
249 None => break,
250 };
251 }
252 path.reverse();
253 return Some(path);
254 }
255
256 for &(dx, dy) in &DIRS {
257 let nx = current.x + dx;
258 let ny = current.y + dy;
259 let move_cost = cost_fn(nx, ny);
260 if move_cost >= 999 {
261 continue;
262 } let tentative_g = current.g + move_cost;
265 let nk = key(nx, ny);
266
267 if let Some(&existing_g) = g_score.get(&nk) {
268 if tentative_g >= existing_g {
269 continue;
270 }
271 }
272
273 g_score.insert(nk, tentative_g);
274 came_from.insert(nk, key(current.x, current.y));
275 let h = heuristic(nx, ny, goal_x, goal_y);
276 open.push(AStarNode {
277 x: nx,
278 y: ny,
279 g: tentative_g,
280 f: tentative_g + h,
281 });
282 }
283 }
284
285 None }
287
288type CellKey = (i32, i32);
298
299#[derive(Default)]
304pub struct SpatialGrid {
305 cells: std::collections::HashMap<CellKey, Vec<String>>,
306}
307
308impl SpatialGrid {
309 #[inline]
311 pub fn new() -> Self {
312 Self {
313 cells: std::collections::HashMap::new(),
314 }
315 }
316
317 #[inline]
319 pub fn with_capacity(capacity: usize) -> Self {
320 Self {
321 cells: std::collections::HashMap::with_capacity(capacity),
322 }
323 }
324
325 #[inline]
331 pub fn insert(&mut self, entity_id: &str, x: i32, y: i32) {
332 let key = (cell_coord(x), cell_coord(y));
333 self.cells.entry(key).or_default().push(entity_id.to_string());
334 }
335
336 #[inline]
341 pub fn remove(&mut self, entity_id: &str, x: i32, y: i32) {
342 let key = (cell_coord(x), cell_coord(y));
343 if let Some(list) = self.cells.get_mut(&key) {
344 if let Some(pos) = list.iter().position(|id| id == entity_id) {
345 list.swap_remove(pos);
346 }
347 }
348 }
349
350 #[inline]
354 pub fn query_cell(&self, x: i32, y: i32) -> &[String] {
355 let key = (cell_coord(x), cell_coord(y));
356 match self.cells.get(&key) {
357 Some(list) => list.as_slice(),
358 None => &[],
359 }
360 }
361
362 pub fn query_neighbors(&self, cx: i32, cy: i32) -> Vec<&str> {
367 let mut result = Vec::new();
368 for (ncx, ncy) in action_horizon_cells(cx, cy) {
369 if let Some(list) = self.cells.get(&(ncx, ncy)) {
370 for id in list {
371 result.push(id.as_str());
372 }
373 }
374 }
375 result
376 }
377
378 pub fn query_radius(&self, x: i32, y: i32, range: i32) -> Vec<(&str, i32, i32)> {
388 let cx = cell_coord(x);
389 let cy = cell_coord(y);
390 let range_sq = (range as i64) * (range as i64);
391
392 let mut result = Vec::new();
393 for (ncx, ncy) in action_horizon_cells(cx, cy) {
394 let cell_min_x = ncx * CELL_SIZE;
396 let cell_max_x = cell_min_x + CELL_SIZE - 1;
397 let cell_min_y = ncy * CELL_SIZE;
398 let cell_max_y = cell_min_y + CELL_SIZE - 1;
399
400 let nearest_x = x.clamp(cell_min_x, cell_max_x);
401 let nearest_y = y.clamp(cell_min_y, cell_max_y);
402
403 if distance_squared(x, y, nearest_x, nearest_y) > range_sq {
404 continue;
405 }
406
407 if let Some(list) = self.cells.get(&(ncx, ncy)) {
408 for id in list {
409 result.push((id.as_str(), cell_min_x, cell_min_y));
410 }
411 }
412 }
413 result
414 }
415
416 #[inline]
418 pub fn cell_count(&self) -> usize {
419 self.cells.len()
420 }
421
422 #[inline]
424 pub fn entity_count(&self) -> usize {
425 self.cells.values().map(|v| v.len()).sum()
426 }
427
428 #[inline]
430 pub fn clear(&mut self) {
431 self.cells.clear();
432 }
433}
434
435pub struct SpatialHashGrid {
448 cells: std::collections::HashMap<(i32, i32), Vec<u64>>,
449 cell_size: i32,
450}
451
452impl SpatialHashGrid {
453 pub fn new(cell_size: i32) -> Self {
455 Self {
456 cells: std::collections::HashMap::with_capacity(256),
457 cell_size,
458 }
459 }
460
461 #[inline]
463 pub fn cell_key(&self, x: i32, y: i32) -> (i32, i32) {
464 (x.div_euclid(self.cell_size), y.div_euclid(self.cell_size))
465 }
466
467 pub fn insert(&mut self, entity_id: u64, x: i32, y: i32) {
469 let key = self.cell_key(x, y);
470 self.cells
471 .entry(key)
472 .or_insert_with(|| Vec::with_capacity(16))
473 .push(entity_id);
474 }
475
476 pub fn remove(&mut self, entity_id: u64, x: i32, y: i32) {
479 let key = self.cell_key(x, y);
480 if let Some(cell) = self.cells.get_mut(&key) {
481 cell.retain(|&id| id != entity_id);
482 }
483 }
484
485 pub fn query_radius(&self, cx: i32, cy: i32, radius: i32) -> Vec<u64> {
488 let mut result = Vec::new();
489 let cell_radius = (radius / self.cell_size) + 1;
490 let center_key = self.cell_key(cx, cy);
491 for dx in -cell_radius..=cell_radius {
492 for dy in -cell_radius..=cell_radius {
493 let key = (center_key.0 + dx, center_key.1 + dy);
494 if let Some(cell) = self.cells.get(&key) {
495 result.extend_from_slice(cell);
496 }
497 }
498 }
499 result
500 }
501
502 pub fn query_cell(&self, x: i32, y: i32) -> &[u64] {
504 let key = self.cell_key(x, y);
505 match self.cells.get(&key) {
506 Some(list) => list.as_slice(),
507 None => &[],
508 }
509 }
510
511 pub fn clear(&mut self) {
513 self.cells.clear();
514 }
515
516 pub fn entity_count(&self) -> usize {
518 self.cells.values().map(|c| c.len()).sum()
519 }
520
521 pub fn cell_count(&self) -> usize {
523 self.cells.len()
524 }
525
526 pub fn cell_size(&self) -> i32 {
528 self.cell_size
529 }
530}
531
532#[cfg(test)]
533mod hash_grid_tests {
534 use super::*;
535
536 #[test]
537 fn insert_and_query_finds_entity() {
538 let mut grid = SpatialHashGrid::new(128);
539 grid.insert(42, 100, 100);
540 let found = grid.query_cell(100, 100);
541 assert_eq!(found, &[42]);
542 }
543
544 #[test]
545 fn remove_and_query_does_not_find_entity() {
546 let mut grid = SpatialHashGrid::new(128);
547 grid.insert(42, 100, 100);
548 grid.remove(42, 100, 100);
549 let found = grid.query_cell(100, 100);
550 assert!(found.is_empty());
551 }
552
553 #[test]
554 fn query_radius_returns_only_nearby() {
555 let mut grid = SpatialHashGrid::new(128);
556 grid.insert(1, 0, 0);
558 grid.insert(2, 1280, 1280);
560 let found = grid.query_radius(0, 0, 200);
562 assert!(found.contains(&1));
563 assert!(!found.contains(&2));
564 }
565
566 #[test]
567 fn empty_grid_returns_empty() {
568 let grid = SpatialHashGrid::new(128);
569 assert!(grid.query_cell(0, 0).is_empty());
570 assert!(grid.query_radius(0, 0, 500).is_empty());
571 assert_eq!(grid.entity_count(), 0);
572 assert_eq!(grid.cell_count(), 0);
573 }
574
575 #[test]
576 fn multiple_entities_same_cell() {
577 let mut grid = SpatialHashGrid::new(128);
578 grid.insert(1, 10, 10);
579 grid.insert(2, 50, 50);
580 grid.insert(3, 100, 100);
581 let found = grid.query_cell(0, 0);
583 assert_eq!(found.len(), 3);
584 assert_eq!(grid.entity_count(), 3);
585 assert_eq!(grid.cell_count(), 1);
586 }
587
588 #[test]
589 fn negative_coordinates() {
590 let mut grid = SpatialHashGrid::new(128);
591 grid.insert(99, -200, -300);
592 let found = grid.query_cell(-200, -300);
593 assert_eq!(found, &[99]);
594 assert!(grid.query_cell(200, 300).is_empty());
596 }
597
598 #[test]
599 fn clear_resets_all() {
600 let mut grid = SpatialHashGrid::new(128);
601 grid.insert(1, 0, 0);
602 grid.insert(2, 128, 0);
603 grid.clear();
604 assert_eq!(grid.entity_count(), 0);
605 assert_eq!(grid.cell_count(), 0);
606 }
607
608 #[test]
609 fn custom_cell_size() {
610 let mut grid = SpatialHashGrid::new(64);
611 grid.insert(1, 0, 0); grid.insert(2, 64, 0); grid.insert(3, 63, 0); assert_eq!(grid.query_cell(0, 0).len(), 2); assert_eq!(grid.query_cell(64, 0).len(), 1); assert_eq!(grid.cell_size(), 64);
617 }
618
619 #[test]
620 fn query_radius_covers_adjacent_cells() {
621 let mut grid = SpatialHashGrid::new(128);
622 grid.insert(10, 130, 0);
624 let found = grid.query_radius(0, 0, 200);
626 assert!(found.contains(&10));
627 }
628}
629
630#[cfg(test)]
631mod grid_tests {
632 use super::*;
633
634 #[test]
635 fn insert_and_query_same_cell() {
636 let mut grid = SpatialGrid::new();
637 grid.insert("e1", 0, 0);
638 grid.insert("e2", 50, 50); let found = grid.query_cell(0, 0);
640 assert_eq!(found.len(), 2);
641 }
642
643 #[test]
644 fn insert_different_cells() {
645 let mut grid = SpatialGrid::new();
646 grid.insert("e1", 0, 0); grid.insert("e2", 128, 0); assert_eq!(grid.query_cell(0, 0).len(), 1);
649 assert_eq!(grid.query_cell(128, 0).len(), 1);
650 assert_eq!(grid.cell_count(), 2);
651 }
652
653 #[test]
654 fn query_empty_cell_returns_empty_slice() {
655 let grid = SpatialGrid::new();
656 assert!(grid.query_cell(999, 999).is_empty());
657 }
658
659 #[test]
660 fn remove_entity() {
661 let mut grid = SpatialGrid::new();
662 grid.insert("e1", 0, 0);
663 grid.insert("e2", 0, 0);
664 grid.remove("e1", 0, 0);
665 let found = grid.query_cell(0, 0);
666 assert_eq!(found.len(), 1);
667 assert_eq!(found[0], "e2");
668 }
669
670 #[test]
671 fn remove_nonexistent_is_noop() {
672 let mut grid = SpatialGrid::new();
673 grid.insert("e1", 0, 0);
674 grid.remove("e2", 0, 0); assert_eq!(grid.query_cell(0, 0).len(), 1);
676 }
677
678 #[test]
679 fn query_neighbors_returns_all_9_cells() {
680 let mut grid = SpatialGrid::new();
681 grid.insert("c", 64, 64); grid.insert("n", 64, -64); grid.insert("s", 64, 192); grid.insert("w", -64, 64); grid.insert("e", 192, 64); grid.insert("nw", -64, -64); grid.insert("ne", 192, -64); grid.insert("sw", -64, 192); grid.insert("se", 192, 192); let neighbors = grid.query_neighbors(0, 0);
692 assert_eq!(neighbors.len(), 9);
693 }
694
695 #[test]
696 fn entity_count_tracks_insertions() {
697 let mut grid = SpatialGrid::new();
698 grid.insert("a", 0, 0);
699 grid.insert("b", 0, 0);
700 grid.insert("c", 128, 0);
701 assert_eq!(grid.entity_count(), 3);
702 }
703
704 #[test]
705 fn clear_resets_grid() {
706 let mut grid = SpatialGrid::new();
707 grid.insert("a", 0, 0);
708 grid.clear();
709 assert_eq!(grid.entity_count(), 0);
710 assert_eq!(grid.cell_count(), 0);
711 }
712
713 #[test]
714 fn negative_coords_map_correctly() {
715 let mut grid = SpatialGrid::new();
716 grid.insert("neg", -1, -1); assert_eq!(grid.query_cell(-1, -1).len(), 1);
718 assert_eq!(grid.query_cell(0, 0).len(), 0);
719 }
720
721 #[test]
722 fn with_capacity_works() {
723 let mut grid = SpatialGrid::with_capacity(1000);
724 grid.insert("e", 0, 0);
725 assert_eq!(grid.entity_count(), 1);
726 }
727
728 #[test]
731 fn cell_coord_negative_one() {
732 assert_eq!(cell_coord(-1), -1);
734 }
735
736 #[test]
737 fn cell_coord_negative_128() {
738 assert_eq!(cell_coord(-128), -1);
740 }
741
742 #[test]
743 fn cell_coord_negative_129() {
744 assert_eq!(cell_coord(-129), -2);
746 }
747
748 #[test]
749 fn cell_coord_i32_min() {
750 let result = cell_coord(i32::MIN);
752 assert_eq!(result, -16777216);
754 }
755
756 #[test]
757 fn grid_negative_coordinates() {
758 let mut grid = SpatialGrid::new();
759 grid.insert("neg_entity", -200, -300);
760 let found = grid.query_cell(-200, -300);
762 assert_eq!(found.len(), 1);
763 assert_eq!(found[0], "neg_entity");
764
765 assert!(grid.query_cell(200, 300).is_empty());
767 }
768
769 #[test]
770 fn grid_negative_positive_boundary() {
771 let mut grid = SpatialGrid::new();
772 grid.insert("boundary", -1, -1);
774 assert_eq!(grid.query_cell(-1, -1).len(), 1);
775 assert_eq!(grid.query_cell(0, 0).len(), 0);
776 }
777
778 #[test]
779 fn cell_coord_negative_exact_boundary() {
780 assert_eq!(cell_coord(-256), -2);
782 assert_eq!(cell_coord(-257), -3);
784 }
785}
786
787#[cfg(test)]
788mod visibility_tests {
789 use super::*;
790
791 #[test]
792 fn visibility_mask_all_visible() {
793 let fov = vec![(0, 0), (1, 0), (0, 1)];
794 let meshlets = vec![(0, 0), (1, 0)];
795 let mask = compute_observer_visibility_mask(&fov, &meshlets);
796 assert_eq!(mask, vec![1, 1]);
797 }
798
799 #[test]
800 fn visibility_mask_none_visible() {
801 let fov = vec![(10, 10)];
802 let meshlets = vec![(0, 0), (1, 1)];
803 let mask = compute_observer_visibility_mask(&fov, &meshlets);
804 assert_eq!(mask, vec![0, 0]);
805 }
806
807 #[test]
808 fn visibility_mask_partial() {
809 let fov = vec![(0, 0), (1, 0)];
810 let meshlets = vec![(0, 0), (5, 5), (1, 0)];
811 let mask = compute_observer_visibility_mask(&fov, &meshlets);
812 assert_eq!(mask, vec![1, 0, 1]);
813 }
814
815 #[test]
816 fn fill_visibility_mask_zero_alloc() {
817 let fov = vec![(0, 0), (2, 2)];
818 let meshlets = vec![(0, 0), (1, 1), (2, 2)];
819 let mut out = vec![0u32; 3];
820 fill_observer_visibility_mask(&fov, &meshlets, &mut out);
821 assert_eq!(out, vec![1, 0, 1]);
822 }
823
824 #[test]
825 fn visibility_mask_empty_fov() {
826 let fov: Vec<(i32, i32)> = vec![];
827 let meshlets = vec![(0, 0)];
828 let mask = compute_observer_visibility_mask(&fov, &meshlets);
829 assert_eq!(mask, vec![0]);
830 }
831}
832
833fn find_min_f(open: &[AStarNode]) -> Option<usize> {
835 if open.is_empty() {
836 return None;
837 }
838 let mut min_idx = 0;
839 let mut min_f = open[0].f;
840 for (i, node) in open.iter().enumerate().skip(1) {
841 if node.f < min_f {
842 min_f = node.f;
843 min_idx = i;
844 }
845 }
846 Some(min_idx)
847}