packages feed

moonlight-planar-1.1.0.0: src-dcel/Moonlight/Planar/Convex.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}

-- | Exact convex polygons: checked admission, canonical hull construction,
-- retained-polygon publication, and geometry observations. Morphology consumes
-- this owner; convex geometry has no dependency on morphology or overlay.
module Moonlight.Planar.Convex
  ( ConvexPolygon
  , ConvexError (..)
  , convexPolygon
  , convexPolygonPoints
  , convexPolygonBoundary
  , convexPolygonComponent
  , convexPolygonRegion
  , convexPolygonFromRetained
  , retainConvexPolygon
  , retainTranslatedConvexPolygon
  , convexPolygonFromLoop
  , convexHullPolygon
  , reflectConvexPolygon
  , convexPolygonCentroid
  , decomposeRegionIntoConvexSlabs
  ) where

import Control.DeepSeq (NFData)
import Data.Bifunctor (first)
import qualified Data.IntMap.Strict as IntMap
import qualified Data.IntSet as IntSet
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes)
import qualified Data.List as List
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Set as Set
import GHC.Generics (Generic)
import Moonlight.Planar.Exact
  ( ExactArithmeticError
  , ExactPoint
  , ExactRational
  , ExactRetainedPolygon
  , ExactVector
  , exactDivide
  , exactOrient2d
  , exactPoint
  , exactPointCoordinates
  , exactRetainedPolygonPoints
  , translateExactPoint
  )
import Moonlight.Planar.Internal.Exact (retainAdmittedConvexCycle)
import Moonlight.Planar.Internal.BoundaryCycle
  ( cyclePairs
  , consecutivePairs
  , firstNonCounterClockwiseTurn
  , rotateCycleLeast
  )
import Moonlight.Planar.Internal.ExactRational (exactRationalFromDyadic)
import Moonlight.Planar.Internal.Region.Types
  ( ConvexPolygon (..)
  , ExactLoop (..)
  , PlanarRegion (..)
  , PolygonComponent (..)
  , RegionValidationError
  )
import Moonlight.Planar.Region (exactLoop, exactLoopPoints, regionBoundaryEdges)

-- | Convex admission and exact geometry failures, independent of any consumer.
data ConvexError
  = ConvexInvalidLoop !RegionValidationError
  | ConvexNonConvexTurn !Int !Ordering
  | ConvexHullDegenerate ![ExactPoint]
  | ConvexExactArithmetic !ExactArithmeticError
  | ConvexSlabOddCrossings !ExactRational !ExactRational !Int
  deriving stock (Eq, Show, Generic)
  deriving anyclass (NFData)

convexPolygon :: NonEmpty ExactPoint -> Either ConvexError ConvexPolygon
convexPolygon submitted = do
  loop <- first ConvexInvalidLoop (exactLoop submitted)
  case firstNonCounterClockwiseTurn exactOrient2d (exactLoopPoints loop) of
    Just (index, turn) -> Left (ConvexNonConvexTurn index turn)
    Nothing -> Right (ConvexPolygon loop)

convexPolygonPoints :: ConvexPolygon -> NonEmpty ExactPoint
convexPolygonPoints (ConvexPolygon loop) = exactLoopPoints loop

convexPolygonComponent :: ConvexPolygon -> PolygonComponent
convexPolygonComponent (ConvexPolygon loop) = PolygonComponent loop []

convexPolygonRegion :: ConvexPolygon -> PlanarRegion
convexPolygonRegion = PlanarRegion . pure . convexPolygonComponent

-- | Strict convexity and simplicity descend from the retained-line carrier;
-- publication only rotates the cycle to its canonical least point.
convexPolygonFromRetained :: ExactRetainedPolygon -> ConvexPolygon
convexPolygonFromRetained = admittedConvexPolygon . exactRetainedPolygonPoints

-- | Retain the existing simple, strictly convex cycle without readmission.
retainConvexPolygon :: ConvexPolygon -> ExactRetainedPolygon
retainConvexPolygon = retainAdmittedConvexCycle . convexPolygonPoints

