1use std::cmp::Ordering;
10use std::collections::BinaryHeap;
11
12use crate::{
13 Grid, Point,
14 any_angle::geometry::canonicalize_grid_vertex,
15 any_angle::{
16 AnyAnglePath, AnyAnglePathfinder, AnyAngleSearchRequest, AnyAngleSearchResult,
17 has_line_of_sight,
18 },
19};
20use condor_core::Point2;
21
22const EPSILON: f64 = 1e-9;
23
24#[derive(Debug, Clone, Copy, Default)]
30pub struct LazyThetaStar;
31
32impl AnyAnglePathfinder for LazyThetaStar {
33 fn name(&self) -> &'static str {
34 "lazy-theta-star"
35 }
36
37 fn search(&self, grid: &Grid, request: AnyAngleSearchRequest) -> AnyAngleSearchResult {
38 let Some(start) = canonicalize_grid_vertex(request.start) else {
39 return Err(crate::AnyAngleSearchError::InvalidStart {
40 point: request.start,
41 });
42 };
43 let Some(goal) = canonicalize_grid_vertex(request.goal) else {
44 return Err(crate::AnyAngleSearchError::InvalidGoal {
45 point: request.goal,
46 });
47 };
48 let Some(start_v) = Vertex::from_point2(start) else {
49 return Err(crate::AnyAngleSearchError::InvalidStart { point: start });
50 };
51 let Some(goal_v) = Vertex::from_point2(goal) else {
52 return Err(crate::AnyAngleSearchError::InvalidGoal { point: goal });
53 };
54
55 if !is_vertex_valid(grid, start_v) {
56 return Err(crate::AnyAngleSearchError::InvalidStart { point: start });
57 }
58 if !is_vertex_valid(grid, goal_v) {
59 return Err(crate::AnyAngleSearchError::InvalidGoal { point: goal });
60 }
61
62 if start_v == goal_v {
63 return crate::any_angle::found(
64 AnyAnglePath::from_points(vec![start, goal])
65 .expect("any-angle path contains at least one point"),
66 1,
67 );
68 }
69
70 let width = grid.width() + 1;
71 let height = grid.height() + 1;
72 let mut g_costs = vec![f64::INFINITY; width * height];
73 let mut parents = vec![None; width * height];
74 let mut closed = vec![false; width * height];
75 let mut visited_nodes = 0;
76 let watch = crate::search::BudgetWatch::start(request.budget);
77
78 let start_idx = vertex_index(start_v, width);
79 let goal_idx = vertex_index(goal_v, width);
80
81 g_costs[start_idx] = 0.0;
82 parents[start_idx] = Some(start_idx);
83
84 let mut frontier = BinaryHeap::new();
85 frontier.push(FrontierEntry {
86 vertex: start_v,
87 f_cost: start.distance_to(goal),
88 });
89
90 while let Some(current_entry) = frontier.pop() {
91 let current_v = current_entry.vertex;
92 let current_idx = vertex_index(current_v, width);
93
94 if closed[current_idx] {
95 continue;
96 }
97
98 if current_entry.f_cost
99 > g_costs[current_idx] + current_v.to_point2().distance_to(goal) + EPSILON
100 {
101 continue;
102 }
103
104 set_vertex(
105 grid,
106 current_v,
107 current_idx,
108 width,
109 &mut g_costs,
110 &mut parents,
111 &closed,
112 );
113 if g_costs[current_idx].is_infinite() {
114 continue;
115 }
116
117 visited_nodes += 1;
118
119 if current_v == goal_v {
120 break;
121 }
122
123 if let Err(reason) = watch.check(visited_nodes) {
124 return Err(crate::any_angle::budget_error(reason));
125 }
126
127 closed[current_idx] = true;
128
129 for neighbor_v in neighbors(grid, current_v) {
130 let neighbor_idx = vertex_index(neighbor_v, width);
131 if closed[neighbor_idx] {
132 continue;
133 }
134
135 let current_parent_idx = parents[current_idx].unwrap_or(current_idx);
136 let current_parent_v = vertex_from_index(current_parent_idx, width);
137
138 let shortcut_g = g_costs[current_parent_idx]
139 + current_parent_v
140 .to_point2()
141 .distance_to(neighbor_v.to_point2());
142 let edge_g = g_costs[current_idx]
143 + current_v.to_point2().distance_to(neighbor_v.to_point2());
144
145 let (candidate_g, candidate_parent_idx) = if shortcut_g < edge_g {
146 (shortcut_g, current_parent_idx)
147 } else {
148 (edge_g, current_idx)
149 };
150
151 if candidate_g + EPSILON < g_costs[neighbor_idx] {
152 g_costs[neighbor_idx] = candidate_g;
153 parents[neighbor_idx] = Some(candidate_parent_idx);
154 frontier.push(FrontierEntry {
155 vertex: neighbor_v,
156 f_cost: candidate_g + neighbor_v.to_point2().distance_to(goal),
157 });
158 }
159 }
160 }
161
162 if g_costs[goal_idx] == f64::INFINITY {
163 return crate::any_angle::not_found(visited_nodes);
164 }
165
166 let mut points = vec![goal];
167 let mut current_idx = goal_idx;
168 while current_idx != start_idx {
169 let next_idx = parents[current_idx].unwrap();
170 if next_idx == current_idx {
171 break;
172 }
173 let point = vertex_from_index(next_idx, width).to_point2();
174 if points
175 .last()
176 .is_some_and(|last| point.distance_to(*last) > EPSILON)
177 {
178 points.push(point);
179 }
180 current_idx = next_idx;
181 }
182 if points
183 .last()
184 .is_some_and(|last| start.distance_to(*last) > EPSILON)
185 {
186 points.push(start);
187 }
188 points.reverse();
189
190 crate::any_angle::found(
191 AnyAnglePath::from_points(points).expect("any-angle path contains at least one point"),
192 visited_nodes,
193 )
194 }
195}
196
197fn set_vertex(
198 grid: &Grid,
199 current_v: Vertex,
200 current_idx: usize,
201 width: usize,
202 g_costs: &mut [f64],
203 parents: &mut [Option<usize>],
204 closed: &[bool],
205) {
206 let Some(parent_idx) = parents[current_idx] else {
207 return;
208 };
209 if parent_idx == current_idx {
210 return;
211 }
212
213 let parent_v = vertex_from_index(parent_idx, width);
214 if has_line_of_sight(grid, parent_v.to_point2(), current_v.to_point2()) {
215 return;
216 }
217
218 let mut best_parent = None;
219 let mut best_g = f64::INFINITY;
220 for neighbor_v in neighbors(grid, current_v) {
221 let neighbor_idx = vertex_index(neighbor_v, width);
222 if !closed[neighbor_idx] {
223 continue;
224 }
225
226 let candidate_g =
227 g_costs[neighbor_idx] + neighbor_v.to_point2().distance_to(current_v.to_point2());
228 if candidate_g < best_g {
229 best_g = candidate_g;
230 best_parent = Some(neighbor_idx);
231 }
232 }
233
234 if let Some(best_parent_idx) = best_parent {
235 g_costs[current_idx] = best_g;
236 parents[current_idx] = Some(best_parent_idx);
237 } else {
238 g_costs[current_idx] = f64::INFINITY;
239 parents[current_idx] = None;
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244struct Vertex {
245 x: usize,
246 y: usize,
247}
248
249impl Vertex {
250 fn from_point2(point: Point2) -> Option<Self> {
251 if point.x < 0.0
252 || point.y < 0.0
253 || !is_grid_vertex_coordinate(point.x)
254 || !is_grid_vertex_coordinate(point.y)
255 {
256 return None;
257 }
258
259 Some(Self {
260 x: point.x.round() as usize,
261 y: point.y.round() as usize,
262 })
263 }
264
265 fn to_point2(self) -> Point2 {
266 Point2::new(self.x as f64, self.y as f64)
267 }
268}
269
270fn is_grid_vertex_coordinate(value: f64) -> bool {
271 (value - value.round()).abs() <= EPSILON
272}
273
274fn vertex_index(vertex: Vertex, width: usize) -> usize {
275 vertex.y * width + vertex.x
276}
277
278fn vertex_from_index(index: usize, width: usize) -> Vertex {
279 Vertex {
280 x: index % width,
281 y: index / width,
282 }
283}
284
285fn is_vertex_valid(grid: &Grid, vertex: Vertex) -> bool {
286 vertex.x <= grid.width() && vertex.y <= grid.height()
287}
288
289fn neighbors(grid: &Grid, vertex: Vertex) -> Vec<Vertex> {
290 let mut neighbors = Vec::with_capacity(8);
291 let x = vertex.x as i64;
292 let y = vertex.y as i64;
293
294 for dx in -1..=1 {
295 for dy in -1..=1 {
296 if dx == 0 && dy == 0 {
297 continue;
298 }
299
300 let nx = x + dx;
301 let ny = y + dy;
302
303 if nx < 0 || nx > grid.width() as i64 || ny < 0 || ny > grid.height() as i64 {
304 continue;
305 }
306
307 let neighbor = Vertex {
308 x: nx as usize,
309 y: ny as usize,
310 };
311
312 if is_move_legal(grid, vertex, neighbor) {
313 neighbors.push(neighbor);
314 }
315 }
316 }
317
318 neighbors
319}
320
321fn is_move_legal(grid: &Grid, from: Vertex, to: Vertex) -> bool {
322 let x_min = from.x.min(to.x);
323 let x_max = from.x.max(to.x);
324 let y_min = from.y.min(to.y);
325 let y_max = from.y.max(to.y);
326
327 if x_min == x_max {
328 let x = x_min;
329 let y = y_min;
330 let left_open = if x > 0 {
331 grid.is_walkable(Point::new(x - 1, y))
332 } else {
333 false
334 };
335 let right_open = if x < grid.width() {
336 grid.is_walkable(Point::new(x, y))
337 } else {
338 false
339 };
340 left_open || right_open
341 } else if y_min == y_max {
342 let x = x_min;
343 let y = y_min;
344 let above_open = if y > 0 {
345 grid.is_walkable(Point::new(x, y - 1))
346 } else {
347 false
348 };
349 let below_open = if y < grid.height() {
350 grid.is_walkable(Point::new(x, y))
351 } else {
352 false
353 };
354 above_open || below_open
355 } else {
356 let cell_x = if to.x > from.x { from.x } else { from.x - 1 };
357 let cell_y = if to.y > from.y { from.y } else { from.y - 1 };
358 grid.is_walkable(Point::new(cell_x, cell_y))
359 }
360}
361
362#[derive(Debug, PartialEq)]
363struct FrontierEntry {
364 vertex: Vertex,
365 f_cost: f64,
366}
367
368impl Eq for FrontierEntry {}
369
370impl Ord for FrontierEntry {
371 fn cmp(&self, other: &Self) -> Ordering {
372 other
373 .f_cost
374 .partial_cmp(&self.f_cost)
375 .unwrap_or(Ordering::Equal)
376 }
377}
378
379impl PartialOrd for FrontierEntry {
380 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
381 Some(self.cmp(other))
382 }
383}