1use glam::Vec2;
17use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
18use std::cmp::Ordering;
19
20#[derive(Debug, Clone)]
26pub struct PathNode {
27 pub pos: Vec2,
28 pub g_cost: f32,
29 pub h_cost: f32,
30 pub f_cost: f32,
31 pub parent: Option<usize>,
32}
33
34impl PathNode {
35 pub fn new(pos: Vec2, g_cost: f32, h_cost: f32, parent: Option<usize>) -> Self {
36 PathNode {
37 pos,
38 g_cost,
39 h_cost,
40 f_cost: g_cost + h_cost,
41 parent,
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48struct HeapNode {
49 f_cost: f32,
50 g_cost: f32,
51 index: usize,
52}
53
54impl PartialEq for HeapNode {
55 fn eq(&self, other: &Self) -> bool {
56 self.f_cost == other.f_cost
57 }
58}
59impl Eq for HeapNode {}
60impl PartialOrd for HeapNode {
61 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
62 Some(self.cmp(other))
63 }
64}
65impl Ord for HeapNode {
66 fn cmp(&self, other: &Self) -> Ordering {
67 other.f_cost.partial_cmp(&self.f_cost)
69 .unwrap_or(Ordering::Equal)
70 .then_with(|| other.g_cost.partial_cmp(&self.g_cost).unwrap_or(Ordering::Equal))
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct PathGrid {
77 pub width: usize,
78 pub height: usize,
79 pub cell_size: f32,
80 pub walkable: Vec<bool>,
81 pub weights: Vec<f32>,
82 pub origin: Vec2,
83}
84
85impl PathGrid {
86 pub fn new(width: usize, height: usize, cell_size: f32) -> Self {
88 let n = width * height;
89 PathGrid {
90 width,
91 height,
92 cell_size,
93 walkable: vec![true; n],
94 weights: vec![1.0; n],
95 origin: Vec2::ZERO,
96 }
97 }
98
99 pub fn with_origin(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
101 let mut g = Self::new(width, height, cell_size);
102 g.origin = origin;
103 g
104 }
105
106 pub fn set_walkable(&mut self, x: usize, y: usize, walkable: bool) {
108 if let Some(idx) = self.index(x, y) {
109 self.walkable[idx] = walkable;
110 }
111 }
112
113 pub fn set_weight(&mut self, x: usize, y: usize, weight: f32) {
115 if let Some(idx) = self.index(x, y) {
116 self.weights[idx] = weight.max(0.001);
117 }
118 }
119
120 pub fn world_to_grid(&self, pos: Vec2) -> (usize, usize) {
122 let local = pos - self.origin;
123 let x = ((local.x / self.cell_size).floor() as isize).clamp(0, self.width as isize - 1) as usize;
124 let y = ((local.y / self.cell_size).floor() as isize).clamp(0, self.height as isize - 1) as usize;
125 (x, y)
126 }
127
128 pub fn grid_to_world(&self, x: usize, y: usize) -> Vec2 {
130 self.origin + Vec2::new(
131 x as f32 * self.cell_size + self.cell_size * 0.5,
132 y as f32 * self.cell_size + self.cell_size * 0.5,
133 )
134 }
135
136 pub fn is_walkable(&self, x: usize, y: usize) -> bool {
138 match self.index(x, y) {
139 Some(idx) => self.walkable[idx],
140 None => false,
141 }
142 }
143
144 pub fn weight(&self, x: usize, y: usize) -> f32 {
146 match self.index(x, y) {
147 Some(idx) => self.weights[idx],
148 None => f32::INFINITY,
149 }
150 }
151
152 pub fn neighbors(&self, x: usize, y: usize) -> Vec<(usize, usize, f32)> {
155 let mut result = Vec::with_capacity(8);
156 let dirs: &[(i32, i32, bool)] = &[
157 (-1, 0, false), (1, 0, false), (0, -1, false), (0, 1, false),
158 (-1, -1, true), (1, -1, true), (-1, 1, true), (1, 1, true),
159 ];
160 for &(dx, dy, diagonal) in dirs {
161 let nx = x as i32 + dx;
162 let ny = y as i32 + dy;
163 if nx < 0 || ny < 0 || nx >= self.width as i32 || ny >= self.height as i32 {
164 continue;
165 }
166 let nx = nx as usize;
167 let ny = ny as usize;
168 if !self.is_walkable(nx, ny) {
169 continue;
170 }
171 if diagonal {
173 if !self.is_walkable(x, ny) || !self.is_walkable(nx, y) {
174 continue;
175 }
176 let cost = std::f32::consts::SQRT_2 * self.weight(nx, ny);
177 result.push((nx, ny, cost));
178 } else {
179 result.push((nx, ny, self.weight(nx, ny)));
180 }
181 }
182 result
183 }
184
185 pub fn neighbors_cardinal(&self, x: usize, y: usize) -> Vec<(usize, usize, f32)> {
187 let mut result = Vec::with_capacity(4);
188 let dirs: &[(i32, i32)] = &[(-1, 0), (1, 0), (0, -1), (0, 1)];
189 for &(dx, dy) in dirs {
190 let nx = x as i32 + dx;
191 let ny = y as i32 + dy;
192 if nx < 0 || ny < 0 || nx >= self.width as i32 || ny >= self.height as i32 {
193 continue;
194 }
195 let nx = nx as usize;
196 let ny = ny as usize;
197 if self.is_walkable(nx, ny) {
198 result.push((nx, ny, self.weight(nx, ny)));
199 }
200 }
201 result
202 }
203
204 pub fn line_of_sight(&self, x0: usize, y0: usize, x1: usize, y1: usize) -> bool {
206 let mut x0 = x0 as i32;
207 let mut y0 = y0 as i32;
208 let x1 = x1 as i32;
209 let y1 = y1 as i32;
210 let dx = (x1 - x0).abs();
211 let dy = (y1 - y0).abs();
212 let sx = if x0 < x1 { 1 } else { -1 };
213 let sy = if y0 < y1 { 1 } else { -1 };
214 let mut err = dx - dy;
215 loop {
216 if x0 == x1 && y0 == y1 { break; }
217 if !self.is_walkable(x0 as usize, y0 as usize) {
218 return false;
219 }
220 let e2 = 2 * err;
221 if e2 > -dy { err -= dy; x0 += sx; }
222 if e2 < dx { err += dx; y0 += sy; }
223 }
224 true
225 }
226
227 fn index(&self, x: usize, y: usize) -> Option<usize> {
228 if x < self.width && y < self.height {
229 Some(y * self.width + x)
230 } else {
231 None
232 }
233 }
234
235 pub fn len(&self) -> usize { self.width * self.height }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Default)]
245pub enum Heuristic {
246 #[default]
247 Manhattan,
248 Euclidean,
249 Chebyshev,
250 Octile,
251}
252
253impl Heuristic {
254 pub fn compute(self, ax: usize, ay: usize, bx: usize, by: usize) -> f32 {
255 let dx = (ax as f32 - bx as f32).abs();
256 let dy = (ay as f32 - by as f32).abs();
257 match self {
258 Heuristic::Manhattan => dx + dy,
259 Heuristic::Euclidean => (dx * dx + dy * dy).sqrt(),
260 Heuristic::Chebyshev => dx.max(dy),
261 Heuristic::Octile => {
262 let min = dx.min(dy);
263 let max = dx.max(dy);
264 std::f32::consts::SQRT_2 * min + (max - min)
265 }
266 }
267 }
268}
269
270#[derive(Debug, Clone, Default)]
276pub struct Path {
277 pub waypoints: Vec<Vec2>,
278 pub total_cost: f32,
279}
280
281impl Path {
282 pub fn new(waypoints: Vec<Vec2>, total_cost: f32) -> Self {
283 Path { waypoints, total_cost }
284 }
285 pub fn is_empty(&self) -> bool { self.waypoints.is_empty() }
286 pub fn len(&self) -> usize { self.waypoints.len() }
287}
288
289#[derive(Debug, Clone)]
291pub struct PathRequest {
292 pub start: Vec2,
293 pub end: Vec2,
294 pub agent_radius: f32,
295 pub heuristic: Heuristic,
296 pub allow_partial: bool,
297}
298
299impl PathRequest {
300 pub fn new(start: Vec2, end: Vec2) -> Self {
301 PathRequest {
302 start,
303 end,
304 agent_radius: 0.0,
305 heuristic: Heuristic::Octile,
306 allow_partial: false,
307 }
308 }
309}
310
311#[derive(Debug, Clone)]
313pub enum PathResult {
314 Found(Path),
315 Partial(Path),
316 NoPath,
317 InvalidRequest(String),
318}
319
320#[derive(Debug, Clone, Default)]
322pub struct PathfindingStats {
323 pub nodes_expanded: usize,
324 pub nodes_generated: usize,
325 pub path_length: usize,
326 pub duration_us: u64,
327}
328
329#[derive(Debug, Clone)]
335pub struct AStarPathfinder {
336 pub heuristic: Heuristic,
337 pub allow_diagonal: bool,
338 pub smooth_result: bool,
339}
340
341impl AStarPathfinder {
342 pub fn new() -> Self {
343 AStarPathfinder {
344 heuristic: Heuristic::Octile,
345 allow_diagonal: true,
346 smooth_result: true,
347 }
348 }
349
350 pub fn with_heuristic(mut self, h: Heuristic) -> Self { self.heuristic = h; self }
351 pub fn with_diagonal(mut self, v: bool) -> Self { self.allow_diagonal = v; self }
352 pub fn with_smoothing(mut self, v: bool) -> Self { self.smooth_result = v; self }
353
354 pub fn find_path(&self, grid: &PathGrid, start: Vec2, end: Vec2) -> Option<Vec<Vec2>> {
356 let (sx, sy) = grid.world_to_grid(start);
357 let (ex, ey) = grid.world_to_grid(end);
358
359 if !grid.is_walkable(sx, sy) || !grid.is_walkable(ex, ey) {
360 return None;
361 }
362 if sx == ex && sy == ey {
363 return Some(vec![start, end]);
364 }
365
366 let start_idx = sy * grid.width + sx;
367 let end_idx = ey * grid.width + ex;
368
369 let mut g_costs: Vec<f32> = vec![f32::INFINITY; grid.len()];
371 let mut parents: Vec<Option<usize>> = vec![None; grid.len()];
372 let mut open: BinaryHeap<HeapNode> = BinaryHeap::new();
373 let mut closed: HashSet<usize> = HashSet::new();
374
375 g_costs[start_idx] = 0.0;
376 let h = self.heuristic.compute(sx, sy, ex, ey);
377 open.push(HeapNode { f_cost: h, g_cost: 0.0, index: start_idx });
378
379 while let Some(current) = open.pop() {
380 let idx = current.index;
381 if closed.contains(&idx) { continue; }
382 closed.insert(idx);
383
384 if idx == end_idx {
385 let mut path_indices = Vec::new();
387 let mut cur = idx;
388 loop {
389 path_indices.push(cur);
390 match parents[cur] {
391 Some(p) => cur = p,
392 None => break,
393 }
394 }
395 path_indices.reverse();
396 let mut waypoints: Vec<Vec2> = path_indices.iter().map(|&i| {
397 let gx = i % grid.width;
398 let gy = i / grid.width;
399 grid.grid_to_world(gx, gy)
400 }).collect();
401 if !waypoints.is_empty() { waypoints[0] = start; }
403 if waypoints.len() > 1 { *waypoints.last_mut().unwrap() = end; }
404
405 if self.smooth_result {
406 waypoints = smooth_path(waypoints, grid);
407 }
408 return Some(waypoints);
409 }
410
411 let cx = idx % grid.width;
412 let cy = idx / grid.width;
413 let neighbors = if self.allow_diagonal {
414 grid.neighbors(cx, cy)
415 } else {
416 grid.neighbors_cardinal(cx, cy)
417 };
418
419 for (nx, ny, move_cost) in neighbors {
420 let nidx = ny * grid.width + nx;
421 if closed.contains(&nidx) { continue; }
422 let tentative_g = g_costs[idx] + move_cost;
423 if tentative_g < g_costs[nidx] {
424 g_costs[nidx] = tentative_g;
425 parents[nidx] = Some(idx);
426 let h = self.heuristic.compute(nx, ny, ex, ey);
427 open.push(HeapNode {
428 f_cost: tentative_g + h,
429 g_cost: tentative_g,
430 index: nidx,
431 });
432 }
433 }
434 }
435 None
436 }
437
438 pub fn find_path_detailed(
440 &self,
441 grid: &PathGrid,
442 request: &PathRequest,
443 ) -> (PathResult, PathfindingStats) {
444 let mut stats = PathfindingStats::default();
445 let (sx, sy) = grid.world_to_grid(request.start);
446 let (ex, ey) = grid.world_to_grid(request.end);
447
448 if !grid.is_walkable(sx, sy) || !grid.is_walkable(ex, ey) {
449 return (PathResult::NoPath, stats);
450 }
451
452 let start_idx = sy * grid.width + sx;
453 let end_idx = ey * grid.width + ex;
454 let mut g_costs: Vec<f32> = vec![f32::INFINITY; grid.len()];
455 let mut parents: Vec<Option<usize>> = vec![None; grid.len()];
456 let mut open: BinaryHeap<HeapNode> = BinaryHeap::new();
457 let mut closed: HashSet<usize> = HashSet::new();
458 let mut best_idx = start_idx;
460 let mut best_h = request.heuristic.compute(sx, sy, ex, ey);
461
462 g_costs[start_idx] = 0.0;
463 open.push(HeapNode { f_cost: best_h, g_cost: 0.0, index: start_idx });
464 stats.nodes_generated += 1;
465
466 while let Some(current) = open.pop() {
467 let idx = current.index;
468 if closed.contains(&idx) { continue; }
469 closed.insert(idx);
470 stats.nodes_expanded += 1;
471
472 let cx = idx % grid.width;
473 let cy = idx / grid.width;
474 let h = request.heuristic.compute(cx, cy, ex, ey);
475 if h < best_h { best_h = h; best_idx = idx; }
476
477 if idx == end_idx {
478 let path = self.reconstruct(grid, &parents, idx, request.start, request.end);
479 stats.path_length = path.len();
480 return (PathResult::Found(Path::new(path, g_costs[idx])), stats);
481 }
482
483 let neighbors = if self.allow_diagonal {
484 grid.neighbors(cx, cy)
485 } else {
486 grid.neighbors_cardinal(cx, cy)
487 };
488 for (nx, ny, move_cost) in neighbors {
489 let nidx = ny * grid.width + nx;
490 if closed.contains(&nidx) { continue; }
491 let tentative_g = g_costs[idx] + move_cost;
492 if tentative_g < g_costs[nidx] {
493 g_costs[nidx] = tentative_g;
494 parents[nidx] = Some(idx);
495 let h2 = request.heuristic.compute(nx, ny, ex, ey);
496 open.push(HeapNode { f_cost: tentative_g + h2, g_cost: tentative_g, index: nidx });
497 stats.nodes_generated += 1;
498 }
499 }
500 }
501
502 if request.allow_partial && best_idx != start_idx {
503 let bx = best_idx % grid.width;
504 let by = best_idx / grid.width;
505 let partial_end = grid.grid_to_world(bx, by);
506 let path = self.reconstruct(grid, &parents, best_idx, request.start, partial_end);
507 stats.path_length = path.len();
508 return (PathResult::Partial(Path::new(path, g_costs[best_idx])), stats);
509 }
510
511 (PathResult::NoPath, stats)
512 }
513
514 fn reconstruct(
515 &self,
516 grid: &PathGrid,
517 parents: &[Option<usize>],
518 end_idx: usize,
519 start_world: Vec2,
520 end_world: Vec2,
521 ) -> Vec<Vec2> {
522 let mut indices = Vec::new();
523 let mut cur = end_idx;
524 loop {
525 indices.push(cur);
526 match parents[cur] {
527 Some(p) => cur = p,
528 None => break,
529 }
530 }
531 indices.reverse();
532 let mut waypoints: Vec<Vec2> = indices.iter().map(|&i| {
533 grid.grid_to_world(i % grid.width, i / grid.width)
534 }).collect();
535 if !waypoints.is_empty() { waypoints[0] = start_world; }
536 if waypoints.len() > 1 { *waypoints.last_mut().unwrap() = end_world; }
537 if self.smooth_result {
538 waypoints = smooth_path(waypoints, grid);
539 }
540 waypoints
541 }
542}
543
544impl Default for AStarPathfinder {
545 fn default() -> Self { Self::new() }
546}
547
548pub fn smooth_path(path: Vec<Vec2>, grid: &PathGrid) -> Vec<Vec2> {
554 if path.len() <= 2 { return path; }
555 let mut smoothed = Vec::with_capacity(path.len());
556 smoothed.push(path[0]);
557 let mut current = 0usize;
558 while current < path.len() - 1 {
559 let mut furthest = current + 1;
560 for next in (current + 2)..path.len() {
561 let (ax, ay) = grid.world_to_grid(path[current]);
562 let (bx, by) = grid.world_to_grid(path[next]);
563 if grid.line_of_sight(ax, ay, bx, by) {
564 furthest = next;
565 } else {
566 break;
567 }
568 }
569 smoothed.push(path[furthest]);
570 current = furthest;
571 }
572 smoothed
573}
574
575pub fn spline_path(path: &[Vec2], samples_per_segment: usize) -> Vec<Vec2> {
577 if path.len() < 2 { return path.to_vec(); }
578 let mut result = Vec::new();
579 for i in 0..path.len().saturating_sub(1) {
580 let p0 = if i == 0 { path[0] } else { path[i - 1] };
581 let p1 = path[i];
582 let p2 = path[i + 1];
583 let p3 = if i + 2 < path.len() { path[i + 2] } else { path[i + 1] };
584 for s in 0..samples_per_segment {
585 let t = s as f32 / samples_per_segment as f32;
586 let t2 = t * t;
587 let t3 = t2 * t;
588 let v = 0.5 * (
589 (2.0 * p1)
590 + (-p0 + p2) * t
591 + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
592 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
593 );
594 result.push(v);
595 }
596 }
597 result.push(*path.last().unwrap());
598 result
599}
600
601#[derive(Debug, Clone)]
608pub struct DijkstraMap {
609 pub width: usize,
610 pub height: usize,
611 pub distances: Vec<f32>,
612 pub flow: Vec<Option<(usize, usize)>>, }
614
615impl DijkstraMap {
616 pub fn new(width: usize, height: usize) -> Self {
618 let n = width * height;
619 DijkstraMap {
620 width,
621 height,
622 distances: vec![f32::INFINITY; n],
623 flow: vec![None; n],
624 }
625 }
626
627 pub fn build(&mut self, grid: &PathGrid, goals: &[Vec2]) {
629 self.distances.fill(f32::INFINITY);
630 self.flow.fill(None);
631
632 let mut queue: VecDeque<usize> = VecDeque::new();
633 for &goal in goals {
634 let (gx, gy) = grid.world_to_grid(goal);
635 if grid.is_walkable(gx, gy) {
636 let idx = gy * grid.width + gx;
637 self.distances[idx] = 0.0;
638 queue.push_back(idx);
639 }
640 }
641
642 while let Some(idx) = queue.pop_front() {
643 let cx = idx % grid.width;
644 let cy = idx / grid.width;
645 for (nx, ny, cost) in grid.neighbors(cx, cy) {
646 let nidx = ny * grid.width + nx;
647 let new_dist = self.distances[idx] + cost;
648 if new_dist < self.distances[nidx] {
649 self.distances[nidx] = new_dist;
650 self.flow[nidx] = Some((cx, cy));
651 queue.push_back(nidx);
652 }
653 }
654 }
655 }
656
657 pub fn build_from_cells(&mut self, grid: &PathGrid, goals: &[(usize, usize)]) {
659 self.distances.fill(f32::INFINITY);
660 self.flow.fill(None);
661
662 let mut queue: VecDeque<usize> = VecDeque::new();
663 for &(gx, gy) in goals {
664 if grid.is_walkable(gx, gy) {
665 let idx = gy * grid.width + gx;
666 self.distances[idx] = 0.0;
667 queue.push_back(idx);
668 }
669 }
670
671 while let Some(idx) = queue.pop_front() {
672 let cx = idx % grid.width;
673 let cy = idx / grid.width;
674 for (nx, ny, cost) in grid.neighbors(cx, cy) {
675 let nidx = ny * grid.width + nx;
676 let new_dist = self.distances[idx] + cost;
677 if new_dist < self.distances[nidx] {
678 self.distances[nidx] = new_dist;
679 self.flow[nidx] = Some((cx, cy));
680 queue.push_back(nidx);
681 }
682 }
683 }
684 }
685
686 pub fn distance_at(&self, grid: &PathGrid, pos: Vec2) -> f32 {
688 let (x, y) = grid.world_to_grid(pos);
689 self.distances[y * self.width + x]
690 }
691
692 pub fn next_step(&self, grid: &PathGrid, pos: Vec2) -> Option<Vec2> {
694 let (x, y) = grid.world_to_grid(pos);
695 let idx = y * self.width + x;
696 self.flow[idx].map(|(nx, ny)| grid.grid_to_world(nx, ny))
697 }
698
699 pub fn extract_path(&self, grid: &PathGrid, start: Vec2) -> Vec<Vec2> {
701 let mut path = Vec::new();
702 let (mut cx, mut cy) = grid.world_to_grid(start);
703 let mut visited: HashSet<usize> = HashSet::new();
704 loop {
705 let idx = cy * self.width + cx;
706 if visited.contains(&idx) { break; }
707 visited.insert(idx);
708 path.push(grid.grid_to_world(cx, cy));
709 if self.distances[idx] == 0.0 { break; }
710 match self.flow[idx] {
711 Some((nx, ny)) => { cx = nx; cy = ny; }
712 None => break,
713 }
714 }
715 path
716 }
717}
718
719#[derive(Debug, Clone, Default)]
726pub struct JumpPointSearch {
727 pub heuristic: Heuristic,
728}
729
730impl JumpPointSearch {
731 pub fn new() -> Self { JumpPointSearch { heuristic: Heuristic::Octile } }
732
733 pub fn find_path(&self, grid: &PathGrid, start: Vec2, end: Vec2) -> Option<Vec<Vec2>> {
735 let (sx, sy) = grid.world_to_grid(start);
736 let (ex, ey) = grid.world_to_grid(end);
737 if !grid.is_walkable(sx, sy) || !grid.is_walkable(ex, ey) {
738 return None;
739 }
740 if sx == ex && sy == ey {
741 return Some(vec![start, end]);
742 }
743
744 let start_idx = sy * grid.width + sx;
745 let end_idx = ey * grid.width + ex;
746 let mut g_costs: Vec<f32> = vec![f32::INFINITY; grid.len()];
747 let mut parents: Vec<Option<usize>> = vec![None; grid.len()];
748 let mut open: BinaryHeap<HeapNode> = BinaryHeap::new();
749 let mut closed: HashSet<usize> = HashSet::new();
750
751 g_costs[start_idx] = 0.0;
752 let h = self.heuristic.compute(sx, sy, ex, ey);
753 open.push(HeapNode { f_cost: h, g_cost: 0.0, index: start_idx });
754
755 while let Some(current) = open.pop() {
756 let idx = current.index;
757 if closed.contains(&idx) { continue; }
758 closed.insert(idx);
759
760 if idx == end_idx {
761 return Some(self.reconstruct(grid, &parents, idx, start, end));
762 }
763
764 let cx = idx % grid.width;
765 let cy = idx / grid.width;
766 let parent_idx = parents[idx];
767 let jump_points = self.identify_successors(grid, cx, cy, parent_idx, ex, ey);
768
769 for (jx, jy) in jump_points {
770 let jidx = jy * grid.width + jx;
771 if closed.contains(&jidx) { continue; }
772 let dist = ((jx as f32 - cx as f32).powi(2) + (jy as f32 - cy as f32).powi(2)).sqrt();
773 let tentative_g = g_costs[idx] + dist;
774 if tentative_g < g_costs[jidx] {
775 g_costs[jidx] = tentative_g;
776 parents[jidx] = Some(idx);
777 let h2 = self.heuristic.compute(jx, jy, ex, ey);
778 open.push(HeapNode { f_cost: tentative_g + h2, g_cost: tentative_g, index: jidx });
779 }
780 }
781 }
782 None
783 }
784
785 fn identify_successors(
786 &self, grid: &PathGrid,
787 x: usize, y: usize,
788 parent: Option<usize>,
789 ex: usize, ey: usize,
790 ) -> Vec<(usize, usize)> {
791 let mut successors = Vec::new();
792 let neighbors = self.prune_neighbors(grid, x, y, parent);
793 for (nx, ny) in neighbors {
794 let dx = (nx as i32 - x as i32).signum();
795 let dy = (ny as i32 - y as i32).signum();
796 if let Some((jx, jy)) = self.jump(grid, x as i32, y as i32, dx, dy, ex, ey) {
797 successors.push((jx as usize, jy as usize));
798 }
799 }
800 successors
801 }
802
803 fn prune_neighbors(
804 &self, grid: &PathGrid,
805 x: usize, y: usize,
806 parent: Option<usize>,
807 ) -> Vec<(usize, usize)> {
808 let Some(pidx) = parent else {
809 return grid.neighbors(x, y).into_iter().map(|(nx, ny, _)| (nx, ny)).collect();
811 };
812 let px = pidx % grid.width;
813 let py = pidx / grid.width;
814 let dx = (x as i32 - px as i32).signum();
815 let dy = (y as i32 - py as i32).signum();
816 let mut result = Vec::new();
817
818 if dx != 0 && dy != 0 {
819 if grid.is_walkable((x as i32 + dx) as usize, y) {
821 result.push(((x as i32 + dx) as usize, y));
822 }
823 if grid.is_walkable(x, (y as i32 + dy) as usize) {
824 result.push((x, (y as i32 + dy) as usize));
825 }
826 if grid.is_walkable((x as i32 + dx) as usize, (y as i32 + dy) as usize) {
827 result.push(((x as i32 + dx) as usize, (y as i32 + dy) as usize));
828 }
829 if !grid.is_walkable((x as i32 - dx) as usize, y)
831 && grid.is_walkable((x as i32 - dx) as usize, (y as i32 + dy) as usize)
832 {
833 result.push(((x as i32 - dx) as usize, (y as i32 + dy) as usize));
834 }
835 if !grid.is_walkable(x, (y as i32 - dy) as usize)
836 && grid.is_walkable((x as i32 + dx) as usize, (y as i32 - dy) as usize)
837 {
838 result.push(((x as i32 + dx) as usize, (y as i32 - dy) as usize));
839 }
840 } else if dx != 0 {
841 if grid.is_walkable((x as i32 + dx) as usize, y) {
843 result.push(((x as i32 + dx) as usize, y));
844 }
845 if !grid.is_walkable(x, y + 1) && grid.is_walkable((x as i32 + dx) as usize, y + 1) {
846 result.push(((x as i32 + dx) as usize, y + 1));
847 }
848 if y > 0 && !grid.is_walkable(x, y - 1) && grid.is_walkable((x as i32 + dx) as usize, y - 1) {
849 result.push(((x as i32 + dx) as usize, y - 1));
850 }
851 } else {
852 if grid.is_walkable(x, (y as i32 + dy) as usize) {
854 result.push((x, (y as i32 + dy) as usize));
855 }
856 if !grid.is_walkable(x + 1, y) && grid.is_walkable(x + 1, (y as i32 + dy) as usize) {
857 result.push((x + 1, (y as i32 + dy) as usize));
858 }
859 if x > 0 && !grid.is_walkable(x - 1, y) && grid.is_walkable(x - 1, (y as i32 + dy) as usize) {
860 result.push((x - 1, (y as i32 + dy) as usize));
861 }
862 }
863 result
864 }
865
866 fn jump(
867 &self, grid: &PathGrid,
868 x: i32, y: i32,
869 dx: i32, dy: i32,
870 ex: usize, ey: usize,
871 ) -> Option<(i32, i32)> {
872 let nx = x + dx;
873 let ny = y + dy;
874 if nx < 0 || ny < 0 || nx >= grid.width as i32 || ny >= grid.height as i32 {
875 return None;
876 }
877 if !grid.is_walkable(nx as usize, ny as usize) { return None; }
878 if nx as usize == ex && ny as usize == ey { return Some((nx, ny)); }
879
880 if dx != 0 && dy != 0 {
882 if self.jump(grid, nx, ny, dx, 0, ex, ey).is_some()
884 || self.jump(grid, nx, ny, 0, dy, ex, ey).is_some()
885 {
886 return Some((nx, ny));
887 }
888 } else if dx != 0 {
889 if (ny + 1 < grid.height as i32 && !grid.is_walkable(nx as usize, ny as usize + 1)
890 && grid.is_walkable((nx + dx) as usize, ny as usize + 1))
891 || (ny - 1 >= 0 && !grid.is_walkable(nx as usize, (ny - 1) as usize)
892 && grid.is_walkable((nx + dx) as usize, (ny - 1) as usize))
893 {
894 return Some((nx, ny));
895 }
896 } else {
897 if (nx + 1 < grid.width as i32 && !grid.is_walkable(nx as usize + 1, ny as usize)
898 && grid.is_walkable(nx as usize + 1, (ny + dy) as usize))
899 || (nx - 1 >= 0 && !grid.is_walkable((nx - 1) as usize, ny as usize)
900 && grid.is_walkable((nx - 1) as usize, (ny + dy) as usize))
901 {
902 return Some((nx, ny));
903 }
904 }
905
906 if dx != 0 && dy != 0 {
907 self.jump(grid, nx, ny, dx, dy, ex, ey)
908 } else {
909 self.jump(grid, nx, ny, dx, dy, ex, ey)
910 }
911 }
912
913 fn reconstruct(
914 &self, grid: &PathGrid,
915 parents: &[Option<usize>],
916 end_idx: usize,
917 start: Vec2, end: Vec2,
918 ) -> Vec<Vec2> {
919 let mut indices = Vec::new();
920 let mut cur = end_idx;
921 loop {
922 indices.push(cur);
923 match parents[cur] {
924 Some(p) => cur = p,
925 None => break,
926 }
927 }
928 indices.reverse();
929 let mut waypoints = Vec::new();
931 for window in indices.windows(2) {
932 let ax = window[0] % grid.width;
933 let ay = window[0] / grid.width;
934 let bx = window[1] % grid.width;
935 let by = window[1] / grid.width;
936 waypoints.push(grid.grid_to_world(ax, ay));
937 let mut cx = ax as i32;
939 let mut cy = ay as i32;
940 let dxs = (bx as i32 - ax as i32).signum();
941 let dys = (by as i32 - ay as i32).signum();
942 while cx as usize != bx || cy as usize != by {
943 cx += dxs;
944 cy += dys;
945 waypoints.push(grid.grid_to_world(cx as usize, cy as usize));
946 }
947 }
948 if indices.len() == 1 {
949 let ax = indices[0] % grid.width;
950 let ay = indices[0] / grid.width;
951 waypoints.push(grid.grid_to_world(ax, ay));
952 }
953 if !waypoints.is_empty() { waypoints[0] = start; }
954 if waypoints.len() > 1 { *waypoints.last_mut().unwrap() = end; }
955 waypoints
956 }
957}
958
959#[derive(Debug, Clone)]
965pub struct HierarchyChunk {
966 pub chunk_x: usize,
967 pub chunk_y: usize,
968 pub center: Vec2,
969 pub walkable: bool,
970 pub portal_cells: Vec<(usize, usize)>,
971}
972
973#[derive(Debug, Clone)]
976pub struct HierarchicalPathfinder {
977 pub chunk_size: usize,
978 fine_finder: AStarPathfinder,
979}
980
981impl HierarchicalPathfinder {
982 pub fn new(chunk_size: usize) -> Self {
983 HierarchicalPathfinder {
984 chunk_size,
985 fine_finder: AStarPathfinder::new(),
986 }
987 }
988
989 pub fn build_chunks(&self, grid: &PathGrid) -> Vec<HierarchyChunk> {
991 let cw = (grid.width + self.chunk_size - 1) / self.chunk_size;
992 let ch = (grid.height + self.chunk_size - 1) / self.chunk_size;
993 let mut chunks = Vec::with_capacity(cw * ch);
994 for cy in 0..ch {
995 for cx in 0..cw {
996 let x0 = cx * self.chunk_size;
997 let y0 = cy * self.chunk_size;
998 let x1 = (x0 + self.chunk_size).min(grid.width);
999 let y1 = (y0 + self.chunk_size).min(grid.height);
1000 let walkable = (x0..x1).any(|x| (y0..y1).any(|y| grid.is_walkable(x, y)));
1001 let center = Vec2::new(
1002 (x0 + x1) as f32 * 0.5 * grid.cell_size,
1003 (y0 + y1) as f32 * 0.5 * grid.cell_size,
1004 ) + grid.origin;
1005 chunks.push(HierarchyChunk {
1006 chunk_x: cx,
1007 chunk_y: cy,
1008 center,
1009 walkable,
1010 portal_cells: Vec::new(),
1011 });
1012 }
1013 }
1014 chunks
1015 }
1016
1017 pub fn find_path(&self, grid: &PathGrid, start: Vec2, end: Vec2) -> Option<Vec<Vec2>> {
1019 let (sx, sy) = grid.world_to_grid(start);
1022 let (ex, ey) = grid.world_to_grid(end);
1023 let chunk_dist = ((sx as i32 / self.chunk_size as i32 - ex as i32 / self.chunk_size as i32).abs()
1024 + (sy as i32 / self.chunk_size as i32 - ey as i32 / self.chunk_size as i32).abs()) as usize;
1025
1026 if chunk_dist <= 2 {
1027 return self.fine_finder.find_path(grid, start, end);
1029 }
1030
1031 let cw = (grid.width + self.chunk_size - 1) / self.chunk_size;
1033 let ch = (grid.height + self.chunk_size - 1) / self.chunk_size;
1034 let mut chunk_grid = PathGrid::new(cw, ch, grid.cell_size * self.chunk_size as f32);
1035 chunk_grid.origin = grid.origin;
1036 for cy in 0..ch {
1038 for cx in 0..cw {
1039 let x0 = cx * self.chunk_size;
1040 let y0 = cy * self.chunk_size;
1041 let x1 = (x0 + self.chunk_size).min(grid.width);
1042 let y1 = (y0 + self.chunk_size).min(grid.height);
1043 let walkable = (x0..x1).any(|x| (y0..y1).any(|y| grid.is_walkable(x, y)));
1044 if !walkable {
1045 chunk_grid.set_walkable(cx, cy, false);
1046 }
1047 }
1048 }
1049
1050 let chunk_start = chunk_grid.grid_to_world(sx / self.chunk_size, sy / self.chunk_size);
1051 let chunk_end = chunk_grid.grid_to_world(ex / self.chunk_size, ey / self.chunk_size);
1052 let coarse = self.fine_finder.find_path(&chunk_grid, chunk_start, chunk_end)?;
1053
1054 let mut full_path = Vec::new();
1056 let mut prev = start;
1057 for &waypoint in coarse.iter().skip(1) {
1058 let (wx, wy) = grid.world_to_grid(waypoint);
1060 let fine_wp = grid.grid_to_world(wx, wy);
1061 if let Some(mut seg) = self.fine_finder.find_path(grid, prev, fine_wp) {
1062 if !full_path.is_empty() { seg.remove(0); }
1063 full_path.extend(seg);
1064 }
1065 prev = fine_wp;
1066 }
1067
1068 if full_path.is_empty() {
1069 self.fine_finder.find_path(grid, start, end)
1070 } else {
1071 Some(full_path)
1072 }
1073 }
1074}
1075
1076#[cfg(test)]
1081mod tests {
1082 use super::*;
1083 use glam::Vec2;
1084
1085 fn make_grid(w: usize, h: usize) -> PathGrid {
1086 PathGrid::new(w, h, 1.0)
1087 }
1088
1089 #[test]
1090 fn test_grid_index_round_trip() {
1091 let grid = make_grid(10, 10);
1092 let (gx, gy) = grid.world_to_grid(Vec2::new(3.7, 5.2));
1093 let world = grid.grid_to_world(gx, gy);
1094 assert!((world.x - 3.5).abs() < 0.5);
1096 assert!((world.y - 5.5).abs() < 0.5);
1097 }
1098
1099 #[test]
1100 fn test_astar_straight_line() {
1101 let grid = make_grid(10, 10);
1102 let finder = AStarPathfinder::new();
1103 let path = finder.find_path(&grid, Vec2::new(0.5, 0.5), Vec2::new(9.5, 0.5));
1104 assert!(path.is_some());
1105 let p = path.unwrap();
1106 assert!(p.len() >= 2);
1107 }
1108
1109 #[test]
1110 fn test_astar_blocked() {
1111 let mut grid = make_grid(5, 5);
1112 for y in 0..5 { grid.set_walkable(2, y, false); }
1114 let finder = AStarPathfinder::new().with_diagonal(false);
1115 let path = finder.find_path(&grid, Vec2::new(0.5, 2.5), Vec2::new(4.5, 2.5));
1116 assert!(path.is_none());
1117 }
1118
1119 #[test]
1120 fn test_astar_around_obstacle() {
1121 let mut grid = make_grid(10, 10);
1122 for y in 1..10 { grid.set_walkable(5, y, false); }
1124 let finder = AStarPathfinder::new();
1125 let path = finder.find_path(&grid, Vec2::new(0.5, 5.5), Vec2::new(9.5, 5.5));
1126 assert!(path.is_some());
1127 }
1128
1129 #[test]
1130 fn test_dijkstra_map() {
1131 let grid = make_grid(10, 10);
1132 let mut dmap = DijkstraMap::new(10, 10);
1133 dmap.build(&grid, &[Vec2::new(5.5, 5.5)]);
1134 assert!(dmap.distance_at(&grid, Vec2::new(5.5, 5.5)) < 1.0);
1136 assert!(dmap.distance_at(&grid, Vec2::new(0.5, 0.5)) > 5.0);
1138 }
1139
1140 #[test]
1141 fn test_dijkstra_next_step() {
1142 let grid = make_grid(10, 10);
1143 let mut dmap = DijkstraMap::new(10, 10);
1144 dmap.build(&grid, &[Vec2::new(9.5, 9.5)]);
1145 let next = dmap.next_step(&grid, Vec2::new(0.5, 0.5));
1146 assert!(next.is_some());
1147 }
1148
1149 #[test]
1150 fn test_jps_finds_path() {
1151 let grid = make_grid(20, 20);
1152 let jps = JumpPointSearch::new();
1153 let path = jps.find_path(&grid, Vec2::new(0.5, 0.5), Vec2::new(19.5, 19.5));
1154 assert!(path.is_some());
1155 }
1156
1157 #[test]
1158 fn test_smooth_path_no_obstacles() {
1159 let grid = make_grid(10, 10);
1160 let path = vec![
1161 Vec2::new(0.5, 0.5),
1162 Vec2::new(1.5, 0.5),
1163 Vec2::new(2.5, 0.5),
1164 Vec2::new(3.5, 0.5),
1165 ];
1166 let smoothed = smooth_path(path.clone(), &grid);
1167 assert!(smoothed.len() <= path.len());
1169 }
1170
1171 #[test]
1172 fn test_hierarchical_close_path() {
1173 let grid = make_grid(20, 20);
1174 let hf = HierarchicalPathfinder::new(4);
1175 let path = hf.find_path(&grid, Vec2::new(0.5, 0.5), Vec2::new(2.5, 2.5));
1176 assert!(path.is_some());
1177 }
1178
1179 #[test]
1180 fn test_hierarchical_far_path() {
1181 let grid = make_grid(40, 40);
1182 let hf = HierarchicalPathfinder::new(4);
1183 let path = hf.find_path(&grid, Vec2::new(0.5, 0.5), Vec2::new(38.5, 38.5));
1184 assert!(path.is_some());
1185 }
1186
1187 #[test]
1188 fn test_path_detailed_found() {
1189 let grid = make_grid(10, 10);
1190 let finder = AStarPathfinder::new();
1191 let req = PathRequest::new(Vec2::new(0.5, 0.5), Vec2::new(9.5, 9.5));
1192 let (result, stats) = finder.find_path_detailed(&grid, &req);
1193 assert!(matches!(result, PathResult::Found(_)));
1194 assert!(stats.nodes_expanded > 0);
1195 }
1196
1197 #[test]
1198 fn test_path_detailed_partial() {
1199 let mut grid = make_grid(10, 10);
1200 for x in 0..10 { grid.set_walkable(x, 5, false); }
1201 let finder = AStarPathfinder::new();
1202 let mut req = PathRequest::new(Vec2::new(0.5, 0.5), Vec2::new(9.5, 9.5));
1203 req.allow_partial = true;
1204 let (result, _) = finder.find_path_detailed(&grid, &req);
1205 assert!(matches!(result, PathResult::Partial(_) | PathResult::NoPath));
1207 }
1208
1209 #[test]
1210 fn test_neighbors_cardinal_only() {
1211 let grid = make_grid(5, 5);
1212 let neighbors = grid.neighbors_cardinal(2, 2);
1213 assert_eq!(neighbors.len(), 4);
1214 }
1215
1216 #[test]
1217 fn test_line_of_sight() {
1218 let mut grid = make_grid(10, 10);
1219 assert!(grid.line_of_sight(0, 0, 9, 9));
1220 grid.set_walkable(5, 5, false);
1221 let _los = grid.line_of_sight(0, 0, 9, 9); }
1224
1225 #[test]
1226 fn test_heuristics() {
1227 assert_eq!(Heuristic::Manhattan.compute(0, 0, 3, 4), 7.0);
1228 assert!((Heuristic::Euclidean.compute(0, 0, 3, 4) - 5.0).abs() < 0.001);
1229 assert_eq!(Heuristic::Chebyshev.compute(0, 0, 3, 4), 4.0);
1230 }
1231
1232 #[test]
1233 fn test_spline_path() {
1234 let path = vec![
1235 Vec2::new(0.0, 0.0),
1236 Vec2::new(5.0, 5.0),
1237 Vec2::new(10.0, 0.0),
1238 ];
1239 let splined = spline_path(&path, 4);
1240 assert!(splined.len() > path.len());
1241 }
1242
1243 #[test]
1244 fn test_grid_weight() {
1245 let mut grid = make_grid(5, 5);
1246 grid.set_weight(2, 2, 5.0);
1247 assert_eq!(grid.weight(2, 2), 5.0);
1248 assert_eq!(grid.weight(1, 1), 1.0);
1249 }
1250}