Skip to main content

repame_nav/
mesh.rs

1//! Navmesh storage: a shared vertex buffer, convex polygon regions, and the
2//! edge adjacency between them.
3//!
4//! Adjacency is derived from the geometry: two polygons are neighbours when
5//! they traverse the same vertex pair in opposite directions. Building it from
6//! geometry (rather than from bake-time bookkeeping) is what makes a merged or
7//! hand-authored mesh and a baked grid mesh share one query path.
8
9use crate::vec::NavVec;
10use crate::NavError;
11use std::collections::HashMap;
12
13/// One convex region, stored as indices into the mesh vertex buffer.
14#[derive(Clone, Debug, PartialEq)]
15pub struct Polygon<V> {
16    /// Vertex indices, wound consistently (one direction around the region).
17    pub indices: Vec<u32>,
18    /// Mean of the vertices. Used as the A* heuristic reference and the
19    /// obstacle containment test point.
20    pub center: V,
21    /// Largest distance from `center` to a vertex.
22    pub radius: f32,
23}
24
25/// The region across one directed edge of a polygon.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct Neighbor {
28    /// The adjacent polygon index.
29    pub polygon: u32,
30    /// The neighbour's own edge index for the shared edge.
31    pub edge: u32,
32}
33
34/// A navigation mesh: vertices plus convex polygon regions plus adjacency.
35///
36/// Queries never iterate a hash map, so the same mesh always answers the same
37/// way. The map is only used while [`NavMesh::from_polygons`] builds adjacency.
38#[derive(Clone, Debug)]
39pub struct NavMesh<V: NavVec> {
40    /// Shared vertex buffer. Baked meshes deduplicate shared corners here.
41    pub vertices: Vec<V>,
42    /// Convex regions over `vertices`.
43    pub polygons: Vec<Polygon<V>>,
44    neighbors: Vec<Vec<Option<Neighbor>>>,
45}
46
47impl<V: NavVec> NavMesh<V> {
48    /// Build a mesh from vertices and per-polygon vertex index lists.
49    ///
50    /// Returns [`NavError::DegeneratePolygon`] for a region with fewer than
51    /// three vertices and [`NavError::BadIndex`] for an out-of-range index.
52    pub fn from_polygons(vertices: Vec<V>, polygons: Vec<Vec<u32>>) -> Result<Self, NavError> {
53        if polygons.is_empty() {
54            return Err(NavError::EmptyRegion);
55        }
56        let mut regions = Vec::with_capacity(polygons.len());
57        for indices in polygons {
58            if indices.len() < 3 {
59                return Err(NavError::DegeneratePolygon);
60            }
61            let mut center = V::ZERO;
62            for &index in &indices {
63                let vertex = *vertices.get(index as usize).ok_or(NavError::BadIndex)?;
64                center = center.add(vertex);
65            }
66            center = center.mul(1.0 / indices.len() as f32);
67            let mut radius = 0.0f32;
68            for &index in &indices {
69                radius = radius.max(center.distance(vertices[index as usize]));
70            }
71            regions.push(Polygon { indices, center, radius });
72        }
73
74        let mut neighbors = vec![Vec::new(); regions.len()];
75        let mut directed: HashMap<(u32, u32), (u32, u32)> = HashMap::new();
76        for (polygon, region) in regions.iter().enumerate() {
77            let count = region.indices.len();
78            neighbors[polygon] = vec![None; count];
79            for edge in 0..count {
80                let a = region.indices[edge];
81                let b = region.indices[(edge + 1) % count];
82                directed.insert((a, b), (polygon as u32, edge as u32));
83            }
84        }
85        for (polygon, region) in regions.iter().enumerate() {
86            let count = region.indices.len();
87            for edge in 0..count {
88                let a = region.indices[edge];
89                let b = region.indices[(edge + 1) % count];
90                if let Some(&(other, other_edge)) = directed.get(&(b, a)) {
91                    if other as usize != polygon {
92                        neighbors[polygon][edge] = Some(Neighbor { polygon: other, edge: other_edge });
93                    }
94                }
95            }
96        }
97
98        Ok(Self { vertices, polygons: regions, neighbors })
99    }
100
101    /// Number of regions.
102    pub fn polygon_count(&self) -> usize {
103        self.polygons.len()
104    }
105
106    /// One region.
107    pub fn polygon(&self, index: u32) -> Option<&Polygon<V>> {
108        self.polygons.get(index as usize)
109    }
110
111    /// Vertex indices of one region.
112    pub fn polygon_indices(&self, index: u32) -> Option<&[u32]> {
113        self.polygons.get(index as usize).map(|p| p.indices.as_slice())
114    }
115
116    /// Neighbour across directed edge `edge` of `polygon`, if the edge is shared.
117    pub fn neighbor(&self, polygon: u32, edge: u32) -> Option<Neighbor> {
118        self.neighbors.get(polygon as usize)?.get(edge as usize).copied().flatten()
119    }
120
121    /// Endpoints of directed edge `edge` of `polygon`.
122    pub fn portal(&self, polygon: u32, edge: u32) -> Option<[V; 2]> {
123        let region = self.polygons.get(polygon as usize)?;
124        let count = region.indices.len();
125        let a = *region.indices.get(edge as usize)?;
126        let b = region.indices[(edge as usize + 1) % count];
127        Some([self.vertices[a as usize], self.vertices[b as usize]])
128    }
129
130    /// Region containing `point`, if any. Ties (they cannot occur in a normal
131    /// tiling) resolve to the lowest region index.
132    pub fn locate(&self, point: V) -> Option<u32> {
133        let mut best: Option<(u32, f32)> = None;
134        for (index, region) in self.polygons.iter().enumerate() {
135            let region_vertices = self.region_vertices(region);
136            if point_in_polygon(&region_vertices, point).is_none() {
137                continue;
138            }
139            let distance = region.center.distance(point);
140            match best {
141                Some((_, d)) if d <= distance => {}
142                _ => best = Some((index as u32, distance)),
143            }
144        }
145        best.map(|(index, _)| index)
146    }
147
148    /// Region nearest to `point` when it is outside the mesh.
149    pub fn closest_polygon(&self, point: V) -> Option<u32> {
150        let mut best: Option<(u32, f32)> = None;
151        for (index, region) in self.polygons.iter().enumerate() {
152            let candidate = closest_point_on_polygon(&self.vertices, region, point);
153            let distance = point.distance(candidate);
154            match best {
155                Some((_, d)) if d <= distance => {}
156                _ => best = Some((index as u32, distance)),
157            }
158        }
159        best.map(|(index, _)| index)
160    }
161
162    /// Closest point on a region to `point`.
163    ///
164    /// Inside the region the point is lifted onto the (possibly tilted)
165    /// polygon surface by barycentric interpolation; outside it is projected
166    /// onto the closest boundary edge.
167    pub fn closest_point_on_polygon(&self, polygon: u32, point: V) -> V {
168        match self.polygons.get(polygon as usize) {
169            Some(region) => closest_point_on_polygon(&self.vertices, region, point),
170            None => point,
171        }
172    }
173
174    pub(crate) fn region_vertices(&self, region: &Polygon<V>) -> Vec<V> {
175        region
176            .indices
177            .iter()
178            .map(|&index| self.vertices[index as usize])
179            .collect()
180    }
181}
182
183/// Barycentric weights of `point` in the navigation plane triangle `a b c`,
184/// or `None` when the point is outside it.
185pub(crate) fn barycentric(plane: [f32; 2], a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> Option<[f32; 3]> {
186    let v0 = [b[0] - a[0], b[1] - a[1]];
187    let v1 = [c[0] - a[0], c[1] - a[1]];
188    let v2 = [plane[0] - a[0], plane[1] - a[1]];
189    let denom = v0[0] * v1[1] - v1[0] * v0[1];
190    if denom.abs() < 1e-12 {
191        return None;
192    }
193    let w1 = (v2[0] * v1[1] - v1[0] * v2[1]) / denom;
194    let w2 = (v0[0] * v2[1] - v2[0] * v0[1]) / denom;
195    let w0 = 1.0 - w1 - w2;
196    const EPS: f32 = 1e-5;
197    if w0 >= -EPS && w1 >= -EPS && w2 >= -EPS {
198        Some([w0, w1, w2])
199    } else {
200        None
201    }
202}
203
204/// First fan triangle of `vertices` containing `point` in the navigation
205/// plane, with the interpolation weights.
206pub(crate) fn point_in_polygon<V: NavVec>(vertices: &[V], point: V) -> Option<(usize, [f32; 3])> {
207    if vertices.len() < 3 {
208        return None;
209    }
210    let plane = point.nav2();
211    let a = vertices[0].nav2();
212    for i in 1..vertices.len() - 1 {
213        let b = vertices[i].nav2();
214        let c = vertices[i + 1].nav2();
215        if let Some(weights) = barycentric(plane, a, b, c) {
216            return Some((i, weights));
217        }
218    }
219    None
220}
221
222/// Closest point on the segment `a b` to `point`, in full space.
223pub(crate) fn closest_on_segment<V: NavVec>(point: V, a: V, b: V) -> V {
224    let ab = b.sub(a);
225    let denom = ab.dot(ab);
226    if denom <= 1e-20 {
227        return a;
228    }
229    let t = (point.sub(a).dot(ab) / denom).clamp(0.0, 1.0);
230    a.add(ab.mul(t))
231}
232
233pub(crate) fn closest_point_on_polygon<V: NavVec>(
234    vertices: &[V],
235    region: &Polygon<V>,
236    point: V,
237) -> V {
238    let region_vertices: Vec<V> =
239        region.indices.iter().map(|&i| vertices[i as usize]).collect();
240    if let Some((triangle, weights)) = point_in_polygon(&region_vertices, point) {
241        let a = region_vertices[0].mul(weights[0]);
242        let b = region_vertices[triangle].mul(weights[1]);
243        let c = region_vertices[triangle + 1].mul(weights[2]);
244        return a.add(b).add(c);
245    }
246    let mut best = region_vertices[0];
247    let mut best_distance = point.distance(best);
248    for i in 0..region_vertices.len() {
249        let a = region_vertices[i];
250        let b = region_vertices[(i + 1) % region_vertices.len()];
251        let candidate = closest_on_segment(point, a, b);
252        let distance = point.distance(candidate);
253        if distance < best_distance {
254            best_distance = distance;
255            best = candidate;
256        }
257    }
258    best
259}