packages feed

moonlight-planar-1.1.0.0: test/algebra/Moonlight/Planar/ConvexSpec.hs

-- | Convex admission and publication laws at the exact-geometry owner.
module Moonlight.Planar.ConvexSpec (tests) where

import Data.Foldable (traverse_)
import qualified Data.List as List
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import Moonlight.Planar.Convex
  ( ConvexError (..)
  , convexHullPolygon
  , convexPolygon
  , convexPolygonCentroid
  , convexPolygonFromLoop
  , convexPolygonFromRetained
  , convexPolygonPoints
  , convexPolygonRegion
  , reflectConvexPolygon
  )
import Moonlight.Planar.Exact (exactOrient2d, exactPoint, exactRetainedPolygon)
import Moonlight.Planar.Region
  ( RegionPointLocation (..)
  , exactLoop
  , regionPointLocation
  )
import Support (assertEqual, requireRight)

tests :: IO ()
tests = sequence_
  [ testConvexAdmission
  , testConvexHull
  , testRetainedPublication
  , testConvexReflection
  , putStrLn "convex: ok"
  ]

testConvexAdmission :: IO ()
testConvexAdmission = do
  let points = exactPoint 0 0 :| [exactPoint 3 0, exactPoint 3 2, exactPoint 0 2]
  polygon <- requireRight "convex rectangle" (convexPolygon points)
  rotated <- requireRight "rotated rectangle"
    (convexPolygon (exactPoint 3 2 :| [exactPoint 0 2, exactPoint 0 0, exactPoint 3 0]))
  assertEqual "convex admission canonicalizes rotation" polygon rotated
  loop <- requireRight "admitted rectangle loop" (exactLoop points)
  assertEqual "admitted loop strengthens without readmission" (Just polygon) (convexPolygonFromLoop loop)
  centroid <- requireRight "convex interior witness" (convexPolygonCentroid polygon)
  assertEqual "convex witness lies inside" RegionInterior (regionPointLocation (convexPolygonRegion polygon) centroid)
  case convexPolygon (exactPoint 0 0 :| [exactPoint 0 2, exactPoint 3 2, exactPoint 3 0]) of
    Left (ConvexNonConvexTurn _ LT) -> pure ()
    outcome -> fail ("clockwise convex cycle was not refused: " <> show outcome)
  let top = exactPoint 0 3
      lowerLeft = exactPoint (-2) (-3)
      right = exactPoint 3 1
      left = exactPoint (-3) 1
      lowerRight = exactPoint 2 (-3)
      star = top :| [lowerLeft, right, left, lowerRight]
  assertEqual "star has strictly positive local turns" (replicate 5 GT)
    [ exactOrient2d top lowerLeft right
    , exactOrient2d lowerLeft right left
    , exactOrient2d right left lowerRight
    , exactOrient2d left lowerRight top
    , exactOrient2d lowerRight top lowerLeft
    ]
  case convexPolygon star of
    Left (ConvexInvalidLoop _) -> pure ()
    outcome -> fail ("local turns must not admit a self-intersecting star: " <> show outcome)

testConvexHull :: IO ()
testConvexHull = do
  let points = exactPoint 0 0 :| [exactPoint 2 0, exactPoint 2 2, exactPoint 0 2]
  expected <- requireRight "canonical square hull" (convexPolygon points)
  traverse_
    (\permutation ->
       case NonEmpty.nonEmpty permutation of
         Nothing -> fail "a nonempty permutation became empty"
         Just submitted -> do
           hull <- requireRight "permuted convex hull" (convexHullPolygon submitted)
           assertEqual "convex hull is permutation invariant" expected hull)
    (List.permutations (NonEmpty.toList points))
  hull <- requireRight "hull discards interior points and repeated vertices"
    (convexHullPolygon (points <> (exactPoint 1 1 :| [exactPoint 0 0])))
  assertEqual "convex hull keeps only extreme points" expected hull
  let collinear = exactPoint 0 0 :| [exactPoint 1 0, exactPoint 2 0]
  assertEqual "degenerate hull is a convex error"
    (Left (ConvexHullDegenerate (NonEmpty.toList collinear)))
    (convexHullPolygon collinear)

testRetainedPublication :: IO ()
testRetainedPublication = do
  let rotated = exactPoint 2 2 :| [exactPoint 0 2, exactPoint 0 0, exactPoint 2 0]
  retained <- requireRight "retained convex polygon" (exactRetainedPolygon rotated)
  checked <- requireRight "checked retained polygon" (convexPolygon rotated)
  assertEqual "trusted retained publication agrees with checked admission"
    checked (convexPolygonFromRetained retained)

testConvexReflection :: IO ()
testConvexReflection = do
  triangle <- requireRight "asymmetric convex triangle"
    (convexPolygon (exactPoint 0 0 :| [exactPoint 3 0, exactPoint 1 2]))
  let reflected = reflectConvexPolygon triangle
  readmitted <- requireRight "reflection preserves counter-clockwise orientation"
    (convexPolygon (convexPolygonPoints reflected))
  assertEqual "reflected carrier remains admissible" reflected readmitted
  assertEqual "convex reflection is involutive" triangle (reflectConvexPolygon reflected)