Skip to main content

condor_geometry/
polygonal.rs

1//! Polygon obstacle scenes and free-space predicates for the polygonal lane.
2//!
3//! Substrate shared by online [`crate::continuous::PolygonPathfinder`] solvers
4//! and source-rooted prepared maps: a [`WorldBounds`] rectangle, simple
5//! [`Polygon`] obstacle rings, and walkability / segment legality under a
6//! geometry epsilon. **Free space** is the closed world rectangle minus strict
7//! obstacle interiors; points on sealed world-boundary edges (obstacle edges
8//! collinear with the world frame) are non-traversable for endpoints and
9//! segments. Embedded public packs and oracle adapters remain in the facade.
10//!
11//! # Contract
12//!
13//! Build a [`PolygonScene`] from a validated [`WorldBounds`] and disjoint
14//! [`Polygon`] obstacles, then use its walkability predicates as the shared
15//! legality rule for online solvers and prepared maps. This module owns runtime
16//! geometry only; embedded public packs are a facade concern.
17
18const EPSILON: f64 = 1e-9;
19
20/// Continuous 2D point in polygon-scene coordinates (not grid cells).
21pub use condor_core::Point2;
22
23/// Axis-aligned rectangular world extent that bounds a [`PolygonScene`].
24///
25/// Free space is the closed rectangle minus obstacle interiors. Constructors
26/// do not enforce `min < max`; call [`WorldBounds::validate`] before search
27/// setup.
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct WorldBounds {
30    /// Inclusive lower-left corner of the axis-aligned world rectangle.
31    pub min: Point2,
32    /// Inclusive upper-right corner of the axis-aligned world rectangle.
33    pub max: Point2,
34}
35
36/// Static geometry or endpoint validation failure for polygonal scenes.
37///
38/// Variants describe invariants required before online search or prepared-map
39/// preprocess.
40#[derive(Debug, Clone, PartialEq, thiserror::Error)]
41#[non_exhaustive]
42pub enum PolygonValidationError {
43    /// `min` is not strictly less than `max` on both axes (zero or negative area).
44    #[error("polygon scene world bounds must have positive area")]
45    InvalidWorldBounds,
46    /// Obstacle ring has fewer than three vertices.
47    #[error("polygon obstacle {obstacle_index} must have at least three vertices (found {actual})")]
48    TooFewVertices {
49        /// Index into [`PolygonScene::obstacles`].
50        obstacle_index: usize,
51        /// Observed vertex count.
52        actual: usize,
53    },
54    /// Two vertices of the same obstacle coincide within geometry epsilon.
55    #[error(
56        "polygon obstacle {obstacle_index} repeats vertices {first_vertex_index} and {second_vertex_index}"
57    )]
58    DuplicateVertices {
59        /// Index into [`PolygonScene::obstacles`].
60        obstacle_index: usize,
61        /// First repeated vertex index.
62        first_vertex_index: usize,
63        /// Second repeated vertex index.
64        second_vertex_index: usize,
65    },
66    /// Obstacle has near-zero signed area (degenerate ring).
67    #[error("polygon obstacle {obstacle_index} must have non-zero area")]
68    ZeroArea {
69        /// Index into [`PolygonScene::obstacles`].
70        obstacle_index: usize,
71    },
72    /// Obstacle vertex lies outside the inclusive world rectangle.
73    #[error(
74        "polygon obstacle {obstacle_index} vertex {vertex_index} {vertex:?} must stay inside world bounds"
75    )]
76    VertexOutsideBounds {
77        /// Index into [`PolygonScene::obstacles`].
78        obstacle_index: usize,
79        /// Offending vertex index within the obstacle ring.
80        vertex_index: usize,
81        /// Offending vertex coordinates.
82        vertex: Point2,
83    },
84    /// Non-adjacent edges of the same obstacle properly intersect.
85    #[error(
86        "polygon obstacle {obstacle_index} edges starting at vertices {first_edge_start_index} and {second_edge_start_index} intersect"
87    )]
88    SelfIntersection {
89        /// Index into [`PolygonScene::obstacles`].
90        obstacle_index: usize,
91        /// Start vertex index of the first intersecting edge.
92        first_edge_start_index: usize,
93        /// Start vertex index of the second intersecting edge.
94        second_edge_start_index: usize,
95    },
96    /// Two obstacles share interior points, edge crossings, or nested vertices.
97    #[error("polygon obstacles {left_index} and {right_index} must be disjoint")]
98    ObstaclesOverlap {
99        /// First overlapping obstacle index.
100        left_index: usize,
101        /// Second overlapping obstacle index.
102        right_index: usize,
103    },
104    /// Start, goal, or source is outside free space (including sealed boundary).
105    #[error("polygon scene {endpoint:?} endpoint {point:?} must lie in traversable free space")]
106    EndpointNotTraversable {
107        /// Which endpoint role failed traversability.
108        endpoint: PolygonEndpoint,
109        /// Rejected continuous point.
110        point: Point2,
111    },
112}
113
114/// Endpoint role carried by [`PolygonValidationError::EndpointNotTraversable`].
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[non_exhaustive]
117pub enum PolygonEndpoint {
118    /// Request start for pair search.
119    Start,
120    /// Request goal for pair search or map query.
121    Goal,
122    /// Fixed source for source-rooted shortest-path maps.
123    Source,
124}
125
126impl WorldBounds {
127    /// Constructs bounds without validating area. Prefer [`WorldBounds::validate`].
128    #[must_use]
129    pub const fn new(min: Point2, max: Point2) -> Self {
130        Self { min, max }
131    }
132
133    /// Inclusive containment with geometry epsilon (points on the rectangle edge count as inside).
134    #[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    /// Ensures the rectangle has strictly positive width and height.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`PolygonValidationError::InvalidWorldBounds`] when the bounds do
147    /// not have positive area.
148    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/// Simple polygon obstacle: closed ordered vertex ring, not necessarily convex.
158///
159/// Does not auto-validate. Scene validation requires at least three distinct
160/// vertices, non-zero area, vertices inside world bounds, no self-intersections,
161/// and pairwise disjointness with other obstacles.
162#[derive(Debug, Clone, PartialEq)]
163pub struct Polygon {
164    vertices: Vec<Point2>,
165}
166
167impl Polygon {
168    /// Builds an obstacle from a vertex ring. Does not check geometry invariants.
169    #[must_use]
170    pub fn new(vertices: Vec<Point2>) -> Self {
171        Self { vertices }
172    }
173
174    /// Obstacle ring vertices in declaration order (not rewound by constructors).
175    #[must_use]
176    pub fn vertices(&self) -> &[Point2] {
177        &self.vertices
178    }
179
180    /// Shoelace signed area (positive for counter-clockwise rings in this coordinate system).
181    ///
182    /// Absolute value near zero is treated as degenerate during validation.
183    #[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    /// Ray-cast interior test: true only for strict interior points.
193    ///
194    /// Points on any edge of the boundary ring return `false` (boundary is free
195    /// space unless the edge is sealed against the world boundary).
196    #[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    /// Validates this obstacle as if it were obstacle index `0` in `world_bounds`.
215    ///
216    /// # Errors
217    ///
218    /// Returns a [`PolygonValidationError`] describing the violated polygon
219    /// geometry invariant.
220    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/// Start/goal pair for online polygonal search.
275///
276/// Coordinates are continuous scene space. Callers validate endpoints through
277/// the scene (or accept pathfinder-reported invalid-endpoint errors). Optional
278/// [`condor_core::SearchBudget`] caps expansions and/or wall-clock time; default
279/// is unlimited.
280#[derive(Debug, Clone, Copy, PartialEq)]
281pub struct PolygonSearchRequest {
282    /// Continuous start endpoint in scene coordinates.
283    pub start: Point2,
284    /// Continuous goal endpoint in scene coordinates.
285    pub goal: Point2,
286    /// Optional expansion / wall-clock caps for this query (default unlimited).
287    pub budget: condor_core::SearchBudget,
288}
289
290impl PolygonSearchRequest {
291    /// Builds a request with an unlimited budget without validating walkability.
292    #[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    /// Returns a copy of this request with the given budget.
302    #[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/// Bounded free space with polygon obstacles and walkability predicates.
310///
311/// Public fields are the static scene snapshot: world rectangle plus obstacle
312/// rings. Pathfinders and prepared maps treat geometry as immutable for a query.
313/// Prefer [`Self::validate_static`] / [`Self::validate`] before trusting a hand-
314/// built scene; constructors do not enforce invariants.
315#[derive(Debug, Clone, PartialEq)]
316pub struct PolygonScene {
317    /// Axis-aligned world rectangle that bounds free space.
318    pub world_bounds: WorldBounds,
319    /// Simple polygon obstacle rings; free space is the world rectangle minus their strict interiors.
320    pub obstacles: Vec<Polygon>,
321}
322
323impl PolygonScene {
324    /// Point free-space test: in bounds and not strictly inside any obstacle.
325    ///
326    /// Does **not** reject sealed world-boundary edges by itself; endpoint
327    /// validation (`validate`, `validate_source`, `validate_goal`) and
328    /// [`Self::segment_is_walkable`] apply the sealed-edge rule.
329    #[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    /// Whether the segment from `start` to `end` stays in free space.
339    ///
340    /// Rejects either endpoint when non-traversable (including sealed boundary),
341    /// then samples every obstacle-edge intersection parameter and each open
342    /// sub-interval midpoint. Visibility graphs and TFS use this as the edge
343    /// legality predicate; cost is always Euclidean length of accepted segments.
344    #[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 &parameters {
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    /// Validates world bounds, obstacle geometry, and pairwise obstacle disjointness.
385    ///
386    /// # Errors
387    ///
388    /// Returns a [`PolygonValidationError`] for invalid bounds, obstacle
389    /// geometry, or intersecting obstacles.
390    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    /// Validates static scene invariants and that `source` is traversable.
403    ///
404    /// # Errors
405    ///
406    /// Returns a [`PolygonValidationError`] when static geometry or the source
407    /// endpoint is invalid.
408    pub fn validate_source(&self, source: Point2) -> Result<(), PolygonValidationError> {
409        self.validate_static()?;
410        validate_traversable_endpoint(self, source, PolygonEndpoint::Source)
411    }
412
413    /// Validates static scene invariants and that `goal` is traversable.
414    ///
415    /// # Errors
416    ///
417    /// Returns a [`PolygonValidationError`] when static geometry or the goal
418    /// endpoint is invalid.
419    pub fn validate_goal(&self, goal: Point2) -> Result<(), PolygonValidationError> {
420        self.validate_static()?;
421        validate_traversable_endpoint(self, goal, PolygonEndpoint::Goal)
422    }
423
424    /// Validates static scene invariants and that both endpoints are traversable.
425    ///
426    /// # Errors
427    ///
428    /// Returns a [`PolygonValidationError`] when static geometry or either
429    /// endpoint is invalid.
430    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}