1use std::cmp::Ordering;
7
8use crate::{
9 Grid,
10 any_angle::geometry::{
11 approximately_equal, canonicalize_grid_vertex, is_endpoint_valid, retain_as_corner,
12 sampling_segment_is_legal, validate_sampling_path,
13 },
14};
15use condor_core::Point2;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum IntervalKind {
23 Flat,
25 Cone,
27}
28
29impl PartialOrd for IntervalKind {
30 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
31 Some(self.cmp(other))
32 }
33}
34
35impl Ord for IntervalKind {
36 fn cmp(&self, other: &Self) -> Ordering {
37 match (self, other) {
38 (Self::Flat, Self::Flat) | (Self::Cone, Self::Cone) => Ordering::Equal,
39 (Self::Flat, Self::Cone) => Ordering::Less,
40 (Self::Cone, Self::Flat) => Ordering::Greater,
41 }
42 }
43}
44
45#[must_use]
47pub fn compare_f64_total(left: f64, right: f64) -> Ordering {
48 match (left.is_finite(), right.is_finite()) {
49 (false, false) => Ordering::Equal,
50 (false, true) => Ordering::Greater,
51 (true, false) => Ordering::Less,
52 (true, true) => {
53 if left < right {
54 Ordering::Less
55 } else if left > right {
56 Ordering::Greater
57 } else {
58 Ordering::Equal
59 }
60 }
61 }
62}
63
64#[must_use]
66pub fn interval_state_key(
67 grid: &Grid,
68 root: Point2,
69 root_g: f64,
70 row: f64,
71 left: f64,
72 right: f64,
73 goal: Point2,
74) -> f64 {
75 if !root_g.is_finite() {
76 return f64::INFINITY;
77 }
78 let Some(best_p) = minimizing_legal_point_on_interval(grid, root, row, left, right, goal)
79 else {
80 return f64::INFINITY;
81 };
82 root_g + root.distance_to(best_p) + best_p.distance_to(goal)
83}
84
85#[must_use]
87pub fn best_interval_goal_candidate(
88 grid: &Grid,
89 root: Point2,
90 root_g: f64,
91 row: f64,
92 left: f64,
93 right: f64,
94 goal: Point2,
95) -> Option<(Point2, f64)> {
96 let best_p = minimizing_legal_point_on_interval(grid, root, row, left, right, goal)?;
97 let cost = root_g + root.distance_to(best_p) + best_p.distance_to(goal);
98 Some((best_p, cost))
99}
100
101#[must_use]
103pub fn visible_flat_span_from_corner(
104 grid: &Grid,
105 corner: Point2,
106 clip_left: f64,
107 clip_right: f64,
108) -> Option<(f64, f64)> {
109 let row_y = corner.y;
110 let cx = corner.x.round() as i32;
111 let left_bound = clip_left.round() as i32;
112 let right_bound = clip_right.round() as i32;
113
114 let mut lo = cx;
115 while lo > left_bound {
116 let prev = Point2::new((lo - 1) as f64, row_y);
117 if !segment_legal(grid, corner, prev) {
118 break;
119 }
120 lo -= 1;
121 }
122
123 let mut hi = cx;
124 while hi < right_bound {
125 let next = Point2::new((hi + 1) as f64, row_y);
126 if !segment_legal(grid, corner, next) {
127 break;
128 }
129 hi += 1;
130 }
131
132 let left = (lo as f64).max(clip_left);
133 let right = (hi as f64).min(clip_right);
134 if right + 1e-12 < left {
135 None
136 } else {
137 Some((left, right))
138 }
139}
140
141fn minimizing_legal_point_on_interval(
142 grid: &Grid,
143 root: Point2,
144 row: f64,
145 left: f64,
146 right: f64,
147 goal: Point2,
148) -> Option<Point2> {
149 let mut best: Option<Point2> = None;
150 let mut best_cost = f64::INFINITY;
151
152 let mut consider = |candidate: Point2| {
153 if candidate.x + 1e-12 < left || candidate.x > right + 1e-12 {
154 return;
155 }
156 if !segment_legal(grid, root, candidate) {
157 return;
158 }
159 let cost = point_goal_cost(root, candidate, goal);
160 if cost < best_cost {
161 best = Some(candidate);
162 best_cost = cost;
163 }
164 };
165
166 consider(Point2::new(left, row));
167 consider(Point2::new(right, row));
168
169 if approximately_equal(root.y, row) {
174 consider(Point2::new(root.x.clamp(left, right), row));
175 }
176 if approximately_equal(goal.y, row) {
177 consider(Point2::new(goal.x.clamp(left, right), row));
178 }
179
180 let reflected_goal_y = 2.0 * row - goal.y;
186 let dy = reflected_goal_y - root.y;
187 if dy.abs() > 1e-15 {
188 let t = (row - root.y) / dy;
189 let x = root.x + t * (goal.x - root.x);
190 if x.is_finite() {
191 consider(Point2::new(x.clamp(left, right), row));
192 }
193 }
194
195 best
196}
197
198fn point_goal_cost(root: Point2, point: Point2, goal: Point2) -> f64 {
199 root.distance_to(point) + point.distance_to(goal)
200}
201
202#[must_use]
208pub fn project_cone_to_row(
209 root: Point2,
210 row: f64,
211 left: f64,
212 right: f64,
213 target_row: f64,
214) -> Option<(f64, f64)> {
215 if approximately_equal(row, target_row) {
216 return Some((left, right));
217 }
218
219 let x0 = cone_bound_at_row(root, row, left, target_row);
220 let x1 = cone_bound_at_row(root, row, right, target_row);
221 let lo = x0.min(x1);
222 let hi = x0.max(x1);
223 if hi + 1e-12 >= lo {
224 Some((lo, hi))
225 } else {
226 None
227 }
228}
229
230fn cone_bound_at_row(root: Point2, interval_row: f64, endpoint_x: f64, target_row: f64) -> f64 {
231 if approximately_equal(interval_row, root.y) && approximately_equal(endpoint_x, root.x) {
232 return root.x;
233 }
234
235 let endpoint = Point2::new(endpoint_x, interval_row);
236 if let Some(x) = ray_intersect_row(root, endpoint, target_row) {
237 return x;
238 }
239
240 if approximately_equal(interval_row, root.y) {
242 if target_row > root.y {
243 if endpoint_x + 1e-12 < root.x {
244 return f64::NEG_INFINITY;
245 }
246 if endpoint_x > root.x + 1e-12 {
247 return f64::INFINITY;
248 }
249 } else if target_row < root.y {
250 if endpoint_x + 1e-12 < root.x {
251 return f64::INFINITY;
252 }
253 if endpoint_x > root.x + 1e-12 {
254 return f64::NEG_INFINITY;
255 }
256 }
257 }
258
259 root.x
260}
261
262fn ray_intersect_row(origin: Point2, through: Point2, row_y: f64) -> Option<f64> {
263 let dy = through.y - origin.y;
264 if dy.abs() <= 1e-15 {
265 return None;
266 }
267 let t = (row_y - origin.y) / dy;
268 if t < -1e-12 {
269 return None;
270 }
271 Some(origin.x + t * (through.x - origin.x))
272}
273
274#[must_use]
276pub fn segment_legal(grid: &Grid, start: Point2, end: Point2) -> bool {
277 sampling_segment_is_legal(grid, start, end)
278}
279
280#[must_use]
282pub fn validate_path(grid: &Grid, points: &[Point2]) -> bool {
283 validate_sampling_path(grid, points)
284}
285
286pub fn parse_request(
288 grid: &Grid,
289 start: Point2,
290 goal: Point2,
291) -> Result<(Point2, Point2), crate::AnyAngleSearchError> {
292 let Some(start) = canonicalize_grid_vertex(start) else {
293 return Err(crate::AnyAngleSearchError::InvalidStart { point: start });
294 };
295 let Some(goal) = canonicalize_grid_vertex(goal) else {
296 return Err(crate::AnyAngleSearchError::InvalidGoal { point: goal });
297 };
298 if !is_endpoint_valid(grid, start) {
299 return Err(crate::AnyAngleSearchError::InvalidStart { point: start });
300 }
301 if !is_endpoint_valid(grid, goal) {
302 return Err(crate::AnyAngleSearchError::InvalidGoal { point: goal });
303 }
304 Ok((start, goal))
305}
306
307#[must_use]
312pub fn is_turn_candidate(grid: &Grid, vx: i32, vy: i32) -> bool {
313 retain_as_corner(grid, vx, vy)
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::{Grid, grid::Cell, point::Point};
320 use condor_core::Point2;
321
322 #[test]
323 fn project_cone_from_root_on_row_reaches_adjacent_rows() {
324 let root = Point2::new(0.0, 0.0);
325 let projected = project_cone_to_row(root, 0.0, 0.0, 63.0, 1.0).expect("projection");
326 assert!(projected.0 <= 0.0 + 1e-9);
327 assert!(projected.1 >= 63.0 - 1e-9);
328 }
329
330 #[test]
331 fn checkerboard_direct_diagonal_is_illegal() {
332 let mut grid = Grid::new(5, 5).expect("grid");
333 for (x, y) in [
334 (1, 0),
335 (3, 0),
336 (0, 1),
337 (2, 1),
338 (4, 1),
339 (1, 2),
340 (3, 2),
341 (0, 3),
342 (2, 3),
343 (4, 3),
344 (1, 4),
345 (3, 4),
346 ] {
347 grid.set_cell(Point::new(x, y), Cell::Blocked)
348 .expect("block");
349 }
350 assert!(!segment_legal(
351 &grid,
352 Point2::new(0.0, 0.0),
353 Point2::new(4.0, 4.0)
354 ));
355 }
356
357 #[test]
358 fn staircase_concave_witness_segments_are_legal() {
359 let mut grid = Grid::new(6, 4).expect("grid");
360 for (x, y) in [
361 (1, 0),
362 (2, 0),
363 (2, 1),
364 (3, 1),
365 (4, 1),
366 (4, 2),
367 (5, 2),
368 (5, 3),
369 ] {
370 grid.set_cell(Point::new(x, y), Cell::Blocked)
371 .expect("block");
372 }
373 let pts = [
374 Point2::new(0.0, 3.0),
375 Point2::new(2.0, 1.0),
376 Point2::new(3.0, 1.0),
377 Point2::new(5.0, 0.0),
378 ];
379 for pair in pts.windows(2) {
380 assert!(
381 segment_legal(&grid, pair[0], pair[1]),
382 "{:?} -> {:?}",
383 pair[0],
384 pair[1]
385 );
386 }
387 assert!(is_turn_candidate(&grid, 2, 1));
388 assert!(is_turn_candidate(&grid, 3, 1));
389 assert!(segment_legal(
390 &grid,
391 Point2::new(0.0, 3.0),
392 Point2::new(2.0, 1.0)
393 ));
394 }
395
396 #[test]
397 fn forbidden_pinch_corner_geometry() {
398 let mut grid = Grid::new(4, 4).expect("grid");
399 grid.set_cell(Point::new(1, 1), Cell::Blocked)
400 .expect("block");
401 grid.set_cell(Point::new(2, 2), Cell::Blocked)
402 .expect("block");
403
404 assert!(segment_legal(
405 &grid,
406 Point2::new(0.0, 0.0),
407 Point2::new(1.0, 2.0)
408 ));
409 assert!(segment_legal(
410 &grid,
411 Point2::new(1.0, 2.0),
412 Point2::new(2.0, 3.0)
413 ));
414 assert!(is_turn_candidate(&grid, 1, 2));
415
416 let key = interval_state_key(
417 &grid,
418 Point2::new(1.0, 2.0),
419 2.0_f64.sqrt(),
420 2.0,
421 1.0,
422 3.0,
423 Point2::new(3.0, 3.0),
424 );
425 assert!(
426 key < 5.0,
427 "turn key should beat suboptimal detour, got {key}"
428 );
429 }
430}