Skip to main content

condor_navmesh/navmesh/
prepared.rs

1//! Build-once / query-many contracts for navmesh adjacency snapshots.
2//!
3//! # Role
4//!
5//! This module is the **prepared substrate**, not a solver. A
6//! [`PreparedNavmeshBuilder`] clones a validated static [`Navmesh`] and indexes
7//! neighbors and portals for repeated lookup. Algorithm lanes (notably TRA* in
8//! [`crate::algorithms::tra_star`]) wrap these tables and add routing or
9//! waypoint-cache policy; they still rebuild through the same preprocess contract.
10//!
11//! # Immutability and dynamic updates
12//!
13//! Prepared maps are **immutable snapshots**. Dynamic availability never patches
14//! them in place. When [`crate::DynamicNavmeshState::prepared_stale`] is set,
15//! discard the map and rebuild from a fresh
16//! [`crate::DynamicNavmeshState::materialize`] snapshot (see
17//! [`crate::DynamicPreparedNavmeshQuery`]).
18//!
19//! # Query contract
20//!
21//! Locate/query defaults still delegate to the owned mesh. The value of
22//! preparation is indexed neighbor and portal access for corridor search and
23//! prepared pathfinders, plus a stable builder `name()` for reports.
24
25use std::collections::{BTreeMap, VecDeque};
26
27use crate::navmesh::{NavmeshValidationError, corridor::NavmeshCorridor};
28use crate::{Navmesh, NavmeshPortal, NavmeshQuery, NavmeshQueryResult, Point2};
29
30/// Preprocesses a validated static navmesh into an immutable prepared map.
31///
32/// Builders may add indexing, acceleration, or algorithm-specific caches (TRA*
33/// waypoint databases). The starter [`StaticPreparedNavmeshBuilder`] only
34/// materializes adjacency tables. Callers must re-run preprocess after any mesh
35/// or availability change—maps are not updated in place.
36pub trait PreparedNavmeshBuilder {
37    /// Immutable prepared map type produced by this builder.
38    type Map: PreparedNavmesh;
39
40    /// Stable builder name for reports and dynamic-query metadata.
41    fn name(&self) -> &'static str;
42
43    /// Validates and snapshots `navmesh` into a query-ready prepared map.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`PreparedNavmeshBuildError`] when the source mesh fails validation.
48    fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError>;
49}
50
51/// Immutable prepared navmesh snapshot with fast adjacency and portal lookup.
52///
53/// Owns (or borrows through) a static [`Navmesh`]. Default locate/query methods
54/// forward to that mesh so connectivity semantics match the unprepared substrate.
55pub trait PreparedNavmesh {
56    /// Stable prepared-map name (usually the builder name).
57    fn name(&self) -> &'static str;
58
59    /// Owned static mesh snapshot this preparation was built from.
60    fn navmesh(&self) -> &Navmesh;
61
62    /// First cell containing `point`; see [`Navmesh::locate_point`].
63    fn locate_point(&self, point: Point2) -> Option<usize> {
64        self.navmesh().locate_point(point)
65    }
66
67    /// All cells containing `point`; see [`Navmesh::locate_cells`].
68    fn locate_cells(&self, point: Point2) -> Vec<usize> {
69        self.navmesh().locate_cells(point)
70    }
71
72    /// Cell-graph connectivity for `query`; see [`Navmesh::query`].
73    fn query(&self, query: NavmeshQuery) -> NavmeshQueryResult {
74        self.navmesh().query(query)
75    }
76
77    /// Indexed neighbor cell list for `cell_index`, or `None` if out of range.
78    fn neighbors(&self, cell_index: usize) -> Option<&[usize]>;
79
80    /// Indexed portals incident to `cell_index`, or `None` if out of range.
81    fn portals_from(&self, cell_index: usize) -> Option<&[NavmeshPortal]>;
82
83    /// Portal joining two cells when one was recorded at preprocess time.
84    ///
85    /// If multiple portals connect the same pair, the first in navmesh order wins
86    /// (matches corridor materialization).
87    fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<NavmeshPortal>;
88
89    /// BFS over prepared neighbors: whether `start_cell` can reach `goal_cell`.
90    ///
91    /// Out-of-range indices return `false`. Prefer this over scanning raw portals
92    /// when the prepared index is already available.
93    fn cells_connected(&self, start_cell: usize, goal_cell: usize) -> bool {
94        let cell_count = self.navmesh().cells().len();
95        if start_cell >= cell_count || goal_cell >= cell_count {
96            return false;
97        }
98
99        let mut seen = vec![false; cell_count];
100        let mut frontier = VecDeque::from([start_cell]);
101        seen[start_cell] = true;
102
103        while let Some(cell_index) = frontier.pop_front() {
104            if cell_index == goal_cell {
105                return true;
106            }
107
108            let Some(neighbors) = self.neighbors(cell_index) else {
109                continue;
110            };
111
112            for &neighbor in neighbors {
113                if neighbor >= seen.len() || seen[neighbor] {
114                    continue;
115                }
116                seen[neighbor] = true;
117                frontier.push_back(neighbor);
118            }
119        }
120
121        false
122    }
123
124    /// Builds a [`NavmeshCorridor`] for a known cell sequence using the owned mesh portals.
125    ///
126    /// Returns `None` when `cells` is empty or any consecutive pair lacks a portal.
127    fn materialize_corridor(
128        &self,
129        start: Point2,
130        goal: Point2,
131        cells: &[usize],
132    ) -> Option<NavmeshCorridor> {
133        NavmeshCorridor::from_cells(self.navmesh(), start, goal, cells)
134    }
135}
136
137/// Prepared navmesh preprocess failed because the source mesh failed validation.
138///
139/// Fail-closed: builders do not emit a partial prepared map when validation fails.
140#[derive(Debug, Clone, PartialEq, thiserror::Error)]
141#[non_exhaustive]
142pub enum PreparedNavmeshBuildError {
143    /// Source [`Navmesh`] failed [`Navmesh::validate`] during preprocess.
144    #[error("invalid navmesh: {source}")]
145    InvalidNavmesh {
146        /// Underlying cell/portal validation failure.
147        #[from]
148        source: NavmeshValidationError,
149    },
150}
151
152/// Starter builder that indexes neighbors and portals from a static navmesh.
153///
154/// Clones the mesh, validates it, and builds per-cell neighbor/portal tables plus a
155/// bidirectional portal lookup. No hierarchical or algorithm-specific caches.
156#[derive(Debug, Clone, Copy, Default)]
157pub struct StaticPreparedNavmeshBuilder;
158
159impl PreparedNavmeshBuilder for StaticPreparedNavmeshBuilder {
160    type Map = StaticPreparedNavmesh;
161
162    fn name(&self) -> &'static str {
163        "static-prepared-navmesh"
164    }
165
166    fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
167        navmesh
168            .validate()
169            .map_err(PreparedNavmeshBuildError::from)?;
170
171        let mut neighbors = vec![Vec::new(); navmesh.cells().len()];
172        let mut portals_from = vec![Vec::new(); navmesh.cells().len()];
173        let mut portal_lookup = BTreeMap::new();
174
175        for &portal in navmesh.portals() {
176            neighbors[portal.left_cell].push(portal.right_cell);
177            neighbors[portal.right_cell].push(portal.left_cell);
178
179            portals_from[portal.left_cell].push(portal);
180            portals_from[portal.right_cell].push(portal);
181
182            // First portal in navmesh order wins for duplicate cell pairs.
183            portal_lookup
184                .entry((portal.left_cell, portal.right_cell))
185                .or_insert(portal);
186            portal_lookup
187                .entry((portal.right_cell, portal.left_cell))
188                .or_insert(portal);
189        }
190
191        Ok(StaticPreparedNavmesh {
192            navmesh: navmesh.clone(),
193            neighbors,
194            portals_from,
195            portal_lookup,
196        })
197    }
198}
199
200/// Pass-through prepared snapshot owning a cloned navmesh and adjacency indexes.
201///
202/// Safe to share across queries until the underlying availability overlay changes;
203/// then rebuild rather than mutating this type.
204#[derive(Debug, Clone, PartialEq)]
205pub struct StaticPreparedNavmesh {
206    navmesh: Navmesh,
207    neighbors: Vec<Vec<usize>>,
208    portals_from: Vec<Vec<NavmeshPortal>>,
209    portal_lookup: BTreeMap<(usize, usize), NavmeshPortal>,
210}
211
212impl StaticPreparedNavmesh {
213    /// Returns the default static prepared builder.
214    #[must_use]
215    pub fn builder() -> StaticPreparedNavmeshBuilder {
216        StaticPreparedNavmeshBuilder
217    }
218}
219
220impl PreparedNavmesh for StaticPreparedNavmesh {
221    fn name(&self) -> &'static str {
222        "static-prepared-navmesh"
223    }
224
225    fn navmesh(&self) -> &Navmesh {
226        &self.navmesh
227    }
228
229    fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
230        self.neighbors.get(cell_index).map(Vec::as_slice)
231    }
232
233    fn portals_from(&self, cell_index: usize) -> Option<&[NavmeshPortal]> {
234        self.portals_from.get(cell_index).map(Vec::as_slice)
235    }
236
237    fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<NavmeshPortal> {
238        self.portal_lookup.get(&(left_cell, right_cell)).copied()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::{PreparedNavmesh, PreparedNavmeshBuilder, StaticPreparedNavmesh};
245    use crate::navmesh::corridor::NavmeshCorridor;
246    use crate::{Navmesh, NavmeshCell, NavmeshPortal, Point2};
247
248    #[test]
249    fn portal_between_selects_same_portal_as_corridor_for_duplicate_pair() {
250        let p0 = NavmeshPortal {
251            left_cell: 0,
252            right_cell: 1,
253            start: Point2::new(2.0, 0.0),
254            end: Point2::new(2.0, 2.0),
255        };
256        let p1 = NavmeshPortal {
257            left_cell: 0,
258            right_cell: 1,
259            start: Point2::new(2.0, 2.0),
260            end: Point2::new(2.0, 4.0),
261        };
262        let navmesh = Navmesh::new(
263            vec![
264                NavmeshCell::new(
265                    "left",
266                    vec![
267                        Point2::new(0.0, 0.0),
268                        Point2::new(2.0, 0.0),
269                        Point2::new(2.0, 4.0),
270                        Point2::new(0.0, 4.0),
271                    ],
272                ),
273                NavmeshCell::new(
274                    "right",
275                    vec![
276                        Point2::new(2.0, 0.0),
277                        Point2::new(4.0, 0.0),
278                        Point2::new(4.0, 4.0),
279                        Point2::new(2.0, 4.0),
280                    ],
281                ),
282            ],
283            vec![p0, p1],
284        );
285        navmesh
286            .validate()
287            .expect("two-portal navmesh should validate");
288
289        let prepared = StaticPreparedNavmesh::builder()
290            .preprocess(&navmesh)
291            .expect("preprocess should succeed");
292
293        assert_eq!(prepared.portal_between(0, 1), Some(p0));
294        assert_eq!(prepared.portal_between(1, 0), Some(p0));
295
296        let corridor = NavmeshCorridor::from_cells(
297            &navmesh,
298            Point2::new(1.0, 1.0),
299            Point2::new(3.0, 1.0),
300            &[0, 1],
301        )
302        .expect("corridor should build");
303        assert_eq!(corridor.portals[0], p0);
304    }
305}