-- | Exact translation preserves admission and the cycle's starting position.
retainTranslatedConvexPolygon :: ExactVector -> ConvexPolygon -> ExactRetainedPolygon
retainTranslatedConvexPolygon offset =
  retainAdmittedConvexCycle . fmap (`translateExactPoint` offset) . convexPolygonPoints

admittedConvexPolygon :: NonEmpty ExactPoint -> ConvexPolygon
admittedConvexPolygon = ConvexPolygon . ExactLoop . rotateCycleLeast

-- | A simple loop already discharges intersection checks. Only strict
-- counter-clockwise convexity remains to admit the stronger carrier.
convexPolygonFromLoop :: ExactLoop -> Maybe ConvexPolygon
convexPolygonFromLoop loop =
  case firstNonCounterClockwiseTurn exactOrient2d (exactLoopPoints loop) of
    Nothing -> Just (ConvexPolygon loop)
    Just _ -> Nothing

convexHullPolygon :: NonEmpty ExactPoint -> Either ConvexError ConvexPolygon
convexHullPolygon submitted =
  let points = NonEmpty.toList submitted
   in case convexHullPoints points of
        Nothing -> Left (ConvexHullDegenerate points)
        Just hullPoints -> Right (admittedConvexPolygon hullPoints)

reflectConvexPolygon :: ConvexPolygon -> ConvexPolygon
reflectConvexPolygon =
  admittedConvexPolygon . fmap negateExactPoint . convexPolygonPoints

-- | The arithmetic mean of the vertices, a strict interior witness for every
-- admitted polygon. This is not an area-density centroid.
convexPolygonCentroid :: ConvexPolygon -> Either ConvexError ExactPoint
convexPolygonCentroid polygon = do
  let points = convexPolygonPoints polygon
      count = fromIntegral (NonEmpty.length points)
      (sumX, sumY) =
        List.foldl'
          (\(accumulatedX, accumulatedY) point ->
             let (x, y) = exactPointCoordinates point
              in (accumulatedX + x, accumulatedY + y))
          (0, 0)
          points
  x <- first ConvexExactArithmetic (exactDivide sumX count)
  y <- first ConvexExactArithmetic (exactDivide sumY count)
  pure (exactPoint x y)

