packages feed

moonlight-triangulation-1.4.0.4: src-public/Moonlight/Triangulation/Internal/Minkowski/Convex.hs

-- | Pure exact convex-polygon algebra: admission, linear edge-angle
-- convolution, reflection, hull construction, and support-half-plane erosion.
module Moonlight.Triangulation.Internal.Minkowski.Convex
  ( convexPolygon
  , convexPolygonPoints
  , convexPolygonComponent
  , convexPolygonRegion
  , convexPolygonFromRetained
  , admittedConvexLoop
  , structuringElement
  , structuringElementPolygon
  , convexMinkowskiSum
  , convexMinkowskiPolygon
  , convexHullPolygon
  , reflectConvexPolygon
  , convexPolygonCentroid
  , erodeConvexBy
  , addExactPoints
  , subtractExactPoints
  ) where

import Data.Bifunctor (first)
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 Moonlight.Triangulation.Exact
  ( ExactClipDisposition (..)
  , ExactClipReceipt
  , ExactClosedHalfPlane
  , ExactPoint
  , ExactRetainedPolygon
  , ExactVector (..)
  , addExactVectors
  , exactClipRetainedPolygon
  , exactClosedHalfPlaneFromDirectedEdge
  , compareExactVectorAngle
  , exactVectorFromPoints
  , exactOrient2d
  , exactPoint
  , exactPointCoordinates
  , exactRetainedPolygon
  , exactRetainedPolygonPoints
  , translateExactPoint
  )
import Moonlight.Triangulation.Internal.BoundaryCycle
  ( cyclePairs
  , cyclePairsNonEmpty
  , firstNonCounterClockwiseTurn
  , rotateCycleLeast
  , rotateCycleLeastBy
  )
import Moonlight.Triangulation.Internal.ExactRational
  ( ExactRational
  , exactDivide
  )
import Moonlight.Triangulation.Internal.Minkowski.Types
  ( ConvexPolygon (..)
  , MinkowskiError (..)
  , StructuringElement (..)
  )
import Moonlight.Triangulation.Internal.Region.Types
  ( ExactLoop (..)
  , PlanarRegion (..)
  , PolygonComponent (..)
  , RegionPointLocation (..)
  )
import Moonlight.Triangulation.Region
  ( exactLoop
  , exactLoopPoints
  , regionPointLocation
  )

convexPolygon
  :: NonEmpty ExactPoint
  -> Either MinkowskiError ConvexPolygon
convexPolygon submitted = do
  loop <- first MinkowskiInvalidConvexLoop (exactLoop submitted)
  let points = exactLoopPoints loop
  case firstNonCounterClockwiseTurn exactOrient2d points of
    Just (index, turn) -> Left (MinkowskiNonConvexTurn 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

-- | Total projection from the retained-line carrier. Strict convexity and
-- simplicity are already invariants of 'ExactRetainedPolygon'; only canonical
-- cycle rotation remains.
convexPolygonFromRetained :: ExactRetainedPolygon -> ConvexPolygon
convexPolygonFromRetained = admittedConvexPolygon . exactRetainedPolygonPoints

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

admittedConvexLoop :: ExactLoop -> Maybe ConvexPolygon
admittedConvexLoop loop =
  case firstNonCounterClockwiseTurn exactOrient2d (exactLoopPoints loop) of
    Nothing -> Just (ConvexPolygon loop)
    Just _ -> Nothing

structuringElement
  :: ConvexPolygon
  -> Either MinkowskiError StructuringElement
structuringElement polygon =
  let origin = exactPoint 0 0
      location = regionPointLocation (convexPolygonRegion polygon) origin
   in case location of
        RegionExterior -> Left (MinkowskiOriginOutside location)
        _ -> Right (StructuringElement polygon)

structuringElementPolygon :: StructuringElement -> ConvexPolygon
structuringElementPolygon (StructuringElement polygon) = polygon

convexMinkowskiSum
  :: ConvexPolygon
  -> ConvexPolygon
  -> PlanarRegion
convexMinkowskiSum left right =
  convexPolygonRegion (convexMinkowskiPolygon left right)

convexMinkowskiPolygon
  :: ConvexPolygon
  -> ConvexPolygon
  -> ConvexPolygon
convexMinkowskiPolygon left right =
  let leftPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints left)
      rightPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints right)
      start = addExactPoints (NonEmpty.head leftPoints) (NonEmpty.head rightPoints)
      directions =
        mergeDirections
          (edgeDirections leftPoints)
          (edgeDirections rightPoints)
      directionList = NonEmpty.toList directions
      scanned = scanl translateExactPoint start directionList
      resultPoints = start :| take (length directionList - 1) (drop 1 scanned)
   in admittedConvexPolygon resultPoints

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

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

