Skip to main content

repame_nav/
map.rs

1//! Runtime navigation map: a baked [`NavMesh`] plus dynamic carved obstacles
2//! and off-mesh links.
3//!
4//! Obstacles carve at polygon granularity: a region whose navigation-plane
5//! centre falls inside an obstacle is removed from the search, and a crossing
6//! whose portal midpoint falls inside an obstacle is removed too, so a path
7//! cannot cut a corner through a thin obstacle. Off-mesh links add an edge
8//! between the regions holding their endpoints.
9//!
10//! Both lists are consumed in insertion order, which keeps queries
11//! deterministic.
12
13use crate::mesh::NavMesh;
14use crate::path::{astar_with, ResolvedLink};
15use crate::vec::NavVec;
16use crate::funnel;
17
18/// A dynamic carved region, in navigation-plane coordinates.
19#[derive(Clone, Debug, PartialEq)]
20pub struct NavObstacle {
21    /// Outline polygon (XY for 2D, XZ for 3D). Any winding.
22    pub outline: Vec<[f32; 2]>,
23    /// Carving is skipped while disabled.
24    pub enabled: bool,
25}
26
27impl NavObstacle {
28    /// Enabled obstacle from an outline.
29    pub fn new(outline: Vec<[f32; 2]>) -> Self {
30        Self { outline, enabled: true }
31    }
32
33    fn contains_point(&self, point: [f32; 2]) -> bool {
34        if !self.enabled || self.outline.len() < 3 {
35            return false;
36        }
37        let mut inside = false;
38        let count = self.outline.len();
39        let mut j = count - 1;
40        for i in 0..count {
41            let a = self.outline[i];
42            let b = self.outline[j];
43            if (a[1] > point[1]) != (b[1] > point[1]) {
44                let t = (point[1] - a[1]) / (b[1] - a[1]);
45                if point[0] < a[0] + t * (b[0] - a[0]) {
46                    inside = !inside;
47                }
48            }
49            j = i;
50        }
51        inside
52    }
53}
54
55/// A one-way or bidirectional off-mesh connection between two world points.
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub struct NavLink<V> {
58    /// Link start, approached from a region containing it.
59    pub start: V,
60    /// Link end; the agent arrives here and continues into its region.
61    pub end: V,
62    /// When set, the link is usable in both directions.
63    pub bidirectional: bool,
64    /// Traversal cost. Defaults to the straight length.
65    pub cost: Option<f32>,
66    /// Usable while enabled.
67    pub enabled: bool,
68}
69
70impl<V> NavLink<V> {
71    /// Enabled one-way link with default cost.
72    pub fn new(start: V, end: V) -> Self {
73        Self { start, end, bidirectional: false, cost: None, enabled: true }
74    }
75}
76
77/// A baked mesh plus the dynamic data layered on top of it for queries.
78#[derive(Clone, Debug)]
79pub struct NavMap<V: NavVec> {
80    /// The baked mesh.
81    pub mesh: NavMesh<V>,
82    /// Dynamic carved regions.
83    pub obstacles: Vec<NavObstacle>,
84    /// Off-mesh connections.
85    pub links: Vec<NavLink<V>>,
86}
87
88impl<V: NavVec> NavMap<V> {
89    /// Wrap a baked mesh.
90    pub fn new(mesh: NavMesh<V>) -> Self {
91        Self { mesh, obstacles: Vec::new(), links: Vec::new() }
92    }
93
94    /// Add a carved obstacle. Returns its index.
95    pub fn add_obstacle(&mut self, obstacle: NavObstacle) -> usize {
96        self.obstacles.push(obstacle);
97        self.obstacles.len() - 1
98    }
99
100    /// Add an off-mesh link. Returns its index.
101    pub fn add_link(&mut self, link: NavLink<V>) -> usize {
102        self.links.push(link);
103        self.links.len() - 1
104    }
105
106    fn blocked_point(&self, point: V) -> bool {
107        let plane = point.nav2();
108        self.obstacles.iter().any(|o| o.contains_point(plane))
109    }
110
111    fn blocked_polygon(&self, polygon: u32) -> bool {
112        match self.mesh.polygon(polygon) {
113            Some(region) => self.blocked_point(region.center),
114            None => false,
115        }
116    }
117
118    fn blocked_portal(&self, portal: [V; 2]) -> bool {
119        let a = portal[0].nav2();
120        let b = portal[1].nav2();
121        let midpoint = V::from_nav2([(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5], 0.0);
122        self.blocked_point(midpoint)
123    }
124
125    fn resolve_links(&self) -> Vec<ResolvedLink<V>> {
126        let mut resolved = Vec::new();
127        for link in &self.links {
128            if !link.enabled || self.blocked_point(link.start) || self.blocked_point(link.end) {
129                continue;
130            }
131            let from = self.mesh.locate(link.start).or_else(|| self.mesh.closest_polygon(link.start));
132            let to = self.mesh.locate(link.end).or_else(|| self.mesh.closest_polygon(link.end));
133            let (Some(from), Some(to)) = (from, to) else {
134                continue;
135            };
136            if from == to {
137                continue;
138            }
139            let cost = link.cost.unwrap_or(link.start.distance(link.end));
140            resolved.push(ResolvedLink {
141                from,
142                to,
143                portal: [link.start, link.end],
144                cost,
145                bidirectional: link.bidirectional,
146            });
147        }
148        resolved
149    }
150
151    /// Shortest path through the mesh with obstacles and links applied.
152    ///
153    /// Deterministic: the same map state and endpoints give the same path.
154    pub fn find_path(&self, from: V, to: V) -> Option<Vec<V>> {
155        let start_polygon = self.mesh.locate(from).or_else(|| self.mesh.closest_polygon(from))?;
156        let end_polygon = self.mesh.locate(to).or_else(|| self.mesh.closest_polygon(to))?;
157        let start = self.mesh.closest_point_on_polygon(start_polygon, from);
158        let end = self.mesh.closest_point_on_polygon(end_polygon, to);
159        if start_polygon == end_polygon {
160            return Some(vec![start, end]);
161        }
162        let links = self.resolve_links();
163        let (_, portals) = astar_with(
164            &self.mesh,
165            start_polygon,
166            end_polygon,
167            start,
168            end,
169            |polygon| self.blocked_polygon(polygon),
170            |portal| self.blocked_portal(portal),
171            &links,
172        )?;
173        let mut path = funnel::funnel(start, end, &portals);
174        funnel::dedup(&mut path);
175        Some(path)
176    }
177}