1use std::{
33 borrow::Borrow,
34 collections::{BTreeMap, BTreeSet, VecDeque},
35};
36
37#[cfg(feature = "polyanya")]
39pub mod adapter;
40pub mod corridor;
42pub mod funnel;
44pub mod prepared;
46
47use condor_core::{Point2, SearchOutcome, SearchVisitStats};
48use condor_geometry::continuous::PolygonPath;
49pub use prepared::{
50 PreparedNavmesh, PreparedNavmeshBuildError, PreparedNavmeshBuilder, StaticPreparedNavmesh,
51 StaticPreparedNavmeshBuilder,
52};
53
54#[derive(Debug, Clone, PartialEq, thiserror::Error)]
60#[non_exhaustive]
61pub enum NavmeshValidationError {
62 #[error("navmesh cell id must not be empty")]
64 EmptyCellId,
65 #[error("navmesh cell '{cell_id}' needs at least three vertices (found {actual})")]
67 TooFewCellVertices {
68 cell_id: String,
70 actual: usize,
72 },
73 #[error("navmesh cell '{cell_id}' must have non-zero area")]
75 DegenerateCell {
76 cell_id: String,
78 },
79 #[error("navmesh cell '{cell_id}' must be convex")]
81 NonConvexCell {
82 cell_id: String,
84 },
85 #[error("duplicate navmesh cell id '{cell_id}'")]
87 DuplicateCellId {
88 cell_id: String,
90 },
91 #[error("navmesh portal {portal_index} references missing cell {cell_index}")]
93 MissingPortalCell {
94 portal_index: usize,
96 cell_index: usize,
98 },
99 #[error("navmesh portal {portal_index} must connect two different cells")]
101 SelfPortal {
102 portal_index: usize,
104 },
105 #[error("navmesh portal {portal_index} endpoints must span a non-zero segment")]
107 DegeneratePortal {
108 portal_index: usize,
110 },
111 #[error("navmesh portal {portal_index} is not on the boundary of cell '{cell_id}'")]
113 PortalOutsideCellBoundary {
114 portal_index: usize,
116 cell_id: String,
118 },
119}
120
121#[derive(Debug, thiserror::Error)]
126#[non_exhaustive]
127pub enum DynamicNavmeshError {
128 #[error("dynamic navmesh base must contain at least one cell")]
130 EmptyBase,
131 #[error(transparent)]
133 InvalidNavmesh(#[from] NavmeshValidationError),
134 #[error("dynamic navmesh update references missing cell '{cell_id}'")]
136 MissingCell {
137 cell_id: String,
139 },
140 #[error(
142 "dynamic navmesh update references missing portal '{left_cell_id}'<->'{right_cell_id}'"
143 )]
144 MissingPortal {
145 left_cell_id: String,
147 right_cell_id: String,
149 },
150 #[error("failed to rebuild prepared navmesh: {source}")]
152 PreparedRebuild {
153 #[source]
155 source: PreparedNavmeshBuildError,
156 },
157}
158const EPSILON: f64 = 1e-9;
159const ENDPOINT_PROBE_PARAMETER: f64 = 1e-6;
160
161#[derive(Debug, Clone, PartialEq)]
167pub struct NavmeshCell {
168 cell_id: String,
169 vertices: Vec<Point2>,
170}
171
172impl NavmeshCell {
173 #[must_use]
175 pub fn new(cell_id: impl Into<String>, vertices: Vec<Point2>) -> Self {
176 Self {
177 cell_id: cell_id.into(),
178 vertices,
179 }
180 }
181
182 #[must_use]
184 pub fn cell_id(&self) -> &str {
185 &self.cell_id
186 }
187
188 #[must_use]
190 pub fn vertices(&self) -> &[Point2] {
191 &self.vertices
192 }
193
194 pub fn validate(&self) -> Result<(), NavmeshValidationError> {
201 if self.cell_id.trim().is_empty() {
202 return Err(NavmeshValidationError::EmptyCellId);
203 }
204
205 if self.vertices.len() < 3 {
206 return Err(NavmeshValidationError::TooFewCellVertices {
207 cell_id: self.cell_id.clone(),
208 actual: self.vertices.len(),
209 });
210 }
211
212 if is_degenerate_polygon(&self.vertices) {
213 return Err(NavmeshValidationError::DegenerateCell {
214 cell_id: self.cell_id.clone(),
215 });
216 }
217
218 if !is_convex_polygon(&self.vertices) {
219 return Err(NavmeshValidationError::NonConvexCell {
220 cell_id: self.cell_id.clone(),
221 });
222 }
223
224 Ok(())
225 }
226
227 #[must_use]
229 pub fn contains_point_inclusive(&self, point: Point2) -> bool {
230 if polygon_edges(&self.vertices).any(|(start, end)| point_on_segment(point, start, end)) {
231 return true;
232 }
233
234 let mut reference_sign = 0.0_f64;
235 for (start, end) in polygon_edges(&self.vertices) {
236 let sign = orientation(start, end, point);
237 if sign.abs() <= EPSILON {
238 continue;
239 }
240
241 if reference_sign.abs() <= EPSILON {
242 reference_sign = sign;
243 continue;
244 }
245
246 if sign.signum() != reference_sign.signum() {
247 return false;
248 }
249 }
250
251 true
252 }
253
254 #[must_use]
256 pub fn has_boundary_segment(&self, start: Point2, end: Point2) -> bool {
257 polygon_edges(&self.vertices).any(|(edge_start, edge_end)| {
258 point_on_segment(start, edge_start, edge_end)
259 && point_on_segment(end, edge_start, edge_end)
260 })
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq)]
269pub struct NavmeshPortal {
270 pub left_cell: usize,
272 pub right_cell: usize,
274 pub start: Point2,
276 pub end: Point2,
278}
279
280#[derive(Debug, Clone, PartialEq)]
286pub struct Navmesh {
287 cells: Vec<NavmeshCell>,
288 portals: Vec<NavmeshPortal>,
289}
290
291impl Navmesh {
292 #[must_use]
294 pub fn new(cells: Vec<NavmeshCell>, portals: Vec<NavmeshPortal>) -> Self {
295 Self { cells, portals }
296 }
297
298 #[must_use]
300 pub fn cells(&self) -> &[NavmeshCell] {
301 &self.cells
302 }
303
304 #[must_use]
306 pub fn portals(&self) -> &[NavmeshPortal] {
307 &self.portals
308 }
309
310 pub fn validate(&self) -> Result<(), NavmeshValidationError> {
317 let mut cell_ids = BTreeMap::new();
318 for (index, cell) in self.cells.iter().enumerate() {
319 cell.validate()?;
320 if cell_ids.insert(cell.cell_id(), index).is_some() {
321 return Err(NavmeshValidationError::DuplicateCellId {
322 cell_id: cell.cell_id().to_owned(),
323 });
324 }
325 }
326
327 for (portal_index, portal) in self.portals.iter().enumerate() {
328 if portal.left_cell >= self.cells.len() || portal.right_cell >= self.cells.len() {
329 let cell_index = if portal.left_cell >= self.cells.len() {
330 portal.left_cell
331 } else {
332 portal.right_cell
333 };
334 return Err(NavmeshValidationError::MissingPortalCell {
335 portal_index,
336 cell_index,
337 });
338 }
339 if portal.left_cell == portal.right_cell {
340 return Err(NavmeshValidationError::SelfPortal { portal_index });
341 }
342 if points_equal(portal.start, portal.end) {
343 return Err(NavmeshValidationError::DegeneratePortal { portal_index });
344 }
345
346 let left = &self.cells[portal.left_cell];
347 let right = &self.cells[portal.right_cell];
348 if !left.has_boundary_segment(portal.start, portal.end) {
349 return Err(NavmeshValidationError::PortalOutsideCellBoundary {
350 portal_index,
351 cell_id: left.cell_id().to_owned(),
352 });
353 }
354 if !right.has_boundary_segment(portal.start, portal.end) {
355 return Err(NavmeshValidationError::PortalOutsideCellBoundary {
356 portal_index,
357 cell_id: right.cell_id().to_owned(),
358 });
359 }
360 }
361
362 Ok(())
363 }
364
365 #[must_use]
370 pub fn locate_point(&self, point: Point2) -> Option<usize> {
371 self.locate_cells(point).into_iter().next()
372 }
373
374 #[must_use]
378 pub fn locate_cells(&self, point: Point2) -> Vec<usize> {
379 self.cells
380 .iter()
381 .enumerate()
382 .filter_map(|(index, cell)| cell.contains_point_inclusive(point).then_some(index))
383 .collect()
384 }
385
386 #[must_use]
391 pub fn neighbors(&self, cell_index: usize) -> Vec<usize> {
392 self.portals
393 .iter()
394 .filter_map(|portal| {
395 if portal.left_cell == cell_index {
396 Some(portal.right_cell)
397 } else if portal.right_cell == cell_index {
398 Some(portal.left_cell)
399 } else {
400 None
401 }
402 })
403 .collect()
404 }
405
406 #[must_use]
408 pub fn portals_from(&self, cell_index: usize) -> Vec<&NavmeshPortal> {
409 self.portals
410 .iter()
411 .filter(|portal| portal.left_cell == cell_index || portal.right_cell == cell_index)
412 .collect()
413 }
414
415 #[must_use]
425 pub fn query(&self, query: NavmeshQuery) -> NavmeshQueryResult {
426 let start_cells = self.locate_cells(query.start);
428 let Some(&start_cell) = start_cells.first() else {
429 return NavmeshQueryResult::InvalidStart;
430 };
431 let goal_cells = self.locate_cells(query.goal);
432 let Some(&goal_cell) = goal_cells.first() else {
433 return NavmeshQueryResult::InvalidGoal;
434 };
435
436 if let Some((start_cell, goal_cell)) = self.connected_cell_pair(&start_cells, &goal_cells) {
437 NavmeshQueryResult::Connected {
438 start_cell,
439 goal_cell,
440 }
441 } else {
442 NavmeshQueryResult::NoPath {
443 start_cell,
444 goal_cell,
445 }
446 }
447 }
448
449 fn connected_cell_pair(
450 &self,
451 start_cells: &[usize],
452 goal_cells: &[usize],
453 ) -> Option<(usize, usize)> {
454 let goal_set: BTreeSet<usize> = goal_cells.iter().copied().collect();
455 let mut seen = vec![false; self.cells.len()];
456 let mut frontier = VecDeque::new();
457
458 for &start_cell in start_cells {
459 if start_cell >= seen.len() || seen[start_cell] {
460 continue;
461 }
462 if goal_set.contains(&start_cell) {
463 return Some((start_cell, start_cell));
464 }
465 seen[start_cell] = true;
466 frontier.push_back((start_cell, start_cell));
467 }
468
469 while let Some((cell_index, source_start_cell)) = frontier.pop_front() {
470 for neighbor in self.neighbors(cell_index) {
471 if neighbor >= seen.len() || seen[neighbor] {
472 continue;
473 }
474 if goal_set.contains(&neighbor) {
475 return Some((source_start_cell, neighbor));
476 }
477 seen[neighbor] = true;
478 frontier.push_back((neighbor, source_start_cell));
479 }
480 }
481
482 None
483 }
484
485 #[must_use]
487 pub fn is_walkable(&self, point: Point2) -> bool {
488 !self.locate_cells(point).is_empty()
489 }
490
491 #[must_use]
498 pub fn segment_is_walkable(&self, start: Point2, end: Point2) -> bool {
499 if !self.is_walkable(start) || !self.is_walkable(end) {
500 return false;
501 }
502
503 let mut parameters = vec![0.0, 1.0];
504 for cell in &self.cells {
505 for (edge_start, edge_end) in polygon_edges(cell.vertices()) {
506 parameters.extend(segment_intersection_parameters(
507 start, end, edge_start, edge_end,
508 ));
509 }
510 }
511
512 sort_and_dedup_parameters(&mut parameters);
513
514 for parameter in ¶meters {
515 let point = interpolate_segment(start, end, *parameter);
516 if !self.is_walkable(point) {
517 return false;
518 }
519 }
520
521 let mut interval_cells = Vec::new();
522 for interval in parameters.windows(2) {
523 let start_parameter = interval[0];
524 let end_parameter = interval[1];
525 if end_parameter - start_parameter <= EPSILON {
526 continue;
527 }
528
529 let midpoint = interpolate_segment(start, end, (start_parameter + end_parameter) / 2.0);
530 let cells = self.locate_cells(midpoint);
531 if cells.is_empty() {
532 return false;
533 }
534 interval_cells.push(cells);
535 }
536
537 for (index, boundary_parameter) in parameters
538 .iter()
539 .copied()
540 .enumerate()
541 .skip(1)
542 .take(interval_cells.len().saturating_sub(1))
543 {
544 let left_cells = &interval_cells[index - 1];
545 let right_cells = &interval_cells[index];
546 if shares_any_cell(left_cells, right_cells) {
547 continue;
548 }
549
550 let boundary_point = interpolate_segment(start, end, boundary_parameter);
551 if !self.portal_transition_allowed(left_cells, right_cells, boundary_point) {
552 return false;
553 }
554 }
555
556 true
557 }
558
559 #[must_use]
564 pub fn path_is_walkable(&self, path: &[Point2]) -> bool {
565 match path {
566 [] => return false,
567 [point] => return self.locate_point(*point).is_some(),
568 _ => {}
569 }
570
571 for pair in path.windows(2) {
572 if !self.segment_is_walkable(pair[0], pair[1]) {
573 return false;
574 }
575 }
576
577 for index in 1..(path.len() - 1) {
578 let incoming_cells = self.endpoint_probe_cells(path[index - 1], path[index], true);
579 let outgoing_cells = self.endpoint_probe_cells(path[index], path[index + 1], false);
580 if shares_any_cell(&incoming_cells, &outgoing_cells) {
581 continue;
582 }
583
584 if !self.portal_transition_allowed(&incoming_cells, &outgoing_cells, path[index]) {
585 return false;
586 }
587 }
588
589 true
590 }
591
592 fn endpoint_probe_cells(&self, start: Point2, end: Point2, near_end: bool) -> Vec<usize> {
593 let parameter = if near_end {
594 1.0 - ENDPOINT_PROBE_PARAMETER
595 } else {
596 ENDPOINT_PROBE_PARAMETER
597 };
598 self.locate_cells(interpolate_segment(start, end, parameter))
599 }
600
601 fn portal_transition_allowed(
602 &self,
603 left_cells: &[usize],
604 right_cells: &[usize],
605 boundary_point: Point2,
606 ) -> bool {
607 self.portals.iter().any(|portal| {
608 let connects_left_to_right =
609 left_cells.contains(&portal.left_cell) && right_cells.contains(&portal.right_cell);
610 let connects_right_to_left =
611 left_cells.contains(&portal.right_cell) && right_cells.contains(&portal.left_cell);
612
613 (connects_left_to_right || connects_right_to_left)
614 && point_on_segment(boundary_point, portal.start, portal.end)
615 })
616 }
617}
618
619#[derive(Debug, Clone, Copy, PartialEq)]
625pub struct NavmeshQuery {
626 pub start: Point2,
628 pub goal: Point2,
630 pub budget: condor_core::SearchBudget,
632}
633
634impl NavmeshQuery {
635 #[must_use]
637 pub const fn new(start: Point2, goal: Point2) -> Self {
638 Self {
639 start,
640 goal,
641 budget: condor_core::SearchBudget::UNLIMITED,
642 }
643 }
644
645 #[must_use]
647 pub const fn with_budget(mut self, budget: condor_core::SearchBudget) -> Self {
648 self.budget = budget;
649 self
650 }
651}
652
653#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658pub enum NavmeshQueryResult {
659 Connected {
661 start_cell: usize,
663 goal_cell: usize,
665 },
666 NoPath {
668 start_cell: usize,
670 goal_cell: usize,
672 },
673 InvalidStart,
675 InvalidGoal,
677}
678
679impl NavmeshQueryResult {
680 #[must_use]
682 pub fn is_connected(self) -> bool {
683 matches!(self, Self::Connected { .. })
684 }
685}
686
687pub type NavmeshPath = PolygonPath;
692
693#[derive(Debug, Clone, Copy, Default, PartialEq)]
698pub struct NavmeshSearchStats {
699 pub visited_nodes: usize,
701}
702
703#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
710#[non_exhaustive]
711pub enum NavmeshSearchError {
712 #[error("invalid navmesh start: {point:?}")]
714 InvalidStart {
715 point: Point2,
717 },
718 #[error("invalid navmesh goal: {point:?}")]
720 InvalidGoal {
721 point: Point2,
723 },
724 #[error(transparent)]
726 BudgetExhausted(#[from] condor_core::BudgetExhausted),
727 #[cfg(feature = "polyanya")]
729 #[error("failed to adapt navmesh for Polyanya: {source}")]
730 PolyanyaMeshAdapter {
731 #[from]
733 #[source]
734 source: adapter::PolyanyaMeshAdapterError,
735 },
736}
737
738pub type NavmeshSearchResult =
744 Result<SearchOutcome<NavmeshPath, NavmeshSearchStats>, NavmeshSearchError>;
745
746pub(crate) const fn search_found(path: NavmeshPath, visited_nodes: usize) -> NavmeshSearchResult {
748 Ok(SearchOutcome::found(
749 path,
750 NavmeshSearchStats { visited_nodes },
751 ))
752}
753
754pub(crate) const fn search_not_found(visited_nodes: usize) -> NavmeshSearchResult {
756 Ok(SearchOutcome::no_path(NavmeshSearchStats { visited_nodes }))
757}
758
759impl SearchVisitStats for NavmeshSearchStats {
760 fn visited_nodes(&self) -> usize {
761 self.visited_nodes
762 }
763}
764
765#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
770pub struct DynamicNavmeshPortalKey {
771 left_cell_id: String,
772 right_cell_id: String,
773}
774
775impl DynamicNavmeshPortalKey {
776 #[must_use]
778 pub fn new(left_cell_id: impl Into<String>, right_cell_id: impl Into<String>) -> Self {
779 let left_cell_id = left_cell_id.into();
780 let right_cell_id = right_cell_id.into();
781 if left_cell_id <= right_cell_id {
782 Self {
783 left_cell_id,
784 right_cell_id,
785 }
786 } else {
787 Self {
788 left_cell_id: right_cell_id,
789 right_cell_id: left_cell_id,
790 }
791 }
792 }
793
794 #[must_use]
796 pub fn left_cell_id(&self) -> &str {
797 &self.left_cell_id
798 }
799
800 #[must_use]
802 pub fn right_cell_id(&self) -> &str {
803 &self.right_cell_id
804 }
805}
806
807#[derive(Debug, Clone, PartialEq, Eq)]
812pub enum DynamicNavmeshUpdate {
813 SetCellEnabled {
815 cell_id: String,
817 enabled: bool,
819 },
820 SetPortalEnabled {
822 left_cell_id: String,
824 right_cell_id: String,
826 enabled: bool,
828 },
829}
830
831impl DynamicNavmeshUpdate {
832 #[must_use]
834 pub fn set_cell_enabled(cell_id: impl Into<String>, enabled: bool) -> Self {
835 Self::SetCellEnabled {
836 cell_id: cell_id.into(),
837 enabled,
838 }
839 }
840
841 #[must_use]
843 pub fn set_portal_enabled(
844 left_cell_id: impl Into<String>,
845 right_cell_id: impl Into<String>,
846 enabled: bool,
847 ) -> Self {
848 Self::SetPortalEnabled {
849 left_cell_id: left_cell_id.into(),
850 right_cell_id: right_cell_id.into(),
851 enabled,
852 }
853 }
854}
855
856#[derive(Debug, Clone, PartialEq)]
871pub struct DynamicNavmeshState {
872 base: Navmesh,
873 disabled_cells: BTreeSet<String>,
874 disabled_portals: BTreeSet<DynamicNavmeshPortalKey>,
875 prepared_stale: bool,
876}
877
878impl DynamicNavmeshState {
879 pub fn new(base: Navmesh) -> Result<Self, DynamicNavmeshError> {
885 if base.cells().is_empty() {
886 return Err(DynamicNavmeshError::EmptyBase);
887 }
888 base.validate()?;
889 Ok(Self {
890 base,
891 disabled_cells: BTreeSet::new(),
892 disabled_portals: BTreeSet::new(),
893 prepared_stale: false,
894 })
895 }
896
897 pub fn with_disabled_availability(
906 base: Navmesh,
907 disabled_cells: impl IntoIterator<Item = String>,
908 disabled_portals: impl IntoIterator<Item = DynamicNavmeshPortalKey>,
909 ) -> Result<Self, DynamicNavmeshError> {
910 let mut state = Self::new(base)?;
911 for cell_id in disabled_cells {
912 state.ensure_cell_exists(&cell_id)?;
913 state.disabled_cells.insert(cell_id);
914 }
915 for portal in disabled_portals {
916 state.ensure_portal_exists(&portal)?;
917 state.disabled_portals.insert(portal);
918 }
919 Ok(state)
920 }
921
922 #[must_use]
924 pub fn base(&self) -> &Navmesh {
925 &self.base
926 }
927
928 #[must_use]
930 pub fn disabled_cells(&self) -> &BTreeSet<String> {
931 &self.disabled_cells
932 }
933
934 #[must_use]
936 pub fn disabled_portals(&self) -> &BTreeSet<DynamicNavmeshPortalKey> {
937 &self.disabled_portals
938 }
939
940 #[must_use]
944 pub fn prepared_stale(&self) -> bool {
945 self.prepared_stale
946 }
947
948 fn mark_prepared_rebuilt(&mut self) {
949 self.prepared_stale = false;
950 }
951
952 pub fn apply_update(
962 &mut self,
963 update: &DynamicNavmeshUpdate,
964 ) -> Result<(), DynamicNavmeshError> {
965 match update {
966 DynamicNavmeshUpdate::SetCellEnabled { cell_id, enabled } => {
967 self.ensure_cell_exists(cell_id)?;
968 if *enabled {
969 self.disabled_cells.remove(cell_id);
970 } else {
971 self.disabled_cells.insert(cell_id.clone());
972 }
973 }
974 DynamicNavmeshUpdate::SetPortalEnabled {
975 left_cell_id,
976 right_cell_id,
977 enabled,
978 } => {
979 let portal = DynamicNavmeshPortalKey::new(left_cell_id, right_cell_id);
980 self.ensure_portal_exists(&portal)?;
981 if *enabled {
982 self.disabled_portals.remove(&portal);
983 } else {
984 self.disabled_portals.insert(portal);
985 }
986 }
987 }
988
989 self.prepared_stale = true;
990 Ok(())
991 }
992
993 pub fn materialize(&self) -> Result<Navmesh, DynamicNavmeshError> {
1007 let mut source_to_materialized = BTreeMap::new();
1008 let mut cells = Vec::new();
1009 for (source_index, cell) in self.base.cells().iter().enumerate() {
1010 if self.disabled_cells.contains(cell.cell_id()) {
1011 continue;
1012 }
1013
1014 let materialized_index = cells.len();
1015 source_to_materialized.insert(source_index, materialized_index);
1016 cells.push(cell.clone());
1017 }
1018
1019 let portals = self
1020 .base
1021 .portals()
1022 .iter()
1023 .filter_map(|portal| {
1024 let left = &self.base.cells()[portal.left_cell];
1025 let right = &self.base.cells()[portal.right_cell];
1026 let portal_key = DynamicNavmeshPortalKey::new(left.cell_id(), right.cell_id());
1027 let left_cell = *source_to_materialized.get(&portal.left_cell)?;
1028 let right_cell = *source_to_materialized.get(&portal.right_cell)?;
1029 (!self.disabled_portals.contains(&portal_key)).then_some(NavmeshPortal {
1030 left_cell,
1031 right_cell,
1032 start: portal.start,
1033 end: portal.end,
1034 })
1035 })
1036 .collect::<Vec<_>>();
1037
1038 let navmesh = Navmesh::new(cells, portals);
1039 navmesh.validate()?;
1040 Ok(navmesh)
1041 }
1042
1043 fn ensure_cell_exists(&self, cell_id: &str) -> Result<(), DynamicNavmeshError> {
1044 if self
1045 .base
1046 .cells()
1047 .iter()
1048 .any(|cell| cell.cell_id() == cell_id)
1049 {
1050 Ok(())
1051 } else {
1052 Err(DynamicNavmeshError::MissingCell {
1053 cell_id: cell_id.to_owned(),
1054 })
1055 }
1056 }
1057
1058 fn ensure_portal_exists(
1059 &self,
1060 portal: &DynamicNavmeshPortalKey,
1061 ) -> Result<(), DynamicNavmeshError> {
1062 if self.base.portals().iter().any(|candidate| {
1063 let left = &self.base.cells()[candidate.left_cell];
1064 let right = &self.base.cells()[candidate.right_cell];
1065 DynamicNavmeshPortalKey::new(left.cell_id(), right.cell_id()) == *portal
1066 }) {
1067 Ok(())
1068 } else {
1069 Err(DynamicNavmeshError::MissingPortal {
1070 left_cell_id: portal.left_cell_id().to_owned(),
1071 right_cell_id: portal.right_cell_id().to_owned(),
1072 })
1073 }
1074 }
1075}
1076
1077#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1082pub enum DynamicPreparedNavmeshRebuildStatus {
1083 RebuiltFromMaterializedSnapshot,
1085}
1086
1087impl DynamicPreparedNavmeshRebuildStatus {
1088 #[must_use]
1090 pub const fn as_str(self) -> &'static str {
1091 match self {
1092 Self::RebuiltFromMaterializedSnapshot => "rebuilt-from-materialized-snapshot",
1093 }
1094 }
1095}
1096
1097#[derive(Debug, Clone, PartialEq, Eq)]
1103pub struct DynamicPreparedNavmeshQueryMetadata {
1104 pub builder_name: &'static str,
1106 pub applied_update_count: usize,
1108 pub prepared_stale_before_updates: bool,
1110 pub prepared_stale_after_updates: bool,
1112 pub prepared_stale_after_rebuild: bool,
1114 pub materialized_cell_count: usize,
1116 pub materialized_portal_count: usize,
1118 pub rebuild_status: DynamicPreparedNavmeshRebuildStatus,
1120}
1121
1122#[derive(Debug, Clone, PartialEq)]
1127pub struct DynamicPreparedNavmeshQueryResult<M> {
1128 pub materialized_navmesh: Navmesh,
1130 pub prepared_navmesh: M,
1132 pub raw_result: NavmeshQueryResult,
1134 pub rebuilt_prepared_result: NavmeshQueryResult,
1136 pub metadata: DynamicPreparedNavmeshQueryMetadata,
1138}
1139
1140#[derive(Debug, Clone, Copy, Default)]
1146pub struct DynamicPreparedNavmeshQuery;
1147
1148impl DynamicPreparedNavmeshQuery {
1149 pub fn run<B, U>(
1159 state: &mut DynamicNavmeshState,
1160 updates: impl IntoIterator<Item = U>,
1161 query: NavmeshQuery,
1162 builder: &B,
1163 ) -> Result<DynamicPreparedNavmeshQueryResult<B::Map>, DynamicNavmeshError>
1164 where
1165 B: PreparedNavmeshBuilder,
1166 U: Borrow<DynamicNavmeshUpdate>,
1167 {
1168 let prepared_stale_before_updates = state.prepared_stale();
1169 let mut applied_update_count = 0;
1170 for update in updates {
1171 state.apply_update(update.borrow())?;
1172 applied_update_count += 1;
1173 }
1174 let prepared_stale_after_updates = state.prepared_stale();
1175
1176 let materialized_navmesh = state.materialize()?;
1177 let raw_result = materialized_navmesh.query(query);
1178 let prepared_navmesh = builder
1179 .preprocess(&materialized_navmesh)
1180 .map_err(|source| DynamicNavmeshError::PreparedRebuild { source })?;
1181 let rebuilt_prepared_result = prepared_navmesh.query(query);
1182 state.mark_prepared_rebuilt();
1183
1184 let metadata = DynamicPreparedNavmeshQueryMetadata {
1185 builder_name: builder.name(),
1186 applied_update_count,
1187 prepared_stale_before_updates,
1188 prepared_stale_after_updates,
1189 prepared_stale_after_rebuild: state.prepared_stale(),
1190 materialized_cell_count: materialized_navmesh.cells().len(),
1191 materialized_portal_count: materialized_navmesh.portals().len(),
1192 rebuild_status: DynamicPreparedNavmeshRebuildStatus::RebuiltFromMaterializedSnapshot,
1193 };
1194
1195 Ok(DynamicPreparedNavmeshQueryResult {
1196 materialized_navmesh,
1197 prepared_navmesh,
1198 raw_result,
1199 rebuilt_prepared_result,
1200 metadata,
1201 })
1202 }
1203}
1204
1205pub trait NavmeshPathfinder {
1213 fn name(&self) -> &'static str;
1215
1216 fn search(&self, navmesh: &Navmesh, query: NavmeshQuery) -> NavmeshSearchResult;
1229}
1230
1231fn polygon_edges(vertices: &[Point2]) -> impl Iterator<Item = (Point2, Point2)> + '_ {
1232 vertices
1233 .iter()
1234 .copied()
1235 .zip(vertices.iter().copied().cycle().skip(1))
1236 .take(vertices.len())
1237}
1238
1239fn sort_and_dedup_parameters(parameters: &mut Vec<f64>) {
1240 parameters.sort_by(f64::total_cmp);
1241 parameters.dedup_by(|left, right| (*left - *right).abs() <= EPSILON);
1242}
1243
1244fn interpolate_segment(start: Point2, end: Point2, parameter: f64) -> Point2 {
1245 Point2::new(
1246 start.x + ((end.x - start.x) * parameter),
1247 start.y + ((end.y - start.y) * parameter),
1248 )
1249}
1250
1251fn segment_intersection_parameters(
1252 a_start: Point2,
1253 a_end: Point2,
1254 b_start: Point2,
1255 b_end: Point2,
1256) -> Vec<f64> {
1257 let mut parameters = Vec::with_capacity(2);
1258 for point in [a_start, a_end, b_start, b_end] {
1259 if point_on_segment(point, a_start, a_end) && point_on_segment(point, b_start, b_end) {
1260 parameters.push(segment_parameter(point, a_start, a_end));
1261 }
1262 }
1263
1264 if !parameters.is_empty() {
1265 sort_and_dedup_parameters(&mut parameters);
1266 return parameters;
1267 }
1268
1269 if let Some(parameter) = proper_intersection_parameter(a_start, a_end, b_start, b_end) {
1270 parameters.push(parameter);
1271 }
1272
1273 parameters
1274}
1275
1276fn segment_parameter(point: Point2, start: Point2, end: Point2) -> f64 {
1277 let dx = end.x - start.x;
1278 let dy = end.y - start.y;
1279 if dx.abs() >= dy.abs() && dx.abs() > EPSILON {
1280 ((point.x - start.x) / dx).clamp(0.0, 1.0)
1281 } else if dy.abs() > EPSILON {
1282 ((point.y - start.y) / dy).clamp(0.0, 1.0)
1283 } else {
1284 0.0
1285 }
1286}
1287
1288fn proper_intersection_parameter(
1289 a_start: Point2,
1290 a_end: Point2,
1291 b_start: Point2,
1292 b_end: Point2,
1293) -> Option<f64> {
1294 let o1 = orientation(a_start, a_end, b_start);
1295 let o2 = orientation(a_start, a_end, b_end);
1296 let o3 = orientation(b_start, b_end, a_start);
1297 let o4 = orientation(b_start, b_end, a_end);
1298
1299 let properly_crosses = (o1 > EPSILON && o2 < -EPSILON || o1 < -EPSILON && o2 > EPSILON)
1300 && (o3 > EPSILON && o4 < -EPSILON || o3 < -EPSILON && o4 > EPSILON);
1301 if !properly_crosses {
1302 return None;
1303 }
1304
1305 let a_dx = a_end.x - a_start.x;
1306 let a_dy = a_end.y - a_start.y;
1307 let b_dx = b_end.x - b_start.x;
1308 let b_dy = b_end.y - b_start.y;
1309 let denominator = cross(a_dx, a_dy, b_dx, b_dy);
1310 if denominator.abs() <= EPSILON {
1311 return None;
1312 }
1313
1314 let offset_x = b_start.x - a_start.x;
1315 let offset_y = b_start.y - a_start.y;
1316 Some((cross(offset_x, offset_y, b_dx, b_dy) / denominator).clamp(0.0, 1.0))
1317}
1318
1319fn point_on_segment(point: Point2, start: Point2, end: Point2) -> bool {
1320 orientation(start, end, point).abs() <= EPSILON
1321 && point.x >= start.x.min(end.x) - EPSILON
1322 && point.x <= start.x.max(end.x) + EPSILON
1323 && point.y >= start.y.min(end.y) - EPSILON
1324 && point.y <= start.y.max(end.y) + EPSILON
1325}
1326
1327fn orientation(start: Point2, end: Point2, point: Point2) -> f64 {
1328 ((end.x - start.x) * (point.y - start.y)) - ((end.y - start.y) * (point.x - start.x))
1329}
1330
1331fn cross(left_x: f64, left_y: f64, right_x: f64, right_y: f64) -> f64 {
1332 (left_x * right_y) - (left_y * right_x)
1333}
1334
1335fn is_degenerate_polygon(vertices: &[Point2]) -> bool {
1336 signed_area(vertices).abs() <= EPSILON
1337}
1338
1339fn signed_area(vertices: &[Point2]) -> f64 {
1340 polygon_edges(vertices)
1341 .map(|(left, right)| (left.x * right.y) - (right.x * left.y))
1342 .sum::<f64>()
1343 / 2.0
1344}
1345
1346fn is_convex_polygon(vertices: &[Point2]) -> bool {
1347 let mut reference_sign = 0.0_f64;
1348 for index in 0..vertices.len() {
1349 let a = vertices[index];
1350 let b = vertices[(index + 1) % vertices.len()];
1351 let c = vertices[(index + 2) % vertices.len()];
1352 let turn = orientation(a, b, c);
1353 if turn.abs() <= EPSILON {
1354 continue;
1355 }
1356
1357 if reference_sign.abs() <= EPSILON {
1358 reference_sign = turn;
1359 continue;
1360 }
1361
1362 if turn.signum() != reference_sign.signum() {
1363 return false;
1364 }
1365 }
1366
1367 true
1368}
1369
1370pub(crate) fn points_equal(left: Point2, right: Point2) -> bool {
1372 (left.x - right.x).abs() <= EPSILON && (left.y - right.y).abs() <= EPSILON
1373}
1374
1375fn shares_any_cell(left: &[usize], right: &[usize]) -> bool {
1376 left.iter().any(|cell| right.contains(cell))
1377}
1378
1379#[cfg(test)]
1380mod tests {
1381 use super::{Navmesh, NavmeshCell, NavmeshPortal, NavmeshQuery, NavmeshQueryResult};
1382 use condor_core::Point2;
1383
1384 #[test]
1385 fn validates_and_queries_a_two_cell_mesh() {
1386 let navmesh = Navmesh::new(
1387 vec![
1388 NavmeshCell::new(
1389 "left",
1390 vec![
1391 Point2::new(0.0, 0.0),
1392 Point2::new(2.0, 0.0),
1393 Point2::new(2.0, 2.0),
1394 Point2::new(0.0, 2.0),
1395 ],
1396 ),
1397 NavmeshCell::new(
1398 "right",
1399 vec![
1400 Point2::new(2.0, 0.0),
1401 Point2::new(4.0, 0.0),
1402 Point2::new(4.0, 2.0),
1403 Point2::new(2.0, 2.0),
1404 ],
1405 ),
1406 ],
1407 vec![NavmeshPortal {
1408 left_cell: 0,
1409 right_cell: 1,
1410 start: Point2::new(2.0, 0.0),
1411 end: Point2::new(2.0, 2.0),
1412 }],
1413 );
1414 navmesh.validate().expect("valid mesh");
1415 assert!(matches!(
1416 navmesh.query(NavmeshQuery::new(
1417 Point2::new(1.0, 1.0),
1418 Point2::new(3.0, 1.0)
1419 )),
1420 NavmeshQueryResult::Connected { .. }
1421 ));
1422 }
1423}