1use condor_core::Point2;
17use serde::{Deserialize, Serialize};
18use std::{collections::VecDeque, error::Error, fmt};
19
20use crate::{
21 grid::{Cell, Grid, GridEditError},
22 point::Point,
23 search::{SearchOutcome, SearchRequest, SearchResult},
24};
25
26const INTERPOLATED_EPSILON: f64 = 1e-9;
27
28pub trait GridReplanner {
38 fn name(&self) -> &'static str;
40
41 fn initialize(&mut self, grid: &Grid, request: SearchRequest) -> SearchResult;
43
44 fn update_cell(&mut self, point: Point, cell: Cell);
46
47 fn update_cost(&mut self, point: Point, cost: usize) -> Result<(), GridEditError>;
54
55 fn replan(&mut self) -> SearchResult;
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "kebab-case")]
62pub enum InterpolatedTraversalCostModel {
63 CellLengthWeightedV0,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct InterpolatedSearchRequest {
70 pub start: Point2,
72 pub goal: Point2,
74}
75
76impl InterpolatedSearchRequest {
77 #[must_use]
79 pub const fn new(start: Point2, goal: Point2) -> Self {
80 Self { start, goal }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq)]
89pub struct InterpolatedPath {
90 points: Vec<Point2>,
91 cost: f64,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
96#[non_exhaustive]
97pub enum InterpolatedPathBuildError {
98 #[error("interpolated paths must contain at least one point")]
100 Empty,
101 #[error("interpolated path point {index} is not finite: {point:?}")]
103 NonFinitePoint { index: usize, point: Point2 },
104 #[error("interpolated path point {index} is outside the {width}x{height} grid: {point:?}")]
106 PointOutOfGrid {
107 index: usize,
108 point: Point2,
109 width: usize,
110 height: usize,
111 },
112 #[error("interpolated path point {index} is in blocked cell {cell:?}: {point:?}")]
114 PointNotWalkable {
115 index: usize,
116 point: Point2,
117 cell: Point,
118 },
119 #[error("interpolated path segment {index} is not walkable from {start:?} to {end:?}")]
121 SegmentNotWalkable {
122 index: usize,
123 start: Point2,
124 end: Point2,
125 },
126 #[error("interpolated path segment {index} produced a non-finite cost under {cost_model:?}")]
128 NonFiniteCost {
129 index: usize,
130 cost_model: InterpolatedTraversalCostModel,
131 },
132}
133
134impl InterpolatedPath {
135 pub fn from_points(points: Vec<Point2>, cost: f64) -> Result<Self, InterpolatedPathBuildError> {
144 if points.is_empty() {
145 return Err(InterpolatedPathBuildError::Empty);
146 }
147 Ok(Self { points, cost })
148 }
149
150 pub fn from_points_on_grid(
158 grid: &Grid,
159 points: Vec<Point2>,
160 cost_model: InterpolatedTraversalCostModel,
161 ) -> Result<Self, InterpolatedPathBuildError> {
162 if points.is_empty() {
163 return Err(InterpolatedPathBuildError::Empty);
164 }
165
166 for (index, &point) in points.iter().enumerate() {
167 if !point.x.is_finite() || !point.y.is_finite() {
168 return Err(InterpolatedPathBuildError::NonFinitePoint { index, point });
169 }
170 if point.x < 0.0
171 || point.y < 0.0
172 || point.x >= grid.width() as f64
173 || point.y >= grid.height() as f64
174 {
175 return Err(InterpolatedPathBuildError::PointOutOfGrid {
176 index,
177 point,
178 width: grid.width(),
179 height: grid.height(),
180 });
181 }
182
183 let cell = Point::new(point.x.floor() as usize, point.y.floor() as usize);
184 if !grid.is_walkable(cell) {
185 return Err(InterpolatedPathBuildError::PointNotWalkable { index, point, cell });
186 }
187 }
188
189 let mut cost = 0.0;
190 for (index, pair) in points.windows(2).enumerate() {
191 let segment_cost = interpolated_segment_cost(grid, pair[0], pair[1], cost_model)
192 .ok_or(InterpolatedPathBuildError::SegmentNotWalkable {
193 index,
194 start: pair[0],
195 end: pair[1],
196 })?;
197 if !segment_cost.is_finite() || !(cost + segment_cost).is_finite() {
198 return Err(InterpolatedPathBuildError::NonFiniteCost { index, cost_model });
199 }
200 cost += segment_cost;
201 }
202
203 Self::from_points(points, cost)
204 }
205
206 #[must_use]
208 pub fn points(&self) -> &[Point2] {
209 &self.points
210 }
211
212 #[must_use]
214 pub fn start(&self) -> Point2 {
215 self.points[0]
216 }
217
218 #[must_use]
220 pub fn goal(&self) -> Point2 {
221 self.points[self.points.len() - 1]
222 }
223
224 #[must_use]
226 pub const fn cost(&self) -> f64 {
227 self.cost
228 }
229}
230
231#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)]
233pub struct InterpolatedSearchStats {
234 pub visited_nodes: usize,
236}
237
238#[derive(Debug, Clone, PartialEq)]
243pub enum InterpolatedPathOutcome {
244 Found(InterpolatedPath),
246 PartialPath(InterpolatedPath),
248 Fallback(InterpolatedPath),
250}
251
252#[non_exhaustive]
254#[derive(Debug, Clone, Copy, PartialEq)]
255pub enum InterpolatedSearchError {
256 InvalidStart { point: Point2 },
258 InvalidGoal { point: Point2 },
260 NotInitialized,
262 PathBuild { source: InterpolatedPathBuildError },
264}
265
266impl fmt::Display for InterpolatedSearchError {
267 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
268 match self {
269 Self::InvalidStart { point } => {
270 write!(formatter, "invalid interpolated start: {point:?}")
271 }
272 Self::InvalidGoal { point } => {
273 write!(formatter, "invalid interpolated goal: {point:?}")
274 }
275 Self::NotInitialized => {
276 formatter.write_str("interpolated replanner is not initialized")
277 }
278 Self::PathBuild { source } => write!(formatter, "interpolated path build: {source}"),
279 }
280 }
281}
282
283impl Error for InterpolatedSearchError {
284 fn source(&self) -> Option<&(dyn Error + 'static)> {
285 match self {
286 Self::PathBuild { source } => Some(source),
287 Self::InvalidStart { .. } | Self::InvalidGoal { .. } | Self::NotInitialized => None,
288 }
289 }
290}
291
292impl From<InterpolatedPathBuildError> for InterpolatedSearchError {
293 fn from(source: InterpolatedPathBuildError) -> Self {
294 Self::PathBuild { source }
295 }
296}
297
298#[repr(transparent)]
304#[derive(Debug, Clone, PartialEq)]
305pub struct InterpolatedSearchOutcome(
306 SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats>,
307);
308pub type InterpolatedSearchResult = Result<InterpolatedSearchOutcome, InterpolatedSearchError>;
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum InterpolatedPathOutcomeKind {
314 Found,
316 PartialPath,
318 Fallback,
320}
321
322#[derive(Debug, Clone, Copy)]
327pub struct InterpolatedPathOutcomeRef<'a> {
328 kind: InterpolatedPathOutcomeKind,
329 path: &'a InterpolatedPath,
330 stats: &'a InterpolatedSearchStats,
331}
332
333impl<'a> InterpolatedPathOutcomeRef<'a> {
334 #[must_use]
336 pub const fn kind(&self) -> InterpolatedPathOutcomeKind {
337 self.kind
338 }
339
340 #[must_use]
345 pub fn expected_kind(&self) -> InterpolatedExpectedKind {
346 match self.kind {
347 InterpolatedPathOutcomeKind::Found => InterpolatedExpectedKind::Found,
348 InterpolatedPathOutcomeKind::PartialPath => InterpolatedExpectedKind::PartialPath,
349 InterpolatedPathOutcomeKind::Fallback => InterpolatedExpectedKind::Fallback,
350 }
351 }
352
353 #[must_use]
355 pub const fn path(&self) -> &'a InterpolatedPath {
356 self.path
357 }
358
359 #[must_use]
361 pub const fn stats(&self) -> &'a InterpolatedSearchStats {
362 self.stats
363 }
364
365 #[must_use]
370 pub fn derived_cost(
371 &self,
372 grid: &Grid,
373 cost_model: InterpolatedTraversalCostModel,
374 ) -> Option<f64> {
375 interpolated_path_cost(grid, self.path.points(), cost_model)
376 }
377
378 #[must_use]
380 pub fn witness_points(&self) -> usize {
381 self.path.points().len()
382 }
383}
384
385impl InterpolatedPathOutcome {
386 #[must_use]
388 pub const fn kind(&self) -> InterpolatedPathOutcomeKind {
389 match self {
390 Self::Found(_) => InterpolatedPathOutcomeKind::Found,
391 Self::PartialPath(_) => InterpolatedPathOutcomeKind::PartialPath,
392 Self::Fallback(_) => InterpolatedPathOutcomeKind::Fallback,
393 }
394 }
395
396 #[must_use]
398 pub const fn path(&self) -> &InterpolatedPath {
399 match self {
400 Self::Found(path) | Self::PartialPath(path) | Self::Fallback(path) => path,
401 }
402 }
403}
404
405pub(crate) fn interpolated_found(
407 path: InterpolatedPath,
408 visited_nodes: usize,
409) -> InterpolatedSearchResult {
410 Ok(InterpolatedSearchOutcome::found(
411 InterpolatedPathOutcome::Found(path),
412 InterpolatedSearchStats { visited_nodes },
413 ))
414}
415
416pub(crate) fn interpolated_partial(
418 path: InterpolatedPath,
419 visited_nodes: usize,
420) -> InterpolatedSearchResult {
421 Ok(InterpolatedSearchOutcome::found(
422 InterpolatedPathOutcome::PartialPath(path),
423 InterpolatedSearchStats { visited_nodes },
424 ))
425}
426
427pub(crate) fn interpolated_fallback(
429 path: InterpolatedPath,
430 visited_nodes: usize,
431) -> InterpolatedSearchResult {
432 Ok(InterpolatedSearchOutcome::found(
433 InterpolatedPathOutcome::Fallback(path),
434 InterpolatedSearchStats { visited_nodes },
435 ))
436}
437
438pub(crate) const fn interpolated_not_found(visited_nodes: usize) -> InterpolatedSearchResult {
440 Ok(InterpolatedSearchOutcome::no_path(
441 InterpolatedSearchStats { visited_nodes },
442 ))
443}
444
445impl InterpolatedSearchOutcome {
446 #[must_use]
448 pub const fn found(path: InterpolatedPathOutcome, stats: InterpolatedSearchStats) -> Self {
449 Self(SearchOutcome::found(path, stats))
450 }
451
452 #[must_use]
454 pub const fn no_path(stats: InterpolatedSearchStats) -> Self {
455 Self(SearchOutcome::no_path(stats))
456 }
457
458 #[must_use]
460 pub const fn as_search_outcome(
461 &self,
462 ) -> &SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats> {
463 &self.0
464 }
465
466 #[must_use]
468 pub fn into_search_outcome(
469 self,
470 ) -> SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats> {
471 self.0
472 }
473
474 #[must_use]
476 pub fn cost(&self) -> Option<f64> {
477 self.path().map(|outcome| outcome.path().cost())
478 }
479
480 #[must_use]
485 pub fn expected_kind(&self) -> InterpolatedExpectedKind {
486 match self.path() {
487 Some(path) => match path {
488 InterpolatedPathOutcome::Found(_) => InterpolatedExpectedKind::Found,
489 InterpolatedPathOutcome::PartialPath(_) => InterpolatedExpectedKind::PartialPath,
490 InterpolatedPathOutcome::Fallback(_) => InterpolatedExpectedKind::Fallback,
491 },
492 None => InterpolatedExpectedKind::NoPath,
493 }
494 }
495
496 #[must_use]
498 pub fn path_outcome(&self) -> Option<InterpolatedPathOutcomeRef<'_>> {
499 self.path().map(|path| InterpolatedPathOutcomeRef {
500 kind: path.kind(),
501 path: path.path(),
502 stats: self.stats(),
503 })
504 }
505}
506
507impl std::ops::Deref for InterpolatedSearchOutcome {
508 type Target = SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats>;
509
510 fn deref(&self) -> &Self::Target {
511 self.as_search_outcome()
512 }
513}
514
515impl From<SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats>>
516 for InterpolatedSearchOutcome
517{
518 fn from(outcome: SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats>) -> Self {
519 Self(outcome)
520 }
521}
522
523impl From<InterpolatedSearchOutcome>
524 for SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats>
525{
526 fn from(outcome: InterpolatedSearchOutcome) -> Self {
527 outcome.into_search_outcome()
528 }
529}
530
531pub trait InterpolatedGridReplanner {
542 fn name(&self) -> &'static str;
544
545 fn initialize(
547 &mut self,
548 grid: &Grid,
549 request: InterpolatedSearchRequest,
550 ) -> InterpolatedSearchResult;
551
552 fn update_cell(&mut self, point: Point, cell: Cell);
554
555 fn update_cost(&mut self, point: Point, cost: usize) -> Result<(), GridEditError>;
561
562 fn replan(&mut self) -> InterpolatedSearchResult;
564}
565
566pub trait InterpolatedMovingGoalReplanner: InterpolatedGridReplanner {
571 fn update_goal(&mut self, goal: Point2);
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub enum InterpolatedQueryResult {
580 Connected { start_cell: Point, goal_cell: Point },
582 NoPath { start_cell: Point, goal_cell: Point },
584 InvalidStart,
586 InvalidGoal,
588}
589
590#[must_use]
592pub fn query_interpolated_grid(
593 grid: &Grid,
594 request: InterpolatedSearchRequest,
595) -> InterpolatedQueryResult {
596 let Some(start_cell) = interpolated_point_to_cell(grid, request.start) else {
597 return InterpolatedQueryResult::InvalidStart;
598 };
599 let Some(goal_cell) = interpolated_point_to_cell(grid, request.goal) else {
600 return InterpolatedQueryResult::InvalidGoal;
601 };
602
603 if interpolated_cells_connected(grid, start_cell, goal_cell) {
604 InterpolatedQueryResult::Connected {
605 start_cell,
606 goal_cell,
607 }
608 } else {
609 InterpolatedQueryResult::NoPath {
610 start_cell,
611 goal_cell,
612 }
613 }
614}
615
616#[must_use]
618pub fn interpolated_path_cost(
619 grid: &Grid,
620 points: &[Point2],
621 cost_model: InterpolatedTraversalCostModel,
622) -> Option<f64> {
623 if points.is_empty() {
624 return None;
625 }
626 if points.len() == 1 {
627 return interpolated_point_to_cell(grid, points[0]).map(|_| 0.0);
628 }
629
630 points.windows(2).try_fold(0.0, |acc, pair| {
631 interpolated_segment_cost(grid, pair[0], pair[1], cost_model).map(|cost| acc + cost)
632 })
633}
634
635#[must_use]
637pub fn interpolated_segment_cost(
638 grid: &Grid,
639 start: Point2,
640 end: Point2,
641 cost_model: InterpolatedTraversalCostModel,
642) -> Option<f64> {
643 match cost_model {
644 InterpolatedTraversalCostModel::CellLengthWeightedV0 => {
645 interpolated_segment_cost_cell_length_weighted_v0(grid, start, end)
646 }
647 }
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
655#[serde(rename_all = "kebab-case")]
656pub enum InterpolatedExpectedKind {
657 Found,
659 PartialPath,
661 Fallback,
663 NoPath,
665 InvalidStart,
667 InvalidGoal,
669}
670
671pub fn best_partial_interpolated_path(
682 grid: &Grid,
683 request: InterpolatedSearchRequest,
684 cost_model: InterpolatedTraversalCostModel,
685) -> Result<Option<InterpolatedPath>, InterpolatedSearchError> {
686 let Some(start_cell) = interpolated_point_to_cell(grid, request.start) else {
687 return Err(InterpolatedSearchError::InvalidStart {
688 point: request.start,
689 });
690 };
691 let Some(goal_cell) = interpolated_point_to_cell(grid, request.goal) else {
692 return Err(InterpolatedSearchError::InvalidGoal {
693 point: request.goal,
694 });
695 };
696 if interpolated_cells_connected(grid, start_cell, goal_cell) {
697 return Ok(None);
698 }
699
700 let (parents, distances) = reachable_interpolated_cells(grid, start_cell);
701 let Some(target) = select_partial_target(grid, request, &distances) else {
702 return Ok(None);
703 };
704 let Some(cell_path) = reconstruct_cell_path(grid, start_cell, target, &parents) else {
705 return Ok(None);
706 };
707
708 let mut points = vec![request.start];
709 for cell in cell_path.into_iter().skip(1) {
710 let center = interpolated_cell_center(cell);
711 if same_interpolated_point(points[points.len() - 1], center) {
712 continue;
713 }
714 points.push(center);
715 }
716
717 InterpolatedPath::from_points_on_grid(grid, points, cost_model)
718 .map(Some)
719 .map_err(Into::into)
720}
721
722pub fn best_fallback_interpolated_path(
729 grid: &Grid,
730 request: InterpolatedSearchRequest,
731 cost_model: InterpolatedTraversalCostModel,
732) -> Result<Option<InterpolatedPath>, InterpolatedSearchError> {
733 let Some(path) = best_partial_interpolated_path(grid, request, cost_model)? else {
734 return Ok(None);
735 };
736 Ok((path.points().len() == 1).then_some(path))
737}
738
739fn interpolated_segment_cost_cell_length_weighted_v0(
740 grid: &Grid,
741 start: Point2,
742 end: Point2,
743) -> Option<f64> {
744 interpolated_point_to_cell(grid, start)?;
745 interpolated_point_to_cell(grid, end)?;
746
747 let total_length = start.distance_to(end);
748 if total_length <= INTERPOLATED_EPSILON {
749 return Some(0.0);
750 }
751
752 let dx = end.x - start.x;
753 let dy = end.y - start.y;
754 let mut parameters = vec![0.0, 1.0];
755
756 if dx.abs() > INTERPOLATED_EPSILON {
757 for boundary in 1..grid.width() {
758 let boundary = boundary as f64;
759 let t = (boundary - start.x) / dx;
760 if t > INTERPOLATED_EPSILON && t < 1.0 - INTERPOLATED_EPSILON {
761 parameters.push(t);
762 }
763 }
764 }
765
766 if dy.abs() > INTERPOLATED_EPSILON {
767 for boundary in 1..grid.height() {
768 let boundary = boundary as f64;
769 let t = (boundary - start.y) / dy;
770 if t > INTERPOLATED_EPSILON && t < 1.0 - INTERPOLATED_EPSILON {
771 parameters.push(t);
772 }
773 }
774 }
775
776 parameters.sort_by(|left, right| left.total_cmp(right));
777 parameters.dedup_by(|left, right| (*left - *right).abs() <= INTERPOLATED_EPSILON);
778
779 let mut cost = 0.0;
780 for interval in parameters.windows(2) {
781 let start_t = interval[0];
782 let end_t = interval[1];
783 if end_t - start_t <= INTERPOLATED_EPSILON {
784 continue;
785 }
786
787 let midpoint = interpolate_segment(start, end, (start_t + end_t) / 2.0);
788 let cell = interpolated_point_to_cell(grid, midpoint)?;
789 let traversal_cost = grid.traversal_cost(cell)? as f64;
790 cost += total_length * (end_t - start_t) * traversal_cost;
791 }
792
793 Some(cost)
794}
795
796fn interpolated_point_to_cell(grid: &Grid, point: Point2) -> Option<Point> {
797 if !point.x.is_finite() || !point.y.is_finite() {
798 return None;
799 }
800 if point.x < 0.0
801 || point.y < 0.0
802 || point.x >= grid.width() as f64
803 || point.y >= grid.height() as f64
804 {
805 return None;
806 }
807
808 let cell = Point::new(point.x.floor() as usize, point.y.floor() as usize);
809 grid.is_walkable(cell).then_some(cell)
810}
811
812fn reachable_interpolated_cells(
813 grid: &Grid,
814 start: Point,
815) -> (Vec<Option<Point>>, Vec<Option<usize>>) {
816 let mut parents = vec![None; grid.cell_count()];
817 let mut distances = vec![None; grid.cell_count()];
818 let mut queue = VecDeque::new();
819
820 let start_index = grid
821 .index_of(start)
822 .expect("start point should index into the grid");
823 distances[start_index] = Some(0);
824 queue.push_back(start);
825
826 while let Some(point) = queue.pop_front() {
827 let point_index = grid
828 .index_of(point)
829 .expect("reachable point should index into the grid");
830 let distance = distances[point_index].expect("reachable cells carry distance");
831 for neighbor in grid.neighbors4(point) {
832 if !grid.is_walkable(neighbor) {
833 continue;
834 }
835 let neighbor_index = grid
836 .index_of(neighbor)
837 .expect("neighbor should index into the grid");
838 if distances[neighbor_index].is_some() {
839 continue;
840 }
841
842 parents[neighbor_index] = Some(point);
843 distances[neighbor_index] = Some(distance + 1);
844 queue.push_back(neighbor);
845 }
846 }
847
848 (parents, distances)
849}
850
851fn select_partial_target(
852 grid: &Grid,
853 request: InterpolatedSearchRequest,
854 distances: &[Option<usize>],
855) -> Option<Point> {
856 let mut best: Option<(Point, f64, usize)> = None;
857
858 for (index, distance) in distances
859 .iter()
860 .copied()
861 .enumerate()
862 .take(grid.cell_count())
863 {
864 let Some(distance) = distance else {
865 continue;
866 };
867 let point = grid.point_from_index(index);
868 if !grid.is_walkable(point) {
869 continue;
870 }
871
872 let goal_distance = interpolated_cell_center(point).distance_to(request.goal);
873 match best {
874 None => best = Some((point, goal_distance, distance)),
875 Some((current_point, current_goal_distance, current_steps)) => {
876 if goal_distance + INTERPOLATED_EPSILON < current_goal_distance
877 || ((goal_distance - current_goal_distance).abs() <= INTERPOLATED_EPSILON
878 && (distance < current_steps
879 || (distance == current_steps
880 && (point.y < current_point.y
881 || (point.y == current_point.y && point.x < current_point.x)))))
882 {
883 best = Some((point, goal_distance, distance));
884 }
885 }
886 }
887 }
888
889 best.map(|(point, _, _)| point)
890}
891
892fn reconstruct_cell_path(
893 grid: &Grid,
894 start: Point,
895 target: Point,
896 parents: &[Option<Point>],
897) -> Option<Vec<Point>> {
898 let mut cursor = target;
899 let mut path = vec![cursor];
900
901 while cursor != start {
902 let cursor_index = grid
903 .index_of(cursor)
904 .expect("path cursor should index into the grid");
905 let parent = parents[cursor_index]?;
906 cursor = parent;
907 path.push(cursor);
908 }
909
910 path.reverse();
911 Some(path)
912}
913
914fn interpolated_cell_center(point: Point) -> Point2 {
915 Point2::new(point.x as f64 + 0.5, point.y as f64 + 0.5)
916}
917
918fn same_interpolated_point(left: Point2, right: Point2) -> bool {
919 (left.x - right.x).abs() <= INTERPOLATED_EPSILON
920 && (left.y - right.y).abs() <= INTERPOLATED_EPSILON
921}
922
923fn interpolated_cells_connected(grid: &Grid, start: Point, goal: Point) -> bool {
924 if start == goal {
925 return true;
926 }
927
928 let mut seen = vec![false; grid.cell_count()];
929 let mut frontier = std::collections::VecDeque::from([start]);
930 let start_index = (start.y * grid.width()) + start.x;
931 seen[start_index] = true;
932
933 while let Some(cell) = frontier.pop_front() {
934 if cell == goal {
935 return true;
936 }
937
938 for neighbor in grid.neighbors4(cell) {
939 let index = (neighbor.y * grid.width()) + neighbor.x;
940 if seen[index] {
941 continue;
942 }
943 seen[index] = true;
944 frontier.push_back(neighbor);
945 }
946 }
947
948 false
949}
950
951fn interpolate_segment(start: Point2, end: Point2, t: f64) -> Point2 {
952 Point2::new(
953 start.x + ((end.x - start.x) * t),
954 start.y + ((end.y - start.y) * t),
955 )
956}
957
958#[cfg(test)]
959mod tests {
960 use super::*;
961 use crate::{
962 Cell, InterpolatedGridReplanner, InterpolatedMovingGoalReplanner, SearchOutcome,
963 algorithms::field_d_star::FieldDStar, best_partial_interpolated_path,
964 interpolated_path_cost,
965 };
966
967 const EPSILON: f64 = 1e-9;
968
969 #[test]
970 fn on_grid_path_build_reports_precise_validation_variants() {
971 let mut grid = Grid::new(3, 1).expect("grid dimensions are valid");
972 let cost_model = InterpolatedTraversalCostModel::CellLengthWeightedV0;
973
974 assert_eq!(
975 InterpolatedPath::from_points_on_grid(&grid, Vec::new(), cost_model),
976 Err(InterpolatedPathBuildError::Empty)
977 );
978
979 let non_finite = Point2::new(f64::NAN, 0.5);
980 assert!(matches!(
981 InterpolatedPath::from_points_on_grid(&grid, vec![non_finite], cost_model),
982 Err(InterpolatedPathBuildError::NonFinitePoint { index: 0, point })
983 if point.x.is_nan() && point.y == 0.5
984 ));
985
986 let outside = Point2::new(3.0, 0.5);
987 assert_eq!(
988 InterpolatedPath::from_points_on_grid(&grid, vec![outside], cost_model),
989 Err(InterpolatedPathBuildError::PointOutOfGrid {
990 index: 0,
991 point: outside,
992 width: 3,
993 height: 1,
994 })
995 );
996
997 grid.set_cell(Point::new(1, 0), Cell::Blocked)
998 .expect("blocked test cell should be valid");
999 let blocked = Point2::new(1.5, 0.5);
1000 assert_eq!(
1001 InterpolatedPath::from_points_on_grid(&grid, vec![blocked], cost_model),
1002 Err(InterpolatedPathBuildError::PointNotWalkable {
1003 index: 0,
1004 point: blocked,
1005 cell: Point::new(1, 0),
1006 })
1007 );
1008
1009 let start = Point2::new(0.5, 0.5);
1010 let end = Point2::new(2.5, 0.5);
1011 assert_eq!(
1012 InterpolatedPath::from_points_on_grid(&grid, vec![start, end], cost_model),
1013 Err(InterpolatedPathBuildError::SegmentNotWalkable {
1014 index: 0,
1015 start,
1016 end,
1017 })
1018 );
1019 }
1020
1021 #[test]
1022 fn partial_path_helper_distinguishes_not_applicable_from_a_path() {
1023 let mut grid = Grid::new(3, 1).expect("grid dimensions are valid");
1024 let request = InterpolatedSearchRequest::new(Point2::new(0.5, 0.5), Point2::new(2.5, 0.5));
1025 let cost_model = InterpolatedTraversalCostModel::CellLengthWeightedV0;
1026
1027 let connected = best_partial_interpolated_path(&grid, request, cost_model)
1028 .expect("valid partial-path construction");
1029 assert!(connected.is_none(), "connected requests are not applicable");
1030
1031 grid.set_cell(Point::new(1, 0), Cell::Blocked)
1032 .expect("barrier point should be valid");
1033 let partial = best_partial_interpolated_path(&grid, request, cost_model)
1034 .expect("valid partial-path construction")
1035 .expect("disconnected request should produce a partial path");
1036 assert_eq!(partial.points(), &[request.start]);
1037 }
1038
1039 #[test]
1040 fn shared_result_surface_classifies_all_interpolated_outcomes() {
1041 let grid = Grid::new(2, 1).expect("grid dimensions are valid");
1042 let cost_model = InterpolatedTraversalCostModel::CellLengthWeightedV0;
1043 let path = InterpolatedPath::from_points_on_grid(
1044 &grid,
1045 vec![Point2::new(0.5, 0.5), Point2::new(1.5, 0.5)],
1046 cost_model,
1047 )
1048 .expect("test path should build");
1049
1050 for (result, expected_kind, expected_outcome_kind, expected_visits) in [
1051 (
1052 InterpolatedSearchOutcome::found(
1053 InterpolatedPathOutcome::Found(path.clone()),
1054 InterpolatedSearchStats { visited_nodes: 3 },
1055 ),
1056 InterpolatedExpectedKind::Found,
1057 Some(InterpolatedPathOutcomeKind::Found),
1058 3,
1059 ),
1060 (
1061 InterpolatedSearchOutcome::found(
1062 InterpolatedPathOutcome::PartialPath(path.clone()),
1063 InterpolatedSearchStats { visited_nodes: 5 },
1064 ),
1065 InterpolatedExpectedKind::PartialPath,
1066 Some(InterpolatedPathOutcomeKind::PartialPath),
1067 5,
1068 ),
1069 (
1070 InterpolatedSearchOutcome::found(
1071 InterpolatedPathOutcome::Fallback(path.clone()),
1072 InterpolatedSearchStats { visited_nodes: 7 },
1073 ),
1074 InterpolatedExpectedKind::Fallback,
1075 Some(InterpolatedPathOutcomeKind::Fallback),
1076 7,
1077 ),
1078 (
1079 InterpolatedSearchOutcome::no_path(InterpolatedSearchStats { visited_nodes: 11 }),
1080 InterpolatedExpectedKind::NoPath,
1081 None,
1082 11,
1083 ),
1084 ] {
1085 assert_eq!(result.is_found(), expected_outcome_kind.is_some());
1086 assert_eq!(result.path().is_some(), expected_outcome_kind.is_some());
1087 assert_eq!(result.stats().visited_nodes, expected_visits);
1088 assert_eq!(result.expected_kind(), expected_kind);
1089 assert_eq!(result.cost(), expected_outcome_kind.map(|_| path.cost()));
1090 let outcome = result.path_outcome();
1091 assert_eq!(outcome.map(|outcome| outcome.kind()), expected_outcome_kind);
1092
1093 if let Some(outcome) = outcome {
1094 assert_eq!(outcome.expected_kind(), expected_kind);
1095 assert_eq!(outcome.path().points(), path.points());
1096 assert_eq!(outcome.path().cost(), path.cost());
1097 assert_eq!(outcome.witness_points(), path.points().len());
1098 let derived_cost = outcome
1099 .derived_cost(&grid, cost_model)
1100 .expect("path-bearing outcome should derive a cost");
1101 assert!((derived_cost - path.cost()).abs() <= EPSILON);
1102 }
1103
1104 let shared: SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats> =
1105 result.clone().into();
1106 let wrapped = InterpolatedSearchOutcome::from(shared.clone());
1107 assert_eq!(wrapped.as_search_outcome(), &shared);
1108 let round_trip: SearchOutcome<InterpolatedPathOutcome, InterpolatedSearchStats> =
1109 wrapped.into();
1110 assert_eq!(round_trip, shared);
1111 }
1112
1113 assert!(matches!(
1114 InterpolatedSearchError::InvalidStart {
1115 point: Point2::new(-1.0, 0.5),
1116 },
1117 InterpolatedSearchError::InvalidStart { .. }
1118 ));
1119 assert!(matches!(
1120 InterpolatedSearchError::InvalidGoal {
1121 point: Point2::new(2.5, 0.5),
1122 },
1123 InterpolatedSearchError::InvalidGoal { .. }
1124 ));
1125 }
1126
1127 #[test]
1128 fn shared_result_surface_reuses_fixed_and_moving_goal_field_d_star_outputs() {
1129 let mut partial_grid = Grid::new(4, 1).expect("grid dimensions are valid");
1130 let request = InterpolatedSearchRequest::new(Point2::new(0.5, 0.5), Point2::new(3.5, 0.5));
1131 let mut replanner = FieldDStar::new();
1132
1133 let initial = replanner
1134 .initialize(&partial_grid, request)
1135 .expect("test request should be valid");
1136 let initial_outcome = initial
1137 .path_outcome()
1138 .expect("initial result should expose a path outcome");
1139 assert_eq!(initial.expected_kind(), InterpolatedExpectedKind::Found);
1140 assert_eq!(initial_outcome.kind(), InterpolatedPathOutcomeKind::Found);
1141 assert_eq!(
1142 initial_outcome.derived_cost(
1143 &partial_grid,
1144 InterpolatedTraversalCostModel::CellLengthWeightedV0
1145 ),
1146 Some(initial_outcome.path().cost())
1147 );
1148
1149 let partial_barrier = Point::new(2, 0);
1150 partial_grid
1151 .set_cell(partial_barrier, Cell::Blocked)
1152 .expect("partial barrier should be valid");
1153 replanner.update_cell(partial_barrier, Cell::Blocked);
1154 let partial = replanner.replan().expect("test request should be valid");
1155 let partial_outcome = partial
1156 .path_outcome()
1157 .expect("partial-path result should expose a path outcome");
1158 assert_eq!(
1159 partial.expected_kind(),
1160 InterpolatedExpectedKind::PartialPath
1161 );
1162 assert_eq!(
1163 partial_outcome.kind(),
1164 InterpolatedPathOutcomeKind::PartialPath
1165 );
1166 let derived_partial_cost = partial_outcome
1167 .derived_cost(
1168 &partial_grid,
1169 InterpolatedTraversalCostModel::CellLengthWeightedV0,
1170 )
1171 .expect("partial-path result should derive a cost");
1172 let explicit_partial_cost = interpolated_path_cost(
1173 &partial_grid,
1174 partial_outcome.path().points(),
1175 InterpolatedTraversalCostModel::CellLengthWeightedV0,
1176 )
1177 .expect("partial-path result should stay valid on the grid");
1178 assert!((derived_partial_cost - explicit_partial_cost).abs() <= EPSILON);
1179
1180 replanner.update_goal(Point2::new(4.5, 0.5));
1181 assert_eq!(
1182 replanner.replan(),
1183 Err(InterpolatedSearchError::InvalidGoal {
1184 point: Point2::new(4.5, 0.5),
1185 })
1186 );
1187
1188 let mut fallback_grid = Grid::new(3, 1).expect("grid dimensions are valid");
1189 let fallback_request =
1190 InterpolatedSearchRequest::new(Point2::new(0.5, 0.5), Point2::new(2.5, 0.5));
1191 replanner
1192 .initialize(&fallback_grid, fallback_request)
1193 .expect("test request should be valid");
1194 let fallback_barrier = Point::new(1, 0);
1195 fallback_grid
1196 .set_cell(fallback_barrier, Cell::Blocked)
1197 .expect("fallback barrier should be valid");
1198 replanner.update_cell(fallback_barrier, Cell::Blocked);
1199 let fallback = replanner.replan().expect("test request should be valid");
1200 let fallback_outcome = fallback
1201 .path_outcome()
1202 .expect("fallback result should expose a path outcome");
1203 assert_eq!(fallback.expected_kind(), InterpolatedExpectedKind::Fallback);
1204 assert_eq!(
1205 fallback_outcome.kind(),
1206 InterpolatedPathOutcomeKind::Fallback
1207 );
1208 assert_eq!(fallback_outcome.witness_points(), 1);
1209 }
1210}