🦀 Functional Rust

835: Point-in-Polygon — Ray Casting

Difficulty: 4 Level: Advanced Determine if a point lies inside an arbitrary polygon in O(n) — the fundamental primitive for geographic analysis, game collision, and mesh intersection.

The Problem This Solves

"Is this GPS coordinate inside this country's border?" "Did this mouse click land inside this irregular shape?" "Is this mesh vertex inside this other mesh?" All three reduce to point-in-polygon. It's one of the most frequently needed geometric primitives, appearing in GIS systems (PostGIS uses ray casting for `ST_Contains`), game engines (click detection on irregular sprites), and computational geometry pipelines. The ray casting algorithm is O(n) per query where n is the number of polygon edges — optimal without preprocessing. The winding number algorithm is an alternative that handles edge cases differently (particularly for self-intersecting polygons); ray casting is simpler and faster for simple polygons. Correctness hinges entirely on handling degenerate cases: the ray passing exactly through a vertex, the ray running along an edge, the point exactly on the boundary. Getting these right requires careful inequality choices (using `<` vs `≤` at exactly the right places).

The Intuition

Shoot a ray from P in the +x direction (i.e., to the right). Count how many times the ray crosses a polygon edge. Odd crossings: P is inside. Even crossings: outside. This is the Jordan curve theorem applied computationally. For each edge (A, B): the ray crosses the edge if A.y and B.y straddle P.y (one strictly above, one at or below — the careful inequality prevents double-counting vertices), and the crossing x-coordinate is to the right of P. The crossing x: `A.x + (P.y - A.y) / (B.y - A.y) × (B.x - A.x)`. Boundary detection (point exactly on an edge) is handled separately using the cross product to test collinearity and a bounding box check for the range.

How It Works in Rust

const EPS: f64 = 1e-10;

// Test if P lies on segment AB: collinear + within bounding box
fn on_segment(p: &Point, a: &Point, b: &Point) -> bool {
 let cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
 if cross.abs() > EPS { return false; }  // Not collinear
 p.x >= f64::min(a.x, b.x) - EPS && p.x <= f64::max(a.x, b.x) + EPS
     && p.y >= f64::min(a.y, b.y) - EPS && p.y <= f64::max(a.y, b.y) + EPS
}

fn point_in_polygon(p: &Point, polygon: &[Point]) -> Location {
 let n = polygon.len();
 if n == 0 { return Location::Outside; }

 let mut crossings = 0usize;
 for i in 0..n {
     let (a, b) = (&polygon[i], &polygon[(i + 1) % n]);

     if on_segment(p, a, b) { return Location::OnBoundary; }

     // Ray crossing condition:
     // 1. Edge straddles P.y: one vertex strictly above, one at-or-below
     //    (the strict/non-strict choice ensures a vertex is counted once)
     // 2. The crossing x-coordinate is to the RIGHT of P
     if (a.y <= p.y && b.y > p.y) || (b.y <= p.y && a.y > p.y) {
         let x_cross = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
         if p.x < x_cross {
             crossings += 1;
         }
     }
 }
 if crossings % 2 == 1 { Location::Inside } else { Location::Outside }
}
The `(i + 1) % n` wrap connects the last vertex to the first, closing the polygon without special-casing. The inequality choice `a.y <= p.y && b.y > p.y` (one `<=`, one `>`) is the standard way to count each vertex exactly once when the ray passes through it.

What This Unlocks

Key Differences

ConceptOCamlRust
Float comparison`<`, `<=` (built-in, raises on NaN)Same; use `EPS` for near-equality
Polygon edges`Array.init n (fun i -> (poly.(i), poly.((i+1) mod n)))``for i in 0..n` with `(i+1) % n`
Mutable counter`let count = ref 0 in count := !count + 1``let mut crossings = 0; crossings += 1`
Result typeVariant or int`enum Location { Inside, Outside, OnBoundary }`
Boundary epsilonSame EPS pattern`const EPS: f64 = 1e-10`
/// Point-in-Polygon: Ray Casting Algorithm.
///
/// Shoot a ray from P in the +x direction; count edge crossings.
/// Odd crossings = inside, even = outside. Boundary points detected separately.

