packages feed

pure-cdt-0.2.0.0: src/Triangulation/Geometry/Polygon.hs

{-# LANGUAGE DerivingStrategies #-}

-- | Simple polygons, convex hulls of three and four points, and the polygon
-- side of merging two triangulations: sliding a bridge between two hulls
-- until it is tangent to both.
--
-- Throughout the library polygons are wound /clockwise/ (with the y axis
-- pointing up): walking along the boundary, the interior is on the right.
module Triangulation.Geometry.Polygon (
  Polygon (..),
  vertices,
  polygonEdges,
  Bridge,
  Hull4 (..),
  hullOf3,
  hullOf4,
  hull4Polygon,
  tangents,
  isConvex,
  isPointInPolygon,
) where

import Control.DeepSeq (NFData)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Triangulation.Geometry.Edge (Edge, mkEdge)
import Triangulation.Geometry.Point (Orientation (..), Point (..), turn)
import Triangulation.Geometry.Ring (
  arc,
  cyclicPairs,
  cyclicTriples,
  predecessor,
  splitLoop,
  successor,
 )

-- | A simple polygon, wound clockwise; see the module header.
newtype Polygon = Polygon (NonEmpty Point)
  deriving stock (Eq, Show)
  deriving newtype (NFData)

-- | The vertices, in boundary order.
vertices :: Polygon -> [Point]
vertices (Polygon points) = NE.toList points

-- | The boundary edges, including the one closing the ring.
polygonEdges :: Polygon -> [Edge]
polygonEdges (Polygon points) = map (uncurry mkEdge) (cyclicPairs points)

-- hulls of 3 and 4 points

-- | Convex hull of three points, wound clockwise. A collinear triple is kept
-- as a degenerate polygon.
hullOf3 :: Point -> Point -> Point -> Polygon
hullOf3 a b c =
  let (pivot, r1, r2) = sort3By compare a b c
   in Polygon $ case hullOrder pivot r1 r2 of
        GT -> pivot :| [r2, r1]
        _ -> pivot :| [r1, r2]

-- | Convex hull of four points.
data Hull4
  = -- | all four points are hull vertices, wound clockwise
    Quadrilateral Point Point Point Point
  | -- | a hull triangle, wound clockwise, and the point inside it
    TriangleWithInner Point Point Point Point

-- | Convex hull of four points, telling a convex quadrilateral from a
-- triangle with a point inside.
hullOf4 :: Point -> Point -> Point -> Point -> Hull4
hullOf4 a b c d =
  let (p1, r1, r2, r3) = sort4By compare a b c d
      (p2, p3, p4) = sort3By (hullOrder p1) r1 r2 r3
   in case (turn p2 p3 p4, turn p3 p4 p1) of
        (CounterClockwise, _) -> TriangleWithInner p1 p2 p4 p3
        (_, Clockwise) -> Quadrilateral p1 p2 p3 p4
        _ -> TriangleWithInner p1 p2 p3 p4 -- rare case: points on the same line

-- | The hull as a polygon, dropping an inner point.
hull4Polygon :: Hull4 -> Polygon
hull4Polygon (Quadrilateral p1 p2 p3 p4) = Polygon (p1 :| [p2, p3, p4])
hull4Polygon (TriangleWithInner p1 p2 p3 _) = Polygon (p1 :| [p2, p3])

-- | Angular order around a pivot: @a@ precedes @b@ when @b@ lies clockwise of @pivot -> a@.
hullOrder :: Point -> Point -> Point -> Ordering
hullOrder pivot a b = case turn pivot a b of
  Clockwise -> LT
  CounterClockwise -> GT
  Collinear -> EQ -- unreachable: 'turn' resolves collinear triples

sort3By :: (a -> a -> Ordering) -> a -> a -> a -> (a, a, a)
sort3By cmp a b c
  | cmp c lo /= GT = (c, lo, hi)
  | cmp c hi /= GT = (lo, c, hi)
  | otherwise = (lo, hi, c)
  where
    (lo, hi) = if cmp a b == GT then (b, a) else (a, b)

sort4By :: (a -> a -> Ordering) -> a -> a -> a -> a -> (a, a, a, a)
sort4By cmp a b c d
  | cmp a x /= GT = (a, x, y, z)
  | cmp a y /= GT = (x, a, y, z)
  | cmp a z /= GT = (x, y, a, z)
  | otherwise = (x, y, z, a)
  where
    (x, y, z) = sort3By cmp b c d

-- tangents

-- | A pair of vertices joining two polygons being merged:
-- @(vertex of the first polygon, vertex of the second polygon)@.
type Bridge = (Point, Point)

-- | Starting from an initial bridge (used as both the bottom and the top
-- one), slide the bridge endpoints along the two hulls until both bridges are
-- tangent to both polygons. Returns the bottom and top tangents and the merged
-- hull. 'Nothing' if a bridge endpoint is not a vertex of its polygon.
tangents :: Polygon -> Polygon -> Bridge -> Maybe (Bridge, Bridge, Polygon)
tangents (Polygon points1) (Polygon points2) start = go start start
  where
    go bottom@(bl, br) top@(tl, tr) = case slide bottom top of
      Just (bottom', top') -> go bottom' top'
      Nothing -> do
        cut1 <- arc tl bl points1
        cut2 <- arc br tr points2
        pure (bottom, top, removeLoops (Polygon (cut1 <> cut2)) bottom top)

    -- One step: move whichever bridge endpoint still sees its hull neighbour
    -- on the wrong side. 'Nothing' when both bridges are tangent.
    slide (bl, br) (tl, tr) = do
      blPred <- predecessor bl points1
      brSucc <- successor br points2
      tlSucc <- successor tl points1
      trPred <- predecessor tr points2
      pick blPred brSucc tlSucc trPred
      where
        pick blPred brSucc tlSucc trPred
          | turn blPred bl br == CounterClockwise = Just ((blPred, br), (tl, tr))
          | turn bl br brSucc == CounterClockwise = Just ((bl, brSucc), (tl, tr))
          | turn tr tl tlSucc == CounterClockwise = Just ((bl, br), (tlSucc, tr))
          | turn trPred tr tl == CounterClockwise = Just ((bl, br), (tl, trPred))
          | otherwise = Nothing

-- | When a bridge degenerates to a single vertex on one side, the merged ring
-- visits that vertex twice; keep the larger of the two loops.
removeLoops :: Polygon -> Bridge -> Bridge -> Polygon
removeLoops polygon (bl, br) (tl, tr)
  | bl == tl = largerLoop polygon bl
  | br == tr = largerLoop polygon br
  | otherwise = polygon

largerLoop :: Polygon -> Point -> Polygon
largerLoop (Polygon points) point =
  let (outer, inner) = splitLoop point points
   in case NE.nonEmpty inner of
        Just inner' | doubledArea (Polygon outer) <= doubledArea (Polygon inner') -> Polygon inner'
        _ -> Polygon outer

