1const EPSILON: f64 = 1e-9;
19
20pub use condor_core::Point2;
22
23#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct WorldBounds {
30 pub min: Point2,
32 pub max: Point2,
34}
35
36#[derive(Debug, Clone, PartialEq, thiserror::Error)]
41#[non_exhaustive]
42pub enum PolygonValidationError {
43 #[error("polygon scene world bounds must have positive area")]
45 InvalidWorldBounds,
46 #[error("polygon obstacle {obstacle_index} must have at least three vertices (found {actual})")]
48 TooFewVertices {
49 obstacle_index: usize,
51 actual: usize,
53 },
54 #[error(
56 "polygon obstacle {obstacle_index} repeats vertices {first_vertex_index} and {second_vertex_index}"
57 )]
58 DuplicateVertices {
59 obstacle_index: usize,
61 first_vertex_index: usize,
63 second_vertex_index: usize,
65 },
66 #[error("polygon obstacle {obstacle_index} must have non-zero area")]
68 ZeroArea {
69 obstacle_index: usize,
71 },
72 #[error(
74 "polygon obstacle {obstacle_index} vertex {vertex_index} {vertex:?} must stay inside world bounds"
75 )]
76 VertexOutsideBounds {
77 obstacle_index: usize,
79 vertex_index: usize,
81 vertex: Point2,
83 },
84 #[error(
86 "polygon obstacle {obstacle_index} edges starting at vertices {first_edge_start_index} and {second_edge_start_index} intersect"
87 )]
88 SelfIntersection {
89 obstacle_index: usize,
91 first_edge_start_index: usize,
93 second_edge_start_index: usize,
95 },
96 #[error("polygon obstacles {left_index} and {right_index} must be disjoint")]
98 ObstaclesOverlap {
99 left_index: usize,
101 right_index: usize,
103 },
104 #[error("polygon scene {endpoint:?} endpoint {point:?} must lie in traversable free space")]
106 EndpointNotTraversable {
107 endpoint: PolygonEndpoint,
109 point: Point2,
111 },
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[non_exhaustive]
117pub enum PolygonEndpoint {
118 Start,
120 Goal,
122 Source,
124}
125
126impl WorldBounds {
127 #[must_use]
129 pub const fn new(min: Point2, max: Point2) -> Self {
130 Self { min, max }
131 }
132
133 #[must_use]
135 pub fn contains(self, point: Point2) -> bool {
136 point.x >= self.min.x - EPSILON
137 && point.x <= self.max.x + EPSILON
138 && point.y >= self.min.y - EPSILON
139 && point.y <= self.max.y + EPSILON
140 }
141
142 pub fn validate(self) -> Result<(), PolygonValidationError> {
149 if self.min.x >= self.max.x || self.min.y >= self.max.y {
150 return Err(PolygonValidationError::InvalidWorldBounds);
151 }
152
153 Ok(())
154 }
155}
156
157#[derive(Debug, Clone, PartialEq)]
163pub struct Polygon {
164 vertices: Vec<Point2>,
165}
166
167impl Polygon {
168 #[must_use]
170 pub fn new(vertices: Vec<Point2>) -> Self {
171 Self { vertices }
172 }
173
174 #[must_use]
176 pub fn vertices(&self) -> &[Point2] {
177 &self.vertices
178 }
179
180 #[must_use]
184 pub fn signed_area(&self) -> f64 {
185 let mut area = 0.0;
186 for (a, b) in polygon_edges(&self.vertices) {
187 area += (a.x * b.y) - (b.x * a.y);
188 }
189 area / 2.0
190 }
191
192 #[must_use]
197 pub fn contains_point_strict(&self, point: Point2) -> bool {
198 if polygon_edges(self.vertices()).any(|(start, end)| point_on_segment(point, start, end)) {
199 return false;
200 }
201
202 let mut inside = false;
203 for (start, end) in polygon_edges(self.vertices()) {
204 let crosses = ((start.y > point.y) != (end.y > point.y))
205 && (point.x
206 < ((end.x - start.x) * (point.y - start.y) / (end.y - start.y)) + start.x);
207 if crosses {
208 inside = !inside;
209 }
210 }
211 inside
212 }
213
214 pub fn validate(&self, world_bounds: WorldBounds) -> Result<(), PolygonValidationError> {
221 self.validate_at(0, world_bounds)
222 }
223
224 fn validate_at(
225 &self,
226 obstacle_index: usize,
227 world_bounds: WorldBounds,
228 ) -> Result<(), PolygonValidationError> {
229 if self.vertices.len() < 3 {
230 return Err(PolygonValidationError::TooFewVertices {
231 obstacle_index,
232 actual: self.vertices.len(),
233 });
234 }
235
236 if let Some((first_vertex_index, second_vertex_index)) =
237 duplicate_vertex_indices(self.vertices())
238 {
239 return Err(PolygonValidationError::DuplicateVertices {
240 obstacle_index,
241 first_vertex_index,
242 second_vertex_index,
243 });
244 }
245
246 if self.signed_area().abs() <= EPSILON {
247 return Err(PolygonValidationError::ZeroArea { obstacle_index });
248 }
249
250 for (vertex_index, vertex) in self.vertices().iter().enumerate() {
251 if !world_bounds.contains(*vertex) {
252 return Err(PolygonValidationError::VertexOutsideBounds {
253 obstacle_index,
254 vertex_index,
255 vertex: *vertex,
256 });
257 }
258 }
259
260 if let Some((first_edge_start_index, second_edge_start_index)) =
261 self_intersection_edge_indices(self.vertices())
262 {
263 return Err(PolygonValidationError::SelfIntersection {
264 obstacle_index,
265 first_edge_start_index,
266 second_edge_start_index,
267 });
268 }
269
270 Ok(())
271 }
272}
273
274#[derive(Debug, Clone, Copy, PartialEq)]
281pub struct PolygonSearchRequest {
282 pub start: Point2,
284 pub goal: Point2,
286 pub budget: condor_core::SearchBudget,
288}
289
290impl PolygonSearchRequest {
291 #[must_use]
293 pub const fn new(start: Point2, goal: Point2) -> Self {
294 Self {
295 start,
296 goal,
297 budget: condor_core::SearchBudget::UNLIMITED,
298 }
299 }
300
301 #[must_use]
303 pub const fn with_budget(mut self, budget: condor_core::SearchBudget) -> Self {
304 self.budget = budget;
305 self
306 }
307}
308
309#[derive(Debug, Clone, PartialEq)]
316pub struct PolygonScene {
317 pub world_bounds: WorldBounds,
319 pub obstacles: Vec<Polygon>,
321}
322
323impl PolygonScene {
324 #[must_use]
330 pub fn is_walkable(&self, point: Point2) -> bool {
331 self.world_bounds.contains(point)
332 && !self
333 .obstacles
334 .iter()
335 .any(|obstacle| obstacle.contains_point_strict(point))
336 }
337
338 #[must_use]
345 pub fn segment_is_walkable(&self, start: Point2, end: Point2) -> bool {
346 if !point_is_traversable(self, start) || !point_is_traversable(self, end) {
347 return false;
348 }
349
350 let mut parameters = vec![0.0, 1.0];
351 for obstacle in &self.obstacles {
352 for (edge_start, edge_end) in polygon_edges(obstacle.vertices()) {
353 parameters.extend(segment_intersection_parameters(
354 start, end, edge_start, edge_end,
355 ));
356 }
357 }
358
359 sort_and_dedup_parameters(&mut parameters);
360
361 for parameter in ¶meters {
362 let point = interpolate_segment(start, end, *parameter);
363 if !point_is_traversable(self, point) {
364 return false;
365 }
366 }
367
368 for interval in parameters.windows(2) {
369 let start_parameter = interval[0];
370 let end_parameter = interval[1];
371 if end_parameter - start_parameter <= EPSILON {
372 continue;
373 }
374
375 let midpoint = interpolate_segment(start, end, (start_parameter + end_parameter) / 2.0);
376 if !point_is_traversable(self, midpoint) {
377 return false;
378 }
379 }
380
381 true
382 }
383
384 pub fn validate_static(&self) -> Result<(), PolygonValidationError> {
391 self.world_bounds.validate()?;
392
393 for (obstacle_index, obstacle) in self.obstacles.iter().enumerate() {
394 obstacle.validate_at(obstacle_index, self.world_bounds)?;
395 }
396
397 validate_obstacle_disjointness(&self.obstacles)?;
398
399 Ok(())
400 }
401
402 pub fn validate_source(&self, source: Point2) -> Result<(), PolygonValidationError> {
409 self.validate_static()?;
410 validate_traversable_endpoint(self, source, PolygonEndpoint::Source)
411 }
412
413 pub fn validate_goal(&self, goal: Point2) -> Result<(), PolygonValidationError> {
420 self.validate_static()?;
421 validate_traversable_endpoint(self, goal, PolygonEndpoint::Goal)
422 }
423
424 pub fn validate(&self, request: PolygonSearchRequest) -> Result<(), PolygonValidationError> {
431 self.validate_static()?;
432 validate_traversable_endpoint(self, request.start, PolygonEndpoint::Start)?;
433 validate_traversable_endpoint(self, request.goal, PolygonEndpoint::Goal)?;
434 Ok(())
435 }
436}
437
438fn polygon_edges(vertices: &[Point2]) -> impl Iterator<Item = (Point2, Point2)> + '_ {
439 vertices
440 .iter()
441 .copied()
442 .zip(vertices.iter().copied().cycle().skip(1))
443 .take(vertices.len())
444}
445
446fn sort_and_dedup_parameters(parameters: &mut Vec<f64>) {
447 parameters.sort_by(f64::total_cmp);
448 parameters.dedup_by(|left, right| (*left - *right).abs() <= EPSILON);
449}
450
451fn interpolate_segment(start: Point2, end: Point2, parameter: f64) -> Point2 {
452 Point2::new(
453 start.x + ((end.x - start.x) * parameter),
454 start.y + ((end.y - start.y) * parameter),
455 )
456}
457
458fn segment_intersection_parameters(
459 a_start: Point2,
460 a_end: Point2,
461 b_start: Point2,
462 b_end: Point2,
463) -> Vec<f64> {
464 let mut parameters = Vec::with_capacity(2);
465 for point in [a_start, a_end, b_start, b_end] {
466 if point_on_segment(point, a_start, a_end) && point_on_segment(point, b_start, b_end) {
467 parameters.push(segment_parameter(point, a_start, a_end));
468 }
469 }
470
471 if !parameters.is_empty() {
472 sort_and_dedup_parameters(&mut parameters);
473 return parameters;
474 }
475
476 if let Some(parameter) = proper_intersection_parameter(a_start, a_end, b_start, b_end) {
477 parameters.push(parameter);
478 }
479
480 parameters
481}
482
483fn segment_parameter(point: Point2, start: Point2, end: Point2) -> f64 {
484 let dx = end.x - start.x;
485 let dy = end.y - start.y;
486 if dx.abs() >= dy.abs() && dx.abs() > EPSILON {
487 ((point.x - start.x) / dx).clamp(0.0, 1.0)
488 } else if dy.abs() > EPSILON {
489 ((point.y - start.y) / dy).clamp(0.0, 1.0)
490 } else {
491 0.0
492 }
493}
494
495fn proper_intersection_parameter(
496 a_start: Point2,
497 a_end: Point2,
498 b_start: Point2,
499 b_end: Point2,
500) -> Option<f64> {
501 let o1 = orientation(a_start, a_end, b_start);
502 let o2 = orientation(a_start, a_end, b_end);
503 let o3 = orientation(b_start, b_end, a_start);
504 let o4 = orientation(b_start, b_end, a_end);
505
506 let properly_crosses = (o1 > EPSILON && o2 < -EPSILON || o1 < -EPSILON && o2 > EPSILON)
507 && (o3 > EPSILON && o4 < -EPSILON || o3 < -EPSILON && o4 > EPSILON);
508 if !properly_crosses {
509 return None;
510 }
511
512 let a_dx = a_end.x - a_start.x;
513 let a_dy = a_end.y - a_start.y;
514 let b_dx = b_end.x - b_start.x;
515 let b_dy = b_end.y - b_start.y;
516 let denominator = cross(a_dx, a_dy, b_dx, b_dy);
517 if denominator.abs() <= EPSILON {
518 return None;
519 }
520
521 let offset_x = b_start.x - a_start.x;
522 let offset_y = b_start.y - a_start.y;
523 Some((cross(offset_x, offset_y, b_dx, b_dy) / denominator).clamp(0.0, 1.0))
524}
525
526fn validate_obstacle_disjointness(obstacles: &[Polygon]) -> Result<(), PolygonValidationError> {
527 for (left_index, left) in obstacles.iter().enumerate() {
528 for (right_index, right) in obstacles.iter().enumerate().skip(left_index + 1) {
529 if polygons_intersect_or_overlap(left, right) {
530 return Err(PolygonValidationError::ObstaclesOverlap {
531 left_index,
532 right_index,
533 });
534 }
535 }
536 }
537
538 Ok(())
539}
540
541fn polygons_intersect_or_overlap(left: &Polygon, right: &Polygon) -> bool {
542 polygon_edges(left.vertices()).any(|left_edge| {
543 polygon_edges(right.vertices()).any(|right_edge| {
544 segments_intersect(left_edge.0, left_edge.1, right_edge.0, right_edge.1)
545 })
546 }) || left
547 .vertices()
548 .iter()
549 .copied()
550 .any(|vertex| right.contains_point_strict(vertex))
551 || right
552 .vertices()
553 .iter()
554 .copied()
555 .any(|vertex| left.contains_point_strict(vertex))
556}
557
558fn point_is_traversable(scene: &PolygonScene, point: Point2) -> bool {
559 scene.world_bounds.contains(point)
560 && !scene
561 .obstacles
562 .iter()
563 .any(|obstacle| obstacle.contains_point_strict(point))
564 && !point_on_sealed_boundary(scene, point)
565}
566
567fn validate_traversable_endpoint(
568 scene: &PolygonScene,
569 point: Point2,
570 endpoint: PolygonEndpoint,
571) -> Result<(), PolygonValidationError> {
572 if !point_is_traversable(scene, point) {
573 return Err(PolygonValidationError::EndpointNotTraversable { endpoint, point });
574 }
575
576 Ok(())
577}
578
579fn point_on_sealed_boundary(scene: &PolygonScene, point: Point2) -> bool {
580 scene.obstacles.iter().any(|obstacle| {
581 polygon_edges(obstacle.vertices()).any(|(start, end)| {
582 point_on_segment(point, start, end)
583 && edge_lies_on_world_boundary(start, end, scene.world_bounds)
584 })
585 })
586}
587
588fn edge_lies_on_world_boundary(start: Point2, end: Point2, bounds: WorldBounds) -> bool {
589 ((start.x - bounds.min.x).abs() <= EPSILON && (end.x - bounds.min.x).abs() <= EPSILON)
590 || ((start.x - bounds.max.x).abs() <= EPSILON && (end.x - bounds.max.x).abs() <= EPSILON)
591 || ((start.y - bounds.min.y).abs() <= EPSILON && (end.y - bounds.min.y).abs() <= EPSILON)
592 || ((start.y - bounds.max.y).abs() <= EPSILON && (end.y - bounds.max.y).abs() <= EPSILON)
593}
594
595fn duplicate_vertex_indices(vertices: &[Point2]) -> Option<(usize, usize)> {
596 for (index, vertex) in vertices.iter().enumerate() {
597 if let Some(second_index) = vertices
598 .iter()
599 .enumerate()
600 .skip(index + 1)
601 .find_map(|(second_index, other)| points_equal(*vertex, *other).then_some(second_index))
602 {
603 return Some((index, second_index));
604 }
605 }
606
607 None
608}
609
610fn self_intersection_edge_indices(vertices: &[Point2]) -> Option<(usize, usize)> {
611 let edge_count = vertices.len();
612 for first_index in 0..edge_count {
613 let first = (
614 vertices[first_index],
615 vertices[(first_index + 1) % edge_count],
616 );
617 for second_index in (first_index + 1)..edge_count {
618 if edges_are_adjacent(first_index, second_index, edge_count) {
619 continue;
620 }
621
622 let second = (
623 vertices[second_index],
624 vertices[(second_index + 1) % edge_count],
625 );
626 if segments_intersect(first.0, first.1, second.0, second.1) {
627 return Some((first_index, second_index));
628 }
629 }
630 }
631
632 None
633}
634
635fn edges_are_adjacent(first_index: usize, second_index: usize, edge_count: usize) -> bool {
636 first_index == second_index
637 || (first_index + 1) % edge_count == second_index
638 || (second_index + 1) % edge_count == first_index
639}
640
641fn segments_intersect(a_start: Point2, a_end: Point2, b_start: Point2, b_end: Point2) -> bool {
642 let a_start_on_b = point_on_segment(a_start, b_start, b_end);
643 let a_end_on_b = point_on_segment(a_end, b_start, b_end);
644 let b_start_on_a = point_on_segment(b_start, a_start, a_end);
645 let b_end_on_a = point_on_segment(b_end, a_start, a_end);
646 if a_start_on_b || a_end_on_b || b_start_on_a || b_end_on_a {
647 return true;
648 }
649
650 let o1 = orientation(a_start, a_end, b_start);
651 let o2 = orientation(a_start, a_end, b_end);
652 let o3 = orientation(b_start, b_end, a_start);
653 let o4 = orientation(b_start, b_end, a_end);
654
655 (o1 > EPSILON && o2 < -EPSILON || o1 < -EPSILON && o2 > EPSILON)
656 && (o3 > EPSILON && o4 < -EPSILON || o3 < -EPSILON && o4 > EPSILON)
657}
658
659fn orientation(start: Point2, end: Point2, point: Point2) -> f64 {
660 ((end.x - start.x) * (point.y - start.y)) - ((end.y - start.y) * (point.x - start.x))
661}
662
663fn cross(ax: f64, ay: f64, bx: f64, by: f64) -> f64 {
664 (ax * by) - (ay * bx)
665}
666
667fn points_equal(left: Point2, right: Point2) -> bool {
668 (left.x - right.x).abs() <= EPSILON && (left.y - right.y).abs() <= EPSILON
669}
670
671fn point_on_segment(point: Point2, start: Point2, end: Point2) -> bool {
672 let cross =
673 ((point.y - start.y) * (end.x - start.x)) - ((point.x - start.x) * (end.y - start.y));
674 if cross.abs() > EPSILON {
675 return false;
676 }
677
678 let dot = ((point.x - start.x) * (end.x - start.x)) + ((point.y - start.y) * (end.y - start.y));
679 if dot < -EPSILON {
680 return false;
681 }
682
683 let length_sq =
684 ((end.x - start.x) * (end.x - start.x)) + ((end.y - start.y) * (end.y - start.y));
685 dot <= length_sq + EPSILON
686}
687
688#[cfg(test)]
689mod tests {
690 use super::{
691 Point2, Polygon, PolygonEndpoint, PolygonScene, PolygonSearchRequest,
692 PolygonValidationError, WorldBounds,
693 };
694
695 #[test]
696 fn scene_validation_reports_the_failing_obstacle_index() {
697 let scene = PolygonScene {
698 world_bounds: WorldBounds::new(Point2::new(0.0, 0.0), Point2::new(10.0, 10.0)),
699 obstacles: vec![
700 Polygon::new(vec![
701 Point2::new(1.0, 1.0),
702 Point2::new(2.0, 1.0),
703 Point2::new(1.0, 2.0),
704 ]),
705 Polygon::new(vec![Point2::new(4.0, 4.0), Point2::new(5.0, 4.0)]),
706 ],
707 };
708
709 assert_eq!(
710 scene.validate_static(),
711 Err(PolygonValidationError::TooFewVertices {
712 obstacle_index: 1,
713 actual: 2,
714 })
715 );
716 }
717
718 #[test]
719 fn polygon_validation_reports_duplicate_vertex_indices() {
720 let polygon = Polygon::new(vec![
721 Point2::new(1.0, 1.0),
722 Point2::new(4.0, 1.0),
723 Point2::new(4.0, 4.0),
724 Point2::new(1.0, 1.0),
725 ]);
726
727 assert_eq!(
728 polygon.validate(WorldBounds::new(
729 Point2::new(0.0, 0.0),
730 Point2::new(10.0, 10.0),
731 )),
732 Err(PolygonValidationError::DuplicateVertices {
733 obstacle_index: 0,
734 first_vertex_index: 0,
735 second_vertex_index: 3,
736 })
737 );
738 }
739
740 #[test]
741 fn polygon_validation_reports_intersecting_edge_indices() {
742 let polygon = Polygon::new(vec![
743 Point2::new(2.0, 7.0),
744 Point2::new(4.0, 2.0),
745 Point2::new(8.0, 7.0),
746 Point2::new(2.0, 4.0),
747 Point2::new(8.0, 4.0),
748 ]);
749
750 assert_eq!(
751 polygon.validate(WorldBounds::new(
752 Point2::new(0.0, 0.0),
753 Point2::new(10.0, 10.0),
754 )),
755 Err(PolygonValidationError::SelfIntersection {
756 obstacle_index: 0,
757 first_edge_start_index: 0,
758 second_edge_start_index: 2,
759 })
760 );
761 }
762
763 #[test]
764 fn rejects_requests_that_start_on_a_sealed_boundary_edge() {
765 let scene = sealed_boundary_scene();
766 let request = PolygonSearchRequest::new(Point2::new(5.0, 0.0), Point2::new(2.0, 5.0));
767
768 assert_eq!(
769 scene.validate(request),
770 Err(PolygonValidationError::EndpointNotTraversable {
771 endpoint: PolygonEndpoint::Start,
772 point: Point2::new(5.0, 0.0),
773 })
774 );
775 }
776
777 #[test]
778 fn rejects_sources_that_start_on_a_sealed_boundary_edge() {
779 assert_eq!(
780 sealed_boundary_scene().validate_source(Point2::new(5.0, 0.0)),
781 Err(PolygonValidationError::EndpointNotTraversable {
782 endpoint: PolygonEndpoint::Source,
783 point: Point2::new(5.0, 0.0),
784 })
785 );
786 }
787
788 #[test]
789 fn rejects_goals_inside_an_obstacle_for_repeated_query_validation() {
790 let scene = PolygonScene {
791 world_bounds: WorldBounds::new(Point2::new(0.0, 0.0), Point2::new(10.0, 10.0)),
792 obstacles: vec![Polygon::new(vec![
793 Point2::new(4.0, 4.0),
794 Point2::new(6.0, 4.0),
795 Point2::new(6.0, 6.0),
796 Point2::new(4.0, 6.0),
797 ])],
798 };
799
800 assert_eq!(
801 scene.validate_goal(Point2::new(5.0, 5.0)),
802 Err(PolygonValidationError::EndpointNotTraversable {
803 endpoint: PolygonEndpoint::Goal,
804 point: Point2::new(5.0, 5.0),
805 })
806 );
807 }
808
809 #[test]
810 fn rejects_segments_that_try_to_slide_along_a_sealed_boundary_edge() {
811 let scene = sealed_boundary_scene();
812 assert!(!scene.segment_is_walkable(Point2::new(4.5, 0.0), Point2::new(5.5, 0.0)));
813 }
814
815 fn sealed_boundary_scene() -> PolygonScene {
816 PolygonScene {
817 world_bounds: WorldBounds::new(Point2::new(0.0, 0.0), Point2::new(10.0, 10.0)),
818 obstacles: vec![Polygon::new(vec![
819 Point2::new(4.0, 0.0),
820 Point2::new(6.0, 0.0),
821 Point2::new(6.0, 10.0),
822 Point2::new(4.0, 10.0),
823 ])],
824 }
825 }
826}