packages feed

moonlight-planar-1.1.0.0: test/support/Support.hs

{-# LANGUAGE NumericUnderscores #-}

-- | Assertions and exact planar fixtures shared by the test slices. Nothing
-- here may depend on an optional package flag, so the minimal configuration
-- compiles the same helpers as the full one.
module Support
  ( requireRight
  , requireQueryPoint
  , requireJust
  , randomPoints
  , assertEqual
  , assertValid
  , integerPoint
  , rectangleLoop
  , rectangleComponent
  , rectangleRegion
  , polygonComponentOf
  , polygonRegion
  , annulusRegion
  ) where

import Control.Monad (unless)
import qualified Data.Set as Set
import Data.Word (Word64)
import Data.List.NonEmpty (NonEmpty (..))
import Moonlight.Planar.Exact (ExactPoint, exactPoint)
import Moonlight.Planar.Point (mkQueryPoint)
import Moonlight.Planar.Region
  ( ExactLoop, PlanarRegion, PolygonComponent, exactLoop, planarRegion, polygonComponent )
import Moonlight.Planar.Point (Point (..), QueryPoint)
import Moonlight.Planar.Types (Triangulation)
import Moonlight.Planar.Validation (validateTriangulation)

requireRight :: Show error => String -> Either error value -> IO value
requireRight label value = case value of
  Left failure -> fail (label <> ": " <> show failure)
  Right result -> pure result

requireQueryPoint :: String -> Point -> IO QueryPoint
requireQueryPoint label = requireRight label . mkQueryPoint

assertEqual :: (Eq value, Show value) => String -> value -> value -> IO ()
assertEqual label expected actual =
  unless (expected == actual) $
    fail (label <> ": expected " <> show expected <> ", got " <> show actual)

assertValid :: String -> Triangulation mode vertex directed undirected face -> IO ()
assertValid label triangulation =
  case validateTriangulation triangulation of
    [] -> pure ()
    violations -> fail (label <> " invariant violations: " <> show violations)

integerPoint :: Integer -> Integer -> ExactPoint
integerPoint x y = exactPoint (fromInteger x) (fromInteger y)

rectangleLoop :: Integer -> Integer -> Integer -> Integer -> IO ExactLoop
rectangleLoop minimumX minimumY maximumX maximumY =
  requireRight
    "rectangle loop"
    ( exactLoop
        ( integerPoint minimumX minimumY
            :| [ integerPoint maximumX minimumY
               , integerPoint maximumX maximumY
               , integerPoint minimumX maximumY
               ]
        )
    )

rectangleComponent
  :: Integer
  -> Integer
  -> Integer
  -> Integer
  -> IO PolygonComponent
rectangleComponent minimumX minimumY maximumX maximumY =
  rectangleLoop minimumX minimumY maximumX maximumY
    >>= requireRight "rectangle component" . (`polygonComponent` [])

requireJust :: String -> Maybe value -> IO value
requireJust _ (Just value) = pure value
requireJust label Nothing = fail (label <> ": expected Just")

randomPoints :: Word64 -> Int -> [Point]
randomPoints seed count = take count (go seed Set.empty)
 where
  go :: Word64 -> Set.Set (Point) -> [Point]
  go state seen =
    let state1 = lcg state
        state2 = lcg state1
        x = unit state1 * 2 - 1
        y = unit state2 * 2 - 1
        point = Point x y
     in if Set.member point seen
          then go state2 seen
          else point : go state2 (Set.insert point seen)

  unit :: Word64 -> Double
  unit value = fromIntegral (value `mod` 9_007_199_254_740_881) / 9_007_199_254_740_881

  lcg :: Word64 -> Word64
  lcg value = value * 6_364_136_223_846_793_005 + 1_442_695_040_888_963_407

rectangleRegion :: Integer -> Integer -> Integer -> Integer -> IO PlanarRegion
rectangleRegion minimumX minimumY maximumX maximumY =
  rectangleComponent minimumX minimumY maximumX maximumY
    >>= requireRight "rectangle region" . planarRegion . (: [])

polygonComponentOf :: [(Integer, Integer)] -> IO PolygonComponent
polygonComponentOf coordinates =
  case map (uncurry integerPoint) coordinates of
    firstPoint : secondPoint : thirdPoint : remaining -> do
      loop <-
        requireRight
          "polygon loop"
          (exactLoop (firstPoint :| (secondPoint : thirdPoint : remaining)))
      requireRight "polygon component" (polygonComponent loop [])
    _ -> fail "polygon fixture requires at least three points"

polygonRegion :: [(Integer, Integer)] -> IO PlanarRegion
polygonRegion coordinates =
  polygonComponentOf coordinates
    >>= requireRight "polygon region" . planarRegion . (: [])

annulusRegion
  :: (Integer, Integer, Integer, Integer)
  -> (Integer, Integer, Integer, Integer)
  -> IO PlanarRegion
annulusRegion
  (outerMinX, outerMinY, outerMaxX, outerMaxY)
  (holeMinX, holeMinY, holeMaxX, holeMaxY) = do
    outer <- rectangleLoop outerMinX outerMinY outerMaxX outerMaxY
    hole <-
      requireRight
        "annulus hole loop"
        ( exactLoop
            ( integerPoint holeMinX holeMinY
                :| [ integerPoint holeMinX holeMaxY
                   , integerPoint holeMaxX holeMaxY
                   , integerPoint holeMaxX holeMinY
                   ]
            )
        )
    component <- requireRight "annulus component" (polygonComponent outer [hole])
    requireRight "annulus region" (planarRegion [component])