Skip to main content

repame_nav/
path.rs

1//! A* over the polygon graph, then a funnel pass to straighten the corridor.
2
3use crate::funnel::{dedup, funnel};
4use crate::mesh::{closest_on_segment, NavMesh};
5use crate::vec::NavVec;
6use std::cmp::Ordering;
7use std::collections::BinaryHeap;
8
9/// A caller-supplied off-mesh link already resolved onto the mesh.
10#[derive(Clone, Copy, Debug)]
11pub(crate) struct ResolvedLink<V> {
12    pub from: u32,
13    pub to: u32,
14    pub portal: [V; 2],
15    pub cost: f32,
16    pub bidirectional: bool,
17}
18
19/// Open-set entry. Ordered by `f` then polygon index, so equal-cost nodes pop
20/// in a fixed order regardless of heap internals.
21#[derive(Clone, Copy, Debug)]
22struct OpenNode {
23    f: f32,
24    polygon: u32,
25}
26
27impl PartialEq for OpenNode {
28    fn eq(&self, other: &Self) -> bool {
29        self.f.to_bits() == other.f.to_bits() && self.polygon == other.polygon
30    }
31}
32impl Eq for OpenNode {}
33impl PartialOrd for OpenNode {
34    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
35        Some(self.cmp(other))
36    }
37}
38impl Ord for OpenNode {
39    fn cmp(&self, other: &Self) -> Ordering {
40        self.f.total_cmp(&other.f).then(self.polygon.cmp(&other.polygon))
41    }
42}
43
44fn heuristic<V: NavVec>(mesh: &NavMesh<V>, polygon: u32, target: V) -> f32 {
45    let region = &mesh.polygons[polygon as usize];
46    (region.center.distance(target) - region.radius).max(0.0)
47}
48
49/// Search the polygon graph from `start_poly` to `end_poly`.
50///
51/// Returns the corridor of polygon indices and, in travel order, the portal
52/// crossed to enter each polygon after the first. `blocked_poly` removes a
53/// region; `blocked_portal` removes a single crossing; `links` adds off-mesh
54/// edges.
55pub(crate) fn astar_with<V: NavVec>(
56    mesh: &NavMesh<V>,
57    start_poly: u32,
58    end_poly: u32,
59    start_point: V,
60    end_point: V,
61    blocked_poly: impl Fn(u32) -> bool,
62    blocked_portal: impl Fn([V; 2]) -> bool,
63    links: &[ResolvedLink<V>],
64) -> Option<(Vec<u32>, Vec<[V; 2]>)> {
65    if start_poly == end_poly {
66        return Some((vec![start_poly], Vec::new()));
67    }
68    let count = mesh.polygons.len();
69    let mut g = vec![f32::INFINITY; count];
70    let mut came: Vec<Option<(u32, [V; 2])>> = vec![None; count];
71    let mut entry = vec![V::ZERO; count];
72    let mut visited = vec![false; count];
73
74    let mut open: BinaryHeap<std::cmp::Reverse<OpenNode>> = BinaryHeap::new();
75    g[start_poly as usize] = 0.0;
76    entry[start_poly as usize] = start_point;
77    open.push(std::cmp::Reverse(OpenNode {
78        f: heuristic(mesh, start_poly, end_point),
79        polygon: start_poly,
80    }));
81
82    while let Some(std::cmp::Reverse(node)) = open.pop() {
83        let polygon = node.polygon;
84        let pi = polygon as usize;
85        if visited[pi] {
86            continue;
87        }
88        visited[pi] = true;
89        if polygon == end_poly {
90            break;
91        }
92        let entry_point = entry[pi];
93
94        for edge in 0..mesh.polygons[pi].indices.len() as u32 {
95            let Some(neighbor) = mesh.neighbor(polygon, edge) else {
96                continue;
97            };
98            let q = neighbor.polygon;
99            if q == polygon {
100                continue;
101            }
102            if q != end_poly && blocked_poly(q) {
103                continue;
104            }
105            let Some(portal) = mesh.portal(polygon, edge) else {
106                continue;
107            };
108            if blocked_portal(portal) {
109                continue;
110            }
111            let q_entry = closest_on_segment(entry_point, portal[0], portal[1]);
112            let ng = g[pi] + entry_point.distance(q_entry);
113            if ng < g[q as usize] {
114                g[q as usize] = ng;
115                came[q as usize] = Some((polygon, portal));
116                entry[q as usize] = q_entry;
117                open.push(std::cmp::Reverse(OpenNode {
118                    f: ng + heuristic(mesh, q, end_point),
119                    polygon: q,
120                }));
121            }
122        }
123
124        for link in links {
125            let (q, portal, cost) = if link.from == polygon {
126                (link.to, link.portal, entry_point.distance(link.portal[0]) + link.cost)
127            } else if link.bidirectional && link.to == polygon {
128                (
129                    link.from,
130                    [link.portal[1], link.portal[0]],
131                    entry_point.distance(link.portal[1]) + link.cost,
132                )
133            } else {
134                continue;
135            };
136            if q != end_poly && blocked_poly(q) {
137                continue;
138            }
139            let q_entry = portal[1];
140            let ng = g[pi] + cost;
141            if ng < g[q as usize] {
142                g[q as usize] = ng;
143                came[q as usize] = Some((polygon, portal));
144                entry[q as usize] = q_entry;
145                open.push(std::cmp::Reverse(OpenNode {
146                    f: ng + heuristic(mesh, q, end_point),
147                    polygon: q,
148                }));
149            }
150        }
151    }
152
153    let mut corridor = vec![end_poly];
154    let mut portals = Vec::new();
155    let mut current = end_poly;
156    while current != start_poly {
157        let (previous, portal) = came[current as usize]?;
158        portals.push(portal);
159        corridor.push(previous);
160        current = previous;
161    }
162    corridor.reverse();
163    portals.reverse();
164    Some((corridor, portals))
165}
166
167impl<V: NavVec> NavMesh<V> {
168    /// Shortest path through the mesh from `from` to `to`.
169    ///
170    /// Endpoints are projected onto their region (a heightfield lift or a
171    /// boundary projection). Returns `None` when either endpoint has no
172    /// region or no corridor connects them.
173    pub fn find_path(&self, from: V, to: V) -> Option<Vec<V>> {
174        let start_polygon = self.locate(from).or_else(|| self.closest_polygon(from))?;
175        let end_polygon = self.locate(to).or_else(|| self.closest_polygon(to))?;
176        let start = self.closest_point_on_polygon(start_polygon, from);
177        let end = self.closest_point_on_polygon(end_polygon, to);
178        if start_polygon == end_polygon {
179            return Some(vec![start, end]);
180        }
181        let (_, portals) =
182            astar_with(self, start_polygon, end_polygon, start, end, |_| false, |_| false, &[])?;
183        let mut path = funnel(start, end, &portals);
184        dedup(&mut path);
185        Some(path)
186    }
187}