use std::collections::{BTreeMap, VecDeque};
use crate::navmesh::{NavmeshValidationError, corridor::NavmeshCorridor};
use crate::{Navmesh, NavmeshPortal, NavmeshQuery, NavmeshQueryResult, Point2};
pub trait PreparedNavmeshBuilder {
type Map: PreparedNavmesh;
fn name(&self) -> &'static str;
fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError>;
}
pub trait PreparedNavmesh {
fn name(&self) -> &'static str;
fn navmesh(&self) -> &Navmesh;
fn locate_point(&self, point: Point2) -> Option<usize> {
self.navmesh().locate_point(point)
}
fn locate_cells(&self, point: Point2) -> Vec<usize> {
self.navmesh().locate_cells(point)
}
fn query(&self, query: NavmeshQuery) -> NavmeshQueryResult {
self.navmesh().query(query)
}
fn neighbors(&self, cell_index: usize) -> Option<&[usize]>;
fn portals_from(&self, cell_index: usize) -> Option<&[NavmeshPortal]>;
fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<NavmeshPortal>;
fn cells_connected(&self, start_cell: usize, goal_cell: usize) -> bool {
let cell_count = self.navmesh().cells().len();
if start_cell >= cell_count || goal_cell >= cell_count {
return false;
}
let mut seen = vec![false; cell_count];
let mut frontier = VecDeque::from([start_cell]);
seen[start_cell] = true;
while let Some(cell_index) = frontier.pop_front() {
if cell_index == goal_cell {
return true;
}
let Some(neighbors) = self.neighbors(cell_index) else {
continue;
};
for &neighbor in neighbors {
if neighbor >= seen.len() || seen[neighbor] {
continue;
}
seen[neighbor] = true;
frontier.push_back(neighbor);
}
}
false
}
fn materialize_corridor(
&self,
start: Point2,
goal: Point2,
cells: &[usize],
) -> Option<NavmeshCorridor> {
NavmeshCorridor::from_cells(self.navmesh(), start, goal, cells)
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum PreparedNavmeshBuildError {
#[error("invalid navmesh: {source}")]
InvalidNavmesh {
#[from]
source: NavmeshValidationError,
},
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StaticPreparedNavmeshBuilder;
impl PreparedNavmeshBuilder for StaticPreparedNavmeshBuilder {
type Map = StaticPreparedNavmesh;
fn name(&self) -> &'static str {
"static-prepared-navmesh"
}
fn preprocess(&self, navmesh: &Navmesh) -> Result<Self::Map, PreparedNavmeshBuildError> {
navmesh
.validate()
.map_err(PreparedNavmeshBuildError::from)?;
let mut neighbors = vec![Vec::new(); navmesh.cells().len()];
let mut portals_from = vec![Vec::new(); navmesh.cells().len()];
let mut portal_lookup = BTreeMap::new();
for &portal in navmesh.portals() {
neighbors[portal.left_cell].push(portal.right_cell);
neighbors[portal.right_cell].push(portal.left_cell);
portals_from[portal.left_cell].push(portal);
portals_from[portal.right_cell].push(portal);
portal_lookup
.entry((portal.left_cell, portal.right_cell))
.or_insert(portal);
portal_lookup
.entry((portal.right_cell, portal.left_cell))
.or_insert(portal);
}
Ok(StaticPreparedNavmesh {
navmesh: navmesh.clone(),
neighbors,
portals_from,
portal_lookup,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StaticPreparedNavmesh {
navmesh: Navmesh,
neighbors: Vec<Vec<usize>>,
portals_from: Vec<Vec<NavmeshPortal>>,
portal_lookup: BTreeMap<(usize, usize), NavmeshPortal>,
}
impl StaticPreparedNavmesh {
#[must_use]
pub fn builder() -> StaticPreparedNavmeshBuilder {
StaticPreparedNavmeshBuilder
}
}
impl PreparedNavmesh for StaticPreparedNavmesh {
fn name(&self) -> &'static str {
"static-prepared-navmesh"
}
fn navmesh(&self) -> &Navmesh {
&self.navmesh
}
fn neighbors(&self, cell_index: usize) -> Option<&[usize]> {
self.neighbors.get(cell_index).map(Vec::as_slice)
}
fn portals_from(&self, cell_index: usize) -> Option<&[NavmeshPortal]> {
self.portals_from.get(cell_index).map(Vec::as_slice)
}
fn portal_between(&self, left_cell: usize, right_cell: usize) -> Option<NavmeshPortal> {
self.portal_lookup.get(&(left_cell, right_cell)).copied()
}
}
#[cfg(test)]
mod tests {
use super::{PreparedNavmesh, PreparedNavmeshBuilder, StaticPreparedNavmesh};
use crate::navmesh::corridor::NavmeshCorridor;
use crate::{Navmesh, NavmeshCell, NavmeshPortal, Point2};
#[test]
fn portal_between_selects_same_portal_as_corridor_for_duplicate_pair() {
let p0 = NavmeshPortal {
left_cell: 0,
right_cell: 1,
start: Point2::new(2.0, 0.0),
end: Point2::new(2.0, 2.0),
};
let p1 = NavmeshPortal {
left_cell: 0,
right_cell: 1,
start: Point2::new(2.0, 2.0),
end: Point2::new(2.0, 4.0),
};
let navmesh = Navmesh::new(
vec![
NavmeshCell::new(
"left",
vec![
Point2::new(0.0, 0.0),
Point2::new(2.0, 0.0),
Point2::new(2.0, 4.0),
Point2::new(0.0, 4.0),
],
),
NavmeshCell::new(
"right",
vec![
Point2::new(2.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(4.0, 4.0),
Point2::new(2.0, 4.0),
],
),
],
vec![p0, p1],
);
navmesh
.validate()
.expect("two-portal navmesh should validate");
let prepared = StaticPreparedNavmesh::builder()
.preprocess(&navmesh)
.expect("preprocess should succeed");
assert_eq!(prepared.portal_between(0, 1), Some(p0));
assert_eq!(prepared.portal_between(1, 0), Some(p0));
let corridor = NavmeshCorridor::from_cells(
&navmesh,
Point2::new(1.0, 1.0),
Point2::new(3.0, 1.0),
&[0, 1],
)
.expect("corridor should build");
assert_eq!(corridor.portals[0], p0);
}
}