#[derive(Clone, Copy, Debug, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn new(x: f64, y: f64) -> Self { Point { x, y } }
}

#[derive(Debug, PartialEq)]
enum Location {
    Inside,
    Outside,
    OnBoundary,
}

const EPS: f64 = 1e-10;

/// Test if point P lies on segment AB.
fn on_segment(p: &Point, a: &Point, b: &Point) -> bool {
    // Cross product near zero AND P within bounding box of AB
    let cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
    if cross.abs() > EPS { return false; }
    p.x >= f64::min(a.x, b.x) - EPS
        && p.x <= f64::max(a.x, b.x) + EPS
        && p.y >= f64::min(a.y, b.y) - EPS
        && p.y <= f64::max(a.y, b.y) + EPS
}

/// Ray casting: find location of P relative to polygon.
fn point_in_polygon(p: &Point, polygon: &[Point]) -> Location {
    let n = polygon.len();
    if n == 0 { return Location::Outside; }

    let mut crossings = 0usize;

    for i in 0..n {
        let a = &polygon[i];
        let b = &polygon[(i + 1) % n];

        // Boundary check
        if on_segment(p, a, b) {
            return Location::OnBoundary;
        }

        // Ray crossing: edge straddles P.y and crossing is to the right
        if (a.y <= p.y && b.y > p.y) || (b.y <= p.y && a.y > p.y) {
            let x_cross = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
            if p.x < x_cross {
                crossings += 1;
            }
        }
    }

    if crossings % 2 == 1 {
        Location::Inside
    } else {
        Location::Outside
    }
}

