use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet, VecDeque},
};
#[cfg(feature = "polyanya")]
pub mod adapter;
pub mod corridor;
pub mod funnel;
pub mod prepared;
use condor_core::{Point2, SearchOutcome, SearchVisitStats};
use condor_geometry::continuous::PolygonPath;
pub use prepared::{
PreparedNavmesh, PreparedNavmeshBuildError, PreparedNavmeshBuilder, StaticPreparedNavmesh,
StaticPreparedNavmeshBuilder,
};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum NavmeshValidationError {
#[error("navmesh cell id must not be empty")]
EmptyCellId,
#[error("navmesh cell '{cell_id}' needs at least three vertices (found {actual})")]
TooFewCellVertices {
cell_id: String,
actual: usize,
},
#[error("navmesh cell '{cell_id}' must have non-zero area")]
DegenerateCell {
cell_id: String,
},
#[error("navmesh cell '{cell_id}' must be convex")]
NonConvexCell {
cell_id: String,
},
#[error("duplicate navmesh cell id '{cell_id}'")]
DuplicateCellId {
cell_id: String,
},
#[error("navmesh portal {portal_index} references missing cell {cell_index}")]
MissingPortalCell {
portal_index: usize,
cell_index: usize,
},
#[error("navmesh portal {portal_index} must connect two different cells")]
SelfPortal {
portal_index: usize,
},
#[error("navmesh portal {portal_index} endpoints must span a non-zero segment")]
DegeneratePortal {
portal_index: usize,
},
#[error("navmesh portal {portal_index} is not on the boundary of cell '{cell_id}'")]
PortalOutsideCellBoundary {
portal_index: usize,
cell_id: String,
},
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DynamicNavmeshError {
#[error("dynamic navmesh base must contain at least one cell")]
EmptyBase,
#[error(transparent)]
InvalidNavmesh(#[from] NavmeshValidationError),
#[error("dynamic navmesh update references missing cell '{cell_id}'")]
MissingCell {
cell_id: String,
},
#[error(
"dynamic navmesh update references missing portal '{left_cell_id}'<->'{right_cell_id}'"
)]
MissingPortal {
left_cell_id: String,
right_cell_id: String,
},
#[error("failed to rebuild prepared navmesh: {source}")]
PreparedRebuild {
#[source]
source: PreparedNavmeshBuildError,
},
}
const EPSILON: f64 = 1e-9;
const ENDPOINT_PROBE_PARAMETER: f64 = 1e-6;
#[derive(Debug, Clone, PartialEq)]
pub struct NavmeshCell {
cell_id: String,
vertices: Vec<Point2>,
}
impl NavmeshCell {
#[must_use]
pub fn new(cell_id: impl Into<String>, vertices: Vec<Point2>) -> Self {
Self {
cell_id: cell_id.into(),
vertices,
}
}
#[must_use]
pub fn cell_id(&self) -> &str {
&self.cell_id
}
#[must_use]
pub fn vertices(&self) -> &[Point2] {
&self.vertices
}
pub fn validate(&self) -> Result<(), NavmeshValidationError> {
if self.cell_id.trim().is_empty() {
return Err(NavmeshValidationError::EmptyCellId);
}
if self.vertices.len() < 3 {
return Err(NavmeshValidationError::TooFewCellVertices {
cell_id: self.cell_id.clone(),
actual: self.vertices.len(),
});
}
if is_degenerate_polygon(&self.vertices) {
return Err(NavmeshValidationError::DegenerateCell {
cell_id: self.cell_id.clone(),
});
}
if !is_convex_polygon(&self.vertices) {
return Err(NavmeshValidationError::NonConvexCell {
cell_id: self.cell_id.clone(),
});
}
Ok(())
}
#[must_use]
pub fn contains_point_inclusive(&self, point: Point2) -> bool {
if polygon_edges(&self.vertices).any(|(start, end)| point_on_segment(point, start, end)) {
return true;
}
let mut reference_sign = 0.0_f64;
for (start, end) in polygon_edges(&self.vertices) {
let sign = orientation(start, end, point);
if sign.abs() <= EPSILON {
continue;
}
if reference_sign.abs() <= EPSILON {
reference_sign = sign;
continue;
}
if sign.signum() != reference_sign.signum() {
return false;
}
}
true
}
#[must_use]
pub fn has_boundary_segment(&self, start: Point2, end: Point2) -> bool {
polygon_edges(&self.vertices).any(|(edge_start, edge_end)| {
point_on_segment(start, edge_start, edge_end)
&& point_on_segment(end, edge_start, edge_end)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NavmeshPortal {
pub left_cell: usize,
pub right_cell: usize,
pub start: Point2,
pub end: Point2,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Navmesh {
cells: Vec<NavmeshCell>,
portals: Vec<NavmeshPortal>,
}
impl Navmesh {
#[must_use]
pub fn new(cells: Vec<NavmeshCell>, portals: Vec<NavmeshPortal>) -> Self {
Self { cells, portals }
}
#[must_use]
pub fn cells(&self) -> &[NavmeshCell] {
&self.cells
}
#[must_use]
pub fn portals(&self) -> &[NavmeshPortal] {
&self.portals
}
pub fn validate(&self) -> Result<(), NavmeshValidationError> {
let mut cell_ids = BTreeMap::new();
for (index, cell) in self.cells.iter().enumerate() {
cell.validate()?;
if cell_ids.insert(cell.cell_id(), index).is_some() {
return Err(NavmeshValidationError::DuplicateCellId {
cell_id: cell.cell_id().to_owned(),
});
}
}
for (portal_index, portal) in self.portals.iter().enumerate() {
if portal.left_cell >= self.cells.len() || portal.right_cell >= self.cells.len() {
let cell_index = if portal.left_cell >= self.cells.len() {
portal.left_cell
} else {
portal.right_cell
};
return Err(NavmeshValidationError::MissingPortalCell {
portal_index,
cell_index,
});
}
if portal.left_cell == portal.right_cell {
return Err(NavmeshValidationError::SelfPortal { portal_index });
}
if points_equal(portal.start, portal.end) {
return Err(NavmeshValidationError::DegeneratePortal { portal_index });
}
let left = &self.cells[portal.left_cell];
let right = &self.cells[portal.right_cell];
if !left.has_boundary_segment(portal.start, portal.end) {
return Err(NavmeshValidationError::PortalOutsideCellBoundary {
portal_index,
cell_id: left.cell_id().to_owned(),
});
}
if !right.has_boundary_segment(portal.start, portal.end) {
return Err(NavmeshValidationError::PortalOutsideCellBoundary {
portal_index,
cell_id: right.cell_id().to_owned(),
});
}
}
Ok(())
}
#[must_use]
pub fn locate_point(&self, point: Point2) -> Option<usize> {
self.locate_cells(point).into_iter().next()
}
#[must_use]
pub fn locate_cells(&self, point: Point2) -> Vec<usize> {
self.cells
.iter()
.enumerate()
.filter_map(|(index, cell)| cell.contains_point_inclusive(point).then_some(index))
.collect()
}
#[must_use]
pub fn neighbors(&self, cell_index: usize) -> Vec<usize> {
self.portals
.iter()
.filter_map(|portal| {
if portal.left_cell == cell_index {
Some(portal.right_cell)
} else if portal.right_cell == cell_index {
Some(portal.left_cell)
} else {
None
}
})
.collect()
}
#[must_use]
pub fn portals_from(&self, cell_index: usize) -> Vec<&NavmeshPortal> {
self.portals
.iter()
.filter(|portal| portal.left_cell == cell_index || portal.right_cell == cell_index)
.collect()
}
#[must_use]
pub fn query(&self, query: NavmeshQuery) -> NavmeshQueryResult {
let start_cells = self.locate_cells(query.start);
let Some(&start_cell) = start_cells.first() else {
return NavmeshQueryResult::InvalidStart;
};
let goal_cells = self.locate_cells(query.goal);
let Some(&goal_cell) = goal_cells.first() else {
return NavmeshQueryResult::InvalidGoal;
};
if let Some((start_cell, goal_cell)) = self.connected_cell_pair(&start_cells, &goal_cells) {
NavmeshQueryResult::Connected {
start_cell,
goal_cell,
}
} else {
NavmeshQueryResult::NoPath {
start_cell,
goal_cell,
}
}
}
fn connected_cell_pair(
&self,
start_cells: &[usize],
goal_cells: &[usize],
) -> Option<(usize, usize)> {
let goal_set: BTreeSet<usize> = goal_cells.iter().copied().collect();
let mut seen = vec![false; self.cells.len()];
let mut frontier = VecDeque::new();
for &start_cell in start_cells {
if start_cell >= seen.len() || seen[start_cell] {
continue;
}
if goal_set.contains(&start_cell) {
return Some((start_cell, start_cell));
}
seen[start_cell] = true;
frontier.push_back((start_cell, start_cell));
}
while let Some((cell_index, source_start_cell)) = frontier.pop_front() {
for neighbor in self.neighbors(cell_index) {
if neighbor >= seen.len() || seen[neighbor] {
continue;
}
if goal_set.contains(&neighbor) {
return Some((source_start_cell, neighbor));
}
seen[neighbor] = true;
frontier.push_back((neighbor, source_start_cell));
}
}
None
}
#[must_use]
pub fn is_walkable(&self, point: Point2) -> bool {
!self.locate_cells(point).is_empty()
}
#[must_use]
pub fn segment_is_walkable(&self, start: Point2, end: Point2) -> bool {
if !self.is_walkable(start) || !self.is_walkable(end) {
return false;
}
let mut parameters = vec![0.0, 1.0];
for cell in &self.cells {
for (edge_start, edge_end) in polygon_edges(cell.vertices()) {
parameters.extend(segment_intersection_parameters(
start, end, edge_start, edge_end,
));
}
}
sort_and_dedup_parameters(&mut parameters);
for parameter in ¶meters {
let point = interpolate_segment(start, end, *parameter);
if !self.is_walkable(point) {
return false;
}
}
let mut interval_cells = Vec::new();
for interval in parameters.windows(2) {
let start_parameter = interval[0];
let end_parameter = interval[1];
if end_parameter - start_parameter <= EPSILON {
continue;
}
let midpoint = interpolate_segment(start, end, (start_parameter + end_parameter) / 2.0);
let cells = self.locate_cells(midpoint);
if cells.is_empty() {
return false;
}
interval_cells.push(cells);
}
for (index, boundary_parameter) in parameters
.iter()
.copied()
.enumerate()
.skip(1)
.take(interval_cells.len().saturating_sub(1))
{
let left_cells = &interval_cells[index - 1];
let right_cells = &interval_cells[index];
if shares_any_cell(left_cells, right_cells) {
continue;
}
let boundary_point = interpolate_segment(start, end, boundary_parameter);
if !self.portal_transition_allowed(left_cells, right_cells, boundary_point) {
return false;
}
}
true
}
#[must_use]
pub fn path_is_walkable(&self, path: &[Point2]) -> bool {
match path {
[] => return false,
[point] => return self.locate_point(*point).is_some(),
_ => {}
}
for pair in path.windows(2) {
if !self.segment_is_walkable(pair[0], pair[1]) {
return false;
}
}
for index in 1..(path.len() - 1) {
let incoming_cells = self.endpoint_probe_cells(path[index - 1], path[index], true);
let outgoing_cells = self.endpoint_probe_cells(path[index], path[index + 1], false);
if shares_any_cell(&incoming_cells, &outgoing_cells) {
continue;
}
if !self.portal_transition_allowed(&incoming_cells, &outgoing_cells, path[index]) {
return false;
}
}
true
}
fn endpoint_probe_cells(&self, start: Point2, end: Point2, near_end: bool) -> Vec<usize> {
let parameter = if near_end {
1.0 - ENDPOINT_PROBE_PARAMETER
} else {
ENDPOINT_PROBE_PARAMETER
};
self.locate_cells(interpolate_segment(start, end, parameter))
}
fn portal_transition_allowed(
&self,
left_cells: &[usize],
right_cells: &[usize],
boundary_point: Point2,
) -> bool {
self.portals.iter().any(|portal| {
let connects_left_to_right =
left_cells.contains(&portal.left_cell) && right_cells.contains(&portal.right_cell);
let connects_right_to_left =
left_cells.contains(&portal.right_cell) && right_cells.contains(&portal.left_cell);
(connects_left_to_right || connects_right_to_left)
&& point_on_segment(boundary_point, portal.start, portal.end)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NavmeshQuery {
pub start: Point2,
pub goal: Point2,
pub budget: condor_core::SearchBudget,
}
impl NavmeshQuery {
#[must_use]
pub const fn new(start: Point2, goal: Point2) -> Self {
Self {
start,
goal,
budget: condor_core::SearchBudget::UNLIMITED,
}
}
#[must_use]
pub const fn with_budget(mut self, budget: condor_core::SearchBudget) -> Self {
self.budget = budget;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavmeshQueryResult {
Connected {
start_cell: usize,
goal_cell: usize,
},
NoPath {
start_cell: usize,
goal_cell: usize,
},
InvalidStart,
InvalidGoal,
}
impl NavmeshQueryResult {
#[must_use]
pub fn is_connected(self) -> bool {
matches!(self, Self::Connected { .. })
}
}
pub type NavmeshPath = PolygonPath;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct NavmeshSearchStats {
pub visited_nodes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum NavmeshSearchError {
#[error("invalid navmesh start: {point:?}")]
InvalidStart {
point: Point2,
},
#[error("invalid navmesh goal: {point:?}")]
InvalidGoal {
point: Point2,
},
#[error(transparent)]
BudgetExhausted(#[from] condor_core::BudgetExhausted),
#[cfg(feature = "polyanya")]
#[error("failed to adapt navmesh for Polyanya: {source}")]
PolyanyaMeshAdapter {
#[from]
#[source]
source: adapter::PolyanyaMeshAdapterError,
},
}
pub type NavmeshSearchResult =
Result<SearchOutcome<NavmeshPath, NavmeshSearchStats>, NavmeshSearchError>;
pub(crate) const fn search_found(path: NavmeshPath, visited_nodes: usize) -> NavmeshSearchResult {
Ok(SearchOutcome::found(
path,
NavmeshSearchStats { visited_nodes },
))
}
pub(crate) const fn search_not_found(visited_nodes: usize) -> NavmeshSearchResult {
Ok(SearchOutcome::no_path(NavmeshSearchStats { visited_nodes }))
}
impl SearchVisitStats for NavmeshSearchStats {
fn visited_nodes(&self) -> usize {
self.visited_nodes
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct DynamicNavmeshPortalKey {
left_cell_id: String,
right_cell_id: String,
}
impl DynamicNavmeshPortalKey {
#[must_use]
pub fn new(left_cell_id: impl Into<String>, right_cell_id: impl Into<String>) -> Self {
let left_cell_id = left_cell_id.into();
let right_cell_id = right_cell_id.into();
if left_cell_id <= right_cell_id {
Self {
left_cell_id,
right_cell_id,
}
} else {
Self {
left_cell_id: right_cell_id,
right_cell_id: left_cell_id,
}
}
}
#[must_use]
pub fn left_cell_id(&self) -> &str {
&self.left_cell_id
}
#[must_use]
pub fn right_cell_id(&self) -> &str {
&self.right_cell_id
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DynamicNavmeshUpdate {
SetCellEnabled {
cell_id: String,
enabled: bool,
},
SetPortalEnabled {
left_cell_id: String,
right_cell_id: String,
enabled: bool,
},
}
impl DynamicNavmeshUpdate {
#[must_use]
pub fn set_cell_enabled(cell_id: impl Into<String>, enabled: bool) -> Self {
Self::SetCellEnabled {
cell_id: cell_id.into(),
enabled,
}
}
#[must_use]
pub fn set_portal_enabled(
left_cell_id: impl Into<String>,
right_cell_id: impl Into<String>,
enabled: bool,
) -> Self {
Self::SetPortalEnabled {
left_cell_id: left_cell_id.into(),
right_cell_id: right_cell_id.into(),
enabled,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DynamicNavmeshState {
base: Navmesh,
disabled_cells: BTreeSet<String>,
disabled_portals: BTreeSet<DynamicNavmeshPortalKey>,
prepared_stale: bool,
}
impl DynamicNavmeshState {
pub fn new(base: Navmesh) -> Result<Self, DynamicNavmeshError> {
if base.cells().is_empty() {
return Err(DynamicNavmeshError::EmptyBase);
}
base.validate()?;
Ok(Self {
base,
disabled_cells: BTreeSet::new(),
disabled_portals: BTreeSet::new(),
prepared_stale: false,
})
}
pub fn with_disabled_availability(
base: Navmesh,
disabled_cells: impl IntoIterator<Item = String>,
disabled_portals: impl IntoIterator<Item = DynamicNavmeshPortalKey>,
) -> Result<Self, DynamicNavmeshError> {
let mut state = Self::new(base)?;
for cell_id in disabled_cells {
state.ensure_cell_exists(&cell_id)?;
state.disabled_cells.insert(cell_id);
}
for portal in disabled_portals {
state.ensure_portal_exists(&portal)?;
state.disabled_portals.insert(portal);
}
Ok(state)
}
#[must_use]
pub fn base(&self) -> &Navmesh {
&self.base
}
#[must_use]
pub fn disabled_cells(&self) -> &BTreeSet<String> {
&self.disabled_cells
}
#[must_use]
pub fn disabled_portals(&self) -> &BTreeSet<DynamicNavmeshPortalKey> {
&self.disabled_portals
}
#[must_use]
pub fn prepared_stale(&self) -> bool {
self.prepared_stale
}
fn mark_prepared_rebuilt(&mut self) {
self.prepared_stale = false;
}
pub fn apply_update(
&mut self,
update: &DynamicNavmeshUpdate,
) -> Result<(), DynamicNavmeshError> {
match update {
DynamicNavmeshUpdate::SetCellEnabled { cell_id, enabled } => {
self.ensure_cell_exists(cell_id)?;
if *enabled {
self.disabled_cells.remove(cell_id);
} else {
self.disabled_cells.insert(cell_id.clone());
}
}
DynamicNavmeshUpdate::SetPortalEnabled {
left_cell_id,
right_cell_id,
enabled,
} => {
let portal = DynamicNavmeshPortalKey::new(left_cell_id, right_cell_id);
self.ensure_portal_exists(&portal)?;
if *enabled {
self.disabled_portals.remove(&portal);
} else {
self.disabled_portals.insert(portal);
}
}
}
self.prepared_stale = true;
Ok(())
}
pub fn materialize(&self) -> Result<Navmesh, DynamicNavmeshError> {
let mut source_to_materialized = BTreeMap::new();
let mut cells = Vec::new();
for (source_index, cell) in self.base.cells().iter().enumerate() {
if self.disabled_cells.contains(cell.cell_id()) {
continue;
}
let materialized_index = cells.len();
source_to_materialized.insert(source_index, materialized_index);
cells.push(cell.clone());
}
let portals = self
.base
.portals()
.iter()
.filter_map(|portal| {
let left = &self.base.cells()[portal.left_cell];
let right = &self.base.cells()[portal.right_cell];
let portal_key = DynamicNavmeshPortalKey::new(left.cell_id(), right.cell_id());
let left_cell = *source_to_materialized.get(&portal.left_cell)?;
let right_cell = *source_to_materialized.get(&portal.right_cell)?;
(!self.disabled_portals.contains(&portal_key)).then_some(NavmeshPortal {
left_cell,
right_cell,
start: portal.start,
end: portal.end,
})
})
.collect::<Vec<_>>();
let navmesh = Navmesh::new(cells, portals);
navmesh.validate()?;
Ok(navmesh)
}
fn ensure_cell_exists(&self, cell_id: &str) -> Result<(), DynamicNavmeshError> {
if self
.base
.cells()
.iter()
.any(|cell| cell.cell_id() == cell_id)
{
Ok(())
} else {
Err(DynamicNavmeshError::MissingCell {
cell_id: cell_id.to_owned(),
})
}
}
fn ensure_portal_exists(
&self,
portal: &DynamicNavmeshPortalKey,
) -> Result<(), DynamicNavmeshError> {
if self.base.portals().iter().any(|candidate| {
let left = &self.base.cells()[candidate.left_cell];
let right = &self.base.cells()[candidate.right_cell];
DynamicNavmeshPortalKey::new(left.cell_id(), right.cell_id()) == *portal
}) {
Ok(())
} else {
Err(DynamicNavmeshError::MissingPortal {
left_cell_id: portal.left_cell_id().to_owned(),
right_cell_id: portal.right_cell_id().to_owned(),
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DynamicPreparedNavmeshRebuildStatus {
RebuiltFromMaterializedSnapshot,
}
impl DynamicPreparedNavmeshRebuildStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::RebuiltFromMaterializedSnapshot => "rebuilt-from-materialized-snapshot",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DynamicPreparedNavmeshQueryMetadata {
pub builder_name: &'static str,
pub applied_update_count: usize,
pub prepared_stale_before_updates: bool,
pub prepared_stale_after_updates: bool,
pub prepared_stale_after_rebuild: bool,
pub materialized_cell_count: usize,
pub materialized_portal_count: usize,
pub rebuild_status: DynamicPreparedNavmeshRebuildStatus,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DynamicPreparedNavmeshQueryResult<M> {
pub materialized_navmesh: Navmesh,
pub prepared_navmesh: M,
pub raw_result: NavmeshQueryResult,
pub rebuilt_prepared_result: NavmeshQueryResult,
pub metadata: DynamicPreparedNavmeshQueryMetadata,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DynamicPreparedNavmeshQuery;
impl DynamicPreparedNavmeshQuery {
pub fn run<B, U>(
state: &mut DynamicNavmeshState,
updates: impl IntoIterator<Item = U>,
query: NavmeshQuery,
builder: &B,
) -> Result<DynamicPreparedNavmeshQueryResult<B::Map>, DynamicNavmeshError>
where
B: PreparedNavmeshBuilder,
U: Borrow<DynamicNavmeshUpdate>,
{
let prepared_stale_before_updates = state.prepared_stale();
let mut applied_update_count = 0;
for update in updates {
state.apply_update(update.borrow())?;
applied_update_count += 1;
}
let prepared_stale_after_updates = state.prepared_stale();
let materialized_navmesh = state.materialize()?;
let raw_result = materialized_navmesh.query(query);
let prepared_navmesh = builder
.preprocess(&materialized_navmesh)
.map_err(|source| DynamicNavmeshError::PreparedRebuild { source })?;
let rebuilt_prepared_result = prepared_navmesh.query(query);
state.mark_prepared_rebuilt();
let metadata = DynamicPreparedNavmeshQueryMetadata {
builder_name: builder.name(),
applied_update_count,
prepared_stale_before_updates,
prepared_stale_after_updates,
prepared_stale_after_rebuild: state.prepared_stale(),
materialized_cell_count: materialized_navmesh.cells().len(),
materialized_portal_count: materialized_navmesh.portals().len(),
rebuild_status: DynamicPreparedNavmeshRebuildStatus::RebuiltFromMaterializedSnapshot,
};
Ok(DynamicPreparedNavmeshQueryResult {
materialized_navmesh,
prepared_navmesh,
raw_result,
rebuilt_prepared_result,
metadata,
})
}
}
pub trait NavmeshPathfinder {
fn name(&self) -> &'static str;
fn search(&self, navmesh: &Navmesh, query: NavmeshQuery) -> NavmeshSearchResult;
}
fn polygon_edges(vertices: &[Point2]) -> impl Iterator<Item = (Point2, Point2)> + '_ {
vertices
.iter()
.copied()
.zip(vertices.iter().copied().cycle().skip(1))
.take(vertices.len())
}
fn sort_and_dedup_parameters(parameters: &mut Vec<f64>) {
parameters.sort_by(f64::total_cmp);
parameters.dedup_by(|left, right| (*left - *right).abs() <= EPSILON);
}
fn interpolate_segment(start: Point2, end: Point2, parameter: f64) -> Point2 {
Point2::new(
start.x + ((end.x - start.x) * parameter),
start.y + ((end.y - start.y) * parameter),
)
}
fn segment_intersection_parameters(
a_start: Point2,
a_end: Point2,
b_start: Point2,
b_end: Point2,
) -> Vec<f64> {
let mut parameters = Vec::with_capacity(2);
for point in [a_start, a_end, b_start, b_end] {
if point_on_segment(point, a_start, a_end) && point_on_segment(point, b_start, b_end) {
parameters.push(segment_parameter(point, a_start, a_end));
}
}
if !parameters.is_empty() {
sort_and_dedup_parameters(&mut parameters);
return parameters;
}
if let Some(parameter) = proper_intersection_parameter(a_start, a_end, b_start, b_end) {
parameters.push(parameter);
}
parameters
}
fn segment_parameter(point: Point2, start: Point2, end: Point2) -> f64 {
let dx = end.x - start.x;
let dy = end.y - start.y;
if dx.abs() >= dy.abs() && dx.abs() > EPSILON {
((point.x - start.x) / dx).clamp(0.0, 1.0)
} else if dy.abs() > EPSILON {
((point.y - start.y) / dy).clamp(0.0, 1.0)
} else {
0.0
}
}
fn proper_intersection_parameter(
a_start: Point2,
a_end: Point2,
b_start: Point2,
b_end: Point2,
) -> Option<f64> {
let o1 = orientation(a_start, a_end, b_start);
let o2 = orientation(a_start, a_end, b_end);
let o3 = orientation(b_start, b_end, a_start);
let o4 = orientation(b_start, b_end, a_end);
let properly_crosses = (o1 > EPSILON && o2 < -EPSILON || o1 < -EPSILON && o2 > EPSILON)
&& (o3 > EPSILON && o4 < -EPSILON || o3 < -EPSILON && o4 > EPSILON);
if !properly_crosses {
return None;
}
let a_dx = a_end.x - a_start.x;
let a_dy = a_end.y - a_start.y;
let b_dx = b_end.x - b_start.x;
let b_dy = b_end.y - b_start.y;
let denominator = cross(a_dx, a_dy, b_dx, b_dy);
if denominator.abs() <= EPSILON {
return None;
}
let offset_x = b_start.x - a_start.x;
let offset_y = b_start.y - a_start.y;
Some((cross(offset_x, offset_y, b_dx, b_dy) / denominator).clamp(0.0, 1.0))
}
fn point_on_segment(point: Point2, start: Point2, end: Point2) -> bool {
orientation(start, end, point).abs() <= EPSILON
&& point.x >= start.x.min(end.x) - EPSILON
&& point.x <= start.x.max(end.x) + EPSILON
&& point.y >= start.y.min(end.y) - EPSILON
&& point.y <= start.y.max(end.y) + EPSILON
}
fn orientation(start: Point2, end: Point2, point: Point2) -> f64 {
((end.x - start.x) * (point.y - start.y)) - ((end.y - start.y) * (point.x - start.x))
}
fn cross(left_x: f64, left_y: f64, right_x: f64, right_y: f64) -> f64 {
(left_x * right_y) - (left_y * right_x)
}
fn is_degenerate_polygon(vertices: &[Point2]) -> bool {
signed_area(vertices).abs() <= EPSILON
}
fn signed_area(vertices: &[Point2]) -> f64 {
polygon_edges(vertices)
.map(|(left, right)| (left.x * right.y) - (right.x * left.y))
.sum::<f64>()
/ 2.0
}
fn is_convex_polygon(vertices: &[Point2]) -> bool {
let mut reference_sign = 0.0_f64;
for index in 0..vertices.len() {
let a = vertices[index];
let b = vertices[(index + 1) % vertices.len()];
let c = vertices[(index + 2) % vertices.len()];
let turn = orientation(a, b, c);
if turn.abs() <= EPSILON {
continue;
}
if reference_sign.abs() <= EPSILON {
reference_sign = turn;
continue;
}
if turn.signum() != reference_sign.signum() {
return false;
}
}
true
}
pub(crate) fn points_equal(left: Point2, right: Point2) -> bool {
(left.x - right.x).abs() <= EPSILON && (left.y - right.y).abs() <= EPSILON
}
fn shares_any_cell(left: &[usize], right: &[usize]) -> bool {
left.iter().any(|cell| right.contains(cell))
}
#[cfg(test)]
mod tests {
use super::{Navmesh, NavmeshCell, NavmeshPortal, NavmeshQuery, NavmeshQueryResult};
use condor_core::Point2;
#[test]
fn validates_and_queries_a_two_cell_mesh() {
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, 2.0),
Point2::new(0.0, 2.0),
],
),
NavmeshCell::new(
"right",
vec![
Point2::new(2.0, 0.0),
Point2::new(4.0, 0.0),
Point2::new(4.0, 2.0),
Point2::new(2.0, 2.0),
],
),
],
vec![NavmeshPortal {
left_cell: 0,
right_cell: 1,
start: Point2::new(2.0, 0.0),
end: Point2::new(2.0, 2.0),
}],
);
navmesh.validate().expect("valid mesh");
assert!(matches!(
navmesh.query(NavmeshQuery::new(
Point2::new(1.0, 1.0),
Point2::new(3.0, 1.0)
)),
NavmeshQueryResult::Connected { .. }
));
}
}