convexHullPoints :: [ExactPoint] -> Maybe (NonEmpty ExactPoint)
convexHullPoints submitted =
  case Set.toAscList (Set.fromList submitted) of
    firstPoint : secondPoint : thirdPoint : remaining ->
      let ordered = firstPoint : secondPoint : thirdPoint : remaining
          lower = dropFinal (reverse (List.foldl' hullStep [] ordered))
          upper = dropFinal (reverse (List.foldl' hullStep [] (reverse ordered)))
       in case lower <> upper of
            firstHullPoint : secondHullPoint : thirdHullPoint : hullTail ->
              Just (firstHullPoint :| (secondHullPoint : thirdHullPoint : hullTail))
            _ -> Nothing
    _ -> Nothing

hullStep :: [ExactPoint] -> ExactPoint -> [ExactPoint]
hullStep (current : previous : remaining) candidate
  | exactOrient2d previous current candidate /= GT =
      hullStep (previous : remaining) candidate
hullStep hull candidate = candidate : hull

dropFinal :: [value] -> [value]
dropFinal values =
  case reverse values of
    _ : remaining -> reverse remaining
    [] -> []

negateExactPoint :: ExactPoint -> ExactPoint
negateExactPoint point =
  let (x, y) = exactPointCoordinates point
   in exactPoint (negate x) (negate y)

-- | One exact source edge restricted to a vertical slab. Its midpoint height
-- orders crossings; the two endpoint values retain the actual affine edge,
-- rather than a rounded segment reconstructed from a sample.
data SlabCrossing = SlabCrossing !ExactRational !ExactPoint !ExactPoint

-- | Partition an admitted polygonal region, including holes and disconnected
-- components, at its distinct vertex abscissae. No boundary vertex lies in an
-- open slab, so its crossing order is fixed and even-odd adjacent pairs bound
-- exact convex trapezoids (or triangles at a collapsed endpoint). Slab closure
-- overlaps have area zero; their union is exactly the source region. Immutable
-- endpoint event groups restrict each slab to its active edges; dormant source
-- boundaries are not rescanned or interpolated at unrelated abscissae.
decomposeRegionIntoConvexSlabs
  :: PlanarRegion
  -> Either ConvexError [ConvexPolygon]
decomposeRegionIntoConvexSlabs region =
  concat <$> sequenceA
    (snd (List.mapAccumL descendActiveSlab IntMap.empty (consecutivePairs abscissae)))
 where
  boundaries = regionBoundaryEdges region
  abscissae = Set.toAscList (Set.fromList (fmap (fst . exactPointCoordinates . fst) boundaries))
  half = exactRationalFromDyadic 1 (-1)
  indexedNonvertical =
    [ (index, min fromX toX, max fromX toX, edge)
    | (index, edge@(from, to)) <- zip [0 ..] boundaries
    , let fromX = fst (exactPointCoordinates from)
          toX = fst (exactPointCoordinates to)
    , fromX /= toX
    ]
  starts = Map.fromListWith IntMap.union
    [(fromX, IntMap.singleton index edge) | (index, fromX, _, edge) <- indexedNonvertical]
  ends = Map.fromListWith IntSet.union
    [(toX, IntSet.singleton index) | (index, _, toX, _) <- indexedNonvertical]

  descendActiveSlab
    :: IntMap.IntMap (ExactPoint, ExactPoint)
    -> (ExactRational, ExactRational)
    -> (IntMap.IntMap (ExactPoint, ExactPoint), Either ConvexError [ConvexPolygon])
  descendActiveSlab active bounds@(leftX, _) =
    let entering = Map.findWithDefault IntMap.empty leftX starts
        leaving = Map.findWithDefault IntSet.empty leftX ends
        activeSection = IntMap.union entering (IntMap.withoutKeys active leaving)
     in (activeSection, decomposeSlab activeSection bounds)

  decomposeSlab
    :: IntMap.IntMap (ExactPoint, ExactPoint)
    -> (ExactRational, ExactRational)
    -> Either ConvexError [ConvexPolygon]
  decomposeSlab active (leftX, rightX) = do
    let middleX = half * (leftX + rightX)
    crossings <- traverse (restrictEdge leftX rightX middleX) (IntMap.elems active)
    let ordered = List.sortOn crossingHeight crossings
        count = length ordered
        pairs = [pair | (index, pair) <- zip [0 :: Int ..] (consecutivePairs ordered), even index]
    if odd count
      then Left (ConvexSlabOddCrossings leftX rightX count)
      else catMaybes <$> traverse crossingPolygon pairs

  restrictEdge
    :: ExactRational
    -> ExactRational
    -> ExactRational
    -> (ExactPoint, ExactPoint)
    -> Either ConvexError SlabCrossing
  restrictEdge leftX rightX middleX (from, to) = do
    let (fromX, fromY) = exactPointCoordinates from
        (toX, toY) = exactPointCoordinates to
    slope <- first ConvexExactArithmetic (exactDivide (toY - fromY) (toX - fromX))
    let heightAt x = fromY + slope * (x - fromX)
    pure (SlabCrossing (heightAt middleX) (exactPoint leftX (heightAt leftX)) (exactPoint rightX (heightAt rightX)))

  crossingHeight :: SlabCrossing -> ExactRational
  crossingHeight (SlabCrossing height _ _) = height

  crossingPolygon :: (SlabCrossing, SlabCrossing) -> Either ConvexError (Maybe ConvexPolygon)
  crossingPolygon (SlabCrossing lowerHeight lowerLeft lowerRight, SlabCrossing upperHeight upperLeft upperRight)
    | lowerHeight == upperHeight = Right Nothing
    | otherwise = Just <$>
        (convexHullPolygon (lowerLeft :| [lowerRight, upperRight, upperLeft]))


-- | Counter-clockwise boundary of an admitted convex polygon.
convexPolygonBoundary :: ConvexPolygon -> [(ExactPoint, ExactPoint)]
convexPolygonBoundary = cyclePairs . convexPolygonPoints