fn main() {
    // Unit square [0,2]×[0,2]
    let square = vec![
        Point::new(0.0, 0.0), Point::new(2.0, 0.0),
        Point::new(2.0, 2.0), Point::new(0.0, 2.0),
    ];

    let tests = [
        (Point::new(1.0, 1.0), "centre"),
        (Point::new(3.0, 1.0), "right outside"),
        (Point::new(0.0, 0.0), "corner"),
        (Point::new(1.0, 0.0), "bottom edge"),
        (Point::new(1.0, 2.5), "above"),
    ];
    println!("Square [0,2]×[0,2]:");
    for (p, label) in &tests {
        println!("  ({},{}) [{}]: {:?}", p.x, p.y, label, point_in_polygon(p, &square));
    }

    // L-shaped polygon
    let l_shape = vec![
        Point::new(0.0, 0.0), Point::new(2.0, 0.0), Point::new(2.0, 1.0),
        Point::new(1.0, 1.0), Point::new(1.0, 2.0), Point::new(0.0, 2.0),
    ];
    println!("\nL-shape:");
    let l_tests = [
        (Point::new(0.5, 0.5), "in bottom-left"),
        (Point::new(1.5, 1.5), "in top-right notch (outside)"),
        (Point::new(0.5, 1.5), "in top-left"),
    ];
    for (p, label) in &l_tests {
        println!("  ({},{}) [{}]: {:?}", p.x, p.y, label, point_in_polygon(p, &l_shape));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn square() -> Vec<Point> {
        vec![
            Point::new(0.0, 0.0), Point::new(2.0, 0.0),
            Point::new(2.0, 2.0), Point::new(0.0, 2.0),
        ]
    }

    #[test]
    fn test_inside() {
        assert_eq!(point_in_polygon(&Point::new(1.0, 1.0), &square()), Location::Inside);
    }

    #[test]
    fn test_outside() {
        assert_eq!(point_in_polygon(&Point::new(3.0, 1.0), &square()), Location::Outside);
        assert_eq!(point_in_polygon(&Point::new(-1.0, 1.0), &square()), Location::Outside);
    }

    #[test]
    fn test_boundary_edge() {
        assert_eq!(point_in_polygon(&Point::new(1.0, 0.0), &square()), Location::OnBoundary);
    }

    #[test]
    fn test_boundary_corner() {
        assert_eq!(point_in_polygon(&Point::new(0.0, 0.0), &square()), Location::OnBoundary);
    }

    #[test]
    fn test_l_shape() {
        let l = vec![
            Point::new(0.0, 0.0), Point::new(2.0, 0.0), Point::new(2.0, 1.0),
            Point::new(1.0, 1.0), Point::new(1.0, 2.0), Point::new(0.0, 2.0),
        ];
        assert_eq!(point_in_polygon(&Point::new(0.5, 0.5), &l), Location::Inside);
        assert_eq!(point_in_polygon(&Point::new(1.5, 1.5), &l), Location::Outside);
        assert_eq!(point_in_polygon(&Point::new(0.5, 1.5), &l), Location::Inside);
    }

    #[test]
    fn test_triangle() {
        let tri = vec![
            Point::new(0.0, 0.0), Point::new(4.0, 0.0), Point::new(2.0, 4.0),
        ];
        assert_eq!(point_in_polygon(&Point::new(2.0, 1.0), &tri), Location::Inside);
        assert_eq!(point_in_polygon(&Point::new(3.5, 3.0), &tri), Location::Outside);
    }
}
(* Point-in-Polygon: Ray Casting in OCaml *)

type point = { x: float; y: float }

(* Ray casting: count how many times the +x ray from P crosses polygon edges *)
(* Returns `Inside`, `Outside`, or `OnBoundary` *)
type location = Inside | Outside | OnBoundary

let point_in_polygon (p : point) (polygon : point array) : location =
  let n = Array.length polygon in
  if n = 0 then Outside
  else begin
    let crossings = ref 0 in
    let on_boundary = ref false in
    for i = 0 to n - 1 do
      let a = polygon.(i) in
      let b = polygon.((i + 1) mod n) in
      (* Check if P is on the segment AB *)
      let cross = (b.x -. a.x) *. (p.y -. a.y) -. (b.y -. a.y) *. (p.x -. a.x) in
      let min_x = min a.x b.x and max_x = max a.x b.x in
      let min_y = min a.y b.y and max_y = max a.y b.y in
      if abs_float cross < 1e-12
         && p.x >= min_x && p.x <= max_x
         && p.y >= min_y && p.y <= max_y then
        on_boundary := true;
      (* Ray crossing test *)
      if (a.y <= p.y && b.y > p.y) || (b.y <= p.y && a.y > p.y) then begin
        let x_cross = a.x +. (p.y -. a.y) /. (b.y -. a.y) *. (b.x -. a.x) in
        if p.x < x_cross then incr crossings
      end
    done;
    if !on_boundary then OnBoundary
    else if !crossings mod 2 = 1 then Inside
    else Outside
  end

let string_of_location = function
  | Inside -> "Inside" | Outside -> "Outside" | OnBoundary -> "OnBoundary"

let () =
  (* Square [0,2]×[0,2] *)
  let square = [| {x=0.0;y=0.0}; {x=2.0;y=0.0};
                  {x=2.0;y=2.0}; {x=0.0;y=2.0} |] in
  let tests = [
    ({x=1.0;y=1.0}, "centre");
    ({x=3.0;y=1.0}, "right outside");
    ({x=0.0;y=0.0}, "corner");
    ({x=1.0;y=0.0}, "edge");
    ({x=1.0;y=2.5}, "above");
  ] in
  Printf.printf "Square [0,2]×[0,2]:\n";
  List.iter (fun (p, label) ->
    Printf.printf "  (%g,%g) [%s]: %s\n" p.x p.y label
      (string_of_location (point_in_polygon p square))
  ) tests;

  (* L-shaped polygon *)
  let l_shape = [| {x=0.0;y=0.0}; {x=2.0;y=0.0}; {x=2.0;y=1.0};
                   {x=1.0;y=1.0}; {x=1.0;y=2.0}; {x=0.0;y=2.0} |] in
  Printf.printf "\nL-shape polygon:\n";
  List.iter (fun (p, label) ->
    Printf.printf "  (%g,%g) [%s]: %s\n" p.x p.y label
      (string_of_location (point_in_polygon p l_shape))
  ) [({x=0.5;y=0.5}, "bottom-left leg"); ({x=1.5;y=1.5}, "top-right — outside")]