condor_navmesh/algorithms/
ta_star.rs1use crate::navmesh::points_equal;
20use crate::{
21 Navmesh, NavmeshPathfinder, NavmeshQuery, NavmeshQueryResult, NavmeshSearchResult, Point2,
22 PolygonPath,
23};
24
25use super::channel_search::{portal_midpoint, search_cell_corridor};
26
27const EPSILON: f64 = 1e-9;
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub struct TAStar;
35
36impl NavmeshPathfinder for TAStar {
37 fn name(&self) -> &'static str {
38 "ta-star"
39 }
40
41 fn search(&self, navmesh: &Navmesh, query: NavmeshQuery) -> NavmeshSearchResult {
42 let (start_cell, goal_cell) = match navmesh.query(query) {
43 NavmeshQueryResult::Connected {
44 start_cell,
45 goal_cell,
46 } => (start_cell, goal_cell),
47 NavmeshQueryResult::InvalidStart => {
48 return Err(crate::NavmeshSearchError::InvalidStart { point: query.start });
49 }
50 NavmeshQueryResult::InvalidGoal => {
51 return Err(crate::NavmeshSearchError::InvalidGoal { point: query.goal });
52 }
53 NavmeshQueryResult::NoPath { .. } => return crate::navmesh::search_not_found(0),
54 };
55
56 if points_equal(query.start, query.goal) {
57 return crate::navmesh::search_found(
58 PolygonPath::from_points(vec![query.start])
59 .expect("polygon path contains at least one point"),
60 1,
61 );
62 }
63
64 let (Some(cells), visited_nodes) =
65 search_cell_corridor(navmesh, start_cell, goal_cell, query.budget)?
66 else {
67 return crate::navmesh::search_not_found(0);
68 };
69
70 let Some(corridor) = crate::navmesh::corridor::NavmeshCorridor::from_cells(
71 navmesh,
72 query.start,
73 query.goal,
74 &cells,
75 ) else {
76 return crate::navmesh::search_not_found(visited_nodes);
77 };
78
79 let midpoint_seed = corridor
80 .portals
81 .iter()
82 .map(portal_midpoint)
83 .collect::<Vec<_>>();
84 let baseline_points =
85 crate::navmesh::funnel::pull_string(navmesh, &corridor, midpoint_seed.clone());
86 if baseline_points.len() >= 2 && !navmesh.path_is_walkable(&baseline_points) {
87 return crate::navmesh::search_not_found(visited_nodes);
88 }
89
90 let refined_seed = refine_query_locally(navmesh, &corridor, midpoint_seed);
91 let refined_points = crate::navmesh::funnel::pull_string(navmesh, &corridor, refined_seed);
92
93 let chosen_points = if refined_points.len() >= 2
94 && navmesh.path_is_walkable(&refined_points)
95 && path_cost(&refined_points) + EPSILON < path_cost(&baseline_points)
96 {
97 refined_points
98 } else {
99 baseline_points
100 };
101
102 crate::navmesh::search_found(
103 PolygonPath::from_points(chosen_points)
104 .expect("polygon path contains at least one point"),
105 visited_nodes,
106 )
107 }
108}
109
110fn refine_query_locally(
111 navmesh: &Navmesh,
112 corridor: &crate::navmesh::corridor::NavmeshCorridor,
113 seed_points: Vec<Point2>,
114) -> Vec<Point2> {
115 if seed_points.is_empty() {
116 return seed_points;
117 }
118
119 let mut refined = seed_points;
120 let max_passes = corridor.portals.len().max(1);
121
122 for _ in 0..max_passes {
123 let mut improved = false;
124
125 for index in 0..corridor.portals.len() {
126 let portal = corridor.portals[index];
127 let prev = if index == 0 {
128 corridor.start
129 } else {
130 refined[index - 1]
131 };
132 let next = if index + 1 == refined.len() {
133 corridor.goal
134 } else {
135 refined[index + 1]
136 };
137
138 let current = refined[index];
139 let current_cost = local_turn_cost(prev, current, next);
140
141 let mut best_point = current;
142 let mut best_cost = current_cost;
143 for candidate in [portal.start, portal.end] {
144 if !navmesh.segment_is_walkable(prev, candidate)
145 || !navmesh.segment_is_walkable(candidate, next)
146 {
147 continue;
148 }
149
150 let candidate_cost = local_turn_cost(prev, candidate, next);
151 if candidate_cost + EPSILON < best_cost {
152 best_point = candidate;
153 best_cost = candidate_cost;
154 }
155 }
156
157 if !points_equal(best_point, current) {
158 refined[index] = best_point;
159 improved = true;
160 }
161 }
162
163 if !improved {
164 break;
165 }
166 }
167
168 refined
169}
170
171fn local_turn_cost(prev: Point2, current: Point2, next: Point2) -> f64 {
172 segment_cost(prev, current) + segment_cost(current, next)
173}
174
175fn path_cost(points: &[Point2]) -> f64 {
176 points
177 .windows(2)
178 .map(|segment| segment_cost(segment[0], segment[1]))
179 .sum()
180}
181
182fn segment_cost(a: Point2, b: Point2) -> f64 {
183 ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
184}