-- | Twice the area (shoelace formula); enough for comparisons.
doubledArea :: Polygon -> Double
doubledArea (Polygon points) =
  abs $ sum [x1 * y2 - y1 * x2 | (Point x1 y1, Point x2 y2) <- cyclicPairs points]

-- checking

-- | Whether the polygon is convex (no counter-clockwise turn along the boundary).
isConvex :: Polygon -> Bool
isConvex (Polygon (_ :| [_])) = True
isConvex (Polygon points) =
  all (\(p1, p2, p3) -> turn p1 p2 p3 /= CounterClockwise) (cyclicTriples points)

-- | Whether the point lies strictly inside the polygon, which need not be
-- convex: the number of polygon edges crossed by a ray from the point to the
-- right is odd (the even–odd rule). A vertex of the polygon does not count as
-- inside; for a point exactly on an edge the answer is not specified.
isPointInPolygon :: Polygon -> Point -> Bool
isPointInPolygon (Polygon points) point@(Point x y) =
  point `notElem` points && odd (length (filter crossesRay (cyclicPairs points)))
  where
    -- half-open in y, so that a ray through a vertex is counted once
    crossesRay (Point x1 y1, Point x2 y2) =
      (y1 > y) /= (y2 > y) && x < x1 + (y - y1) * (x2 - x1) / (y2 - y1)