convexPolygonCentroid
  :: ConvexPolygon
  -> Either MinkowskiError 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 MinkowskiExactArithmetic (exactDivide sumX count)
  y <- first MinkowskiExactArithmetic (exactDivide sumY count)
  pure (exactPoint x y)

-- | Erode one convex polygon by another through the strongest translated
-- support half-plane for each source edge. The canonical sorted exact descent
-- retains those source lines; a lower-dimensional residual is represented by
-- the empty polygonal region at this two-dimensional publication boundary.
erodeConvexBy
  :: ConvexPolygon
  -> ConvexPolygon
  -> Either MinkowskiError (Maybe ConvexPolygon, ExactClipReceipt)
erodeConvexBy source kernel = do
  let sourcePoints = convexPolygonPoints source
      kernelPoints = convexPolygonPoints kernel
      firstKernel = NonEmpty.head kernelPoints
      initial =
        fmap
          (`subtractExactPoints` firstKernel)
          sourcePoints
  retained <- first MinkowskiInvalidHalfPlane (exactRetainedPolygon initial)
  halfPlanes <-
    traverse
      (uncurry (strongestHalfPlane kernelPoints))
      (cyclePairs sourcePoints)
  (disposition, clipReceipt) <-
    first MinkowskiClipFailed (exactClipRetainedPolygon retained halfPlanes)
  let eroded =
        case disposition of
          ExactClipFullDimensional clipped ->
            Just (convexPolygonFromRetained clipped)
          ExactClipLowerDimensional _ -> Nothing
          ExactClipEmpty -> Nothing
  pure (eroded, clipReceipt)

edgeDirections :: NonEmpty ExactPoint -> NonEmpty ExactVector
edgeDirections = fmap (uncurry exactVectorFromPoints) . cyclePairsNonEmpty

mergeDirections
  :: NonEmpty ExactVector
  -> NonEmpty ExactVector
  -> NonEmpty ExactVector
mergeDirections (left :| leftTail) (right :| rightTail) =
  case compareExactVectorAngle left right of
    LT -> left :| mergeRemaining leftTail (right : rightTail)
    GT -> right :| mergeRemaining (left : leftTail) rightTail
    EQ -> addExactVectors left right :| mergeRemaining leftTail rightTail

mergeRemaining :: [ExactVector] -> [ExactVector] -> [ExactVector]
mergeRemaining [] right = right
mergeRemaining left [] = left
mergeRemaining left@(leftHead : leftTail) right@(rightHead : rightTail) =
  case compareExactVectorAngle leftHead rightHead of
    LT -> leftHead : mergeRemaining leftTail right
    GT -> rightHead : mergeRemaining left rightTail
    EQ -> addExactVectors leftHead rightHead : mergeRemaining leftTail rightTail

pointSweepKey :: ExactPoint -> (ExactRational, ExactRational)
pointSweepKey point =
  let (x, y) = exactPointCoordinates point
   in (y, x)

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
    [] -> []

strongestHalfPlane
  :: NonEmpty ExactPoint
  -> ExactPoint
  -> ExactPoint
  -> Either MinkowskiError ExactClosedHalfPlane
strongestHalfPlane kernelPoints from to =
  let direction = exactVectorFromPoints from to
      supportPoint =
        case kernelPoints of
          initial :| remaining ->
            List.foldl'
              (\selected candidate ->
                 if directionPointCross direction candidate
                      < directionPointCross direction selected
                   then candidate
                   else selected)
              initial
              remaining
   in first MinkowskiInvalidHalfPlane
        ( exactClosedHalfPlaneFromDirectedEdge
            (subtractExactPoints from supportPoint)
            (subtractExactPoints to supportPoint)
        )

directionPointCross :: ExactVector -> ExactPoint -> ExactRational
directionPointCross (ExactVector directionX directionY) point =
  let (x, y) = exactPointCoordinates point
   in directionX * y - directionY * x

addExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
addExactPoints left right =
  let (leftX, leftY) = exactPointCoordinates left
      (rightX, rightY) = exactPointCoordinates right
   in exactPoint (leftX + rightX) (leftY + rightY)

subtractExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
subtractExactPoints left right =
  let (leftX, leftY) = exactPointCoordinates left
      (rightX, rightY) = exactPointCoordinates right
   in exactPoint (leftX - rightX) (leftY - rightY)

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