packages feed

moonlight-planar-1.1.0.0: test/curve/Moonlight/Planar/AffineSpec.hs

-- | Exact action laws and witness-preserving polygon transport.
module Moonlight.Planar.AffineSpec (tests) where

import Data.Foldable (traverse_)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import Moonlight.Planar.Affine
import Moonlight.Planar.Convex (convexPolygon, convexPolygonPoints)
import Moonlight.Planar.Exact
  ( ExactPoint
  , ExactRational
  , ExactVector (..)
  , exactHalf
  , exactPoint
  , exactThird
  , translateExactPoint
  )
import Moonlight.Planar.Region
  ( exactLoop
  , exactLoopPoints
  , planarRegion
  , planarRegionComponents
  , polygonComponent
  , polygonHoleLoops
  , polygonOuterLoop
  , regionPointLocation
  )
import Support (assertEqual, requireRight)

tests :: IO ()
tests = sequence_
  [ testAffineActions
  , testIsoAdmission
  , testIsoInverse
  , testTranslationIso
  , testRegionTransport
  , testConvexTransport
  , putStrLn "affine: ok"
  ]

reflection :: Affine2
reflection = affine2 (ExactVector (-1) 0) (ExactVector 0 1) (ExactVector 7 3)

shear :: Affine2
shear = affine2 (ExactVector 2 1) (ExactVector 1 1) (ExactVector (-4) 5)

collapse :: Affine2
collapse = affine2 (ExactVector 1 2) (ExactVector 2 4) (ExactVector 3 7)

testAffineActions :: IO ()
testAffineActions = do
  let transforms = [identityAffine2, reflection, shear, collapse]
      point = exactPoint 3 (-2)
      vector = ExactVector (-1) 4
  traverse_
    (\transform -> do
       assertEqual "left affine identity" transform (composeAffine2 identityAffine2 transform)
       assertEqual "right affine identity" transform (composeAffine2 transform identityAffine2)
       assertEqual "affine preserves point displacement"
         (transformPoint transform (translateExactPoint point vector))
         (translateExactPoint (transformPoint transform point) (transformVector transform vector)))
    transforms
  traverse_
    (\(outer, inner) -> do
       let composed = composeAffine2 outer inner
       assertEqual "point composition order"
         (transformPoint outer (transformPoint inner point))
         (transformPoint composed point)
       assertEqual "vector composition order"
         (transformVector outer (transformVector inner vector))
         (transformVector composed vector)
       assertEqual "determinants multiply"
         (affineDeterminant outer * affineDeterminant inner)
         (affineDeterminant composed))
    [(outer, inner) | outer <- transforms, inner <- transforms]
  traverse_
    (\(a, b, c) -> assertEqual "affine composition associativity"
      (composeAffine2 (composeAffine2 a b) c)
      (composeAffine2 a (composeAffine2 b c)))
    [(a, b, c) | a <- transforms, b <- transforms, c <- transforms]
  assertEqual "singular point action is still lawful"
    (exactPoint 2 5) (transformPoint collapse point)
  assertEqual "column observation"
    (ExactVector 2 1, ExactVector 1 1, ExactVector (-4) 5)
    (affineColumns shear)

testTranslationIso :: IO ()
testTranslationIso = do
  let offset = ExactVector exactThird (-7)
      other = ExactVector 4 exactHalf
      translation = translationAffineIso2 offset
      point = exactPoint 2 3
      vector = ExactVector 5 6
  assertEqual "zero translation is identity" identityAffineIso2
    (translationAffineIso2 (ExactVector 0 0))
  assertEqual "translation moves points" (translateExactPoint point offset)
    (transformPoint (affineIsoMap translation) point)
  assertEqual "translation leaves vectors unchanged" vector
    (transformVector (affineIsoMap translation) vector)
  assertEqual "translation determinant" 1 (affineDeterminant (affineIsoMap translation))
  assertEqual "translation inverse" (translationAffineIso2 (ExactVector (negate exactThird) 7))
    (inverseAffineIso2 translation)
  assertEqual "translation composition"
    (translationAffineIso2 (ExactVector (4 + exactThird) (exactHalf - 7)))
    (composeAffineIso2 translation (translationAffineIso2 other))

testIsoAdmission :: IO ()
testIsoAdmission = do
  assertEqual "singular map cannot transport topology" Nothing (affineIso2 collapse)
  assertEqual "identity iso admission" (Just identityAffineIso2) (affineIso2 identityAffine2)
  reversing <- requireIso reflection
  preserving <- requireIso shear
  traverse_
    (\(outer, inner) -> assertEqual "iso composition agrees with checked admission"
      (affineIso2 (composeAffine2 (affineIsoMap outer) (affineIsoMap inner)))
      (Just (composeAffineIso2 outer inner)))
    [(outer, inner) | outer <- [reversing, preserving], inner <- [reversing, preserving]]

testIsoInverse :: IO ()
testIsoInverse = do
  let rationalFrame = affine2
        (ExactVector 2 1) (ExactVector exactThird exactHalf) (ExactVector 5 (-3))
      point = exactPoint exactHalf (negate exactThird)
      vector = ExactVector exactThird 7
  frames <- traverse requireIso
    [identityAffine2, reflection, shear, rationalFrame, composeAffine2 reflection rationalFrame]
  traverse_
    (\frame -> do
       let inverse = inverseAffineIso2 frame
       assertEqual "inverse on the left" identityAffineIso2 (composeAffineIso2 inverse frame)
       assertEqual "inverse on the right" identityAffineIso2 (composeAffineIso2 frame inverse)
       assertEqual "inverse involution" frame (inverseAffineIso2 inverse)
       assertEqual "inverse agrees with checked admission"
         (Just inverse) (affineIso2 (affineIsoMap inverse))
       assertEqual "point inverse roundtrip" point
         (transformPoint (affineIsoMap inverse) (transformPoint (affineIsoMap frame) point))
       assertEqual "vector inverse roundtrip" vector
         (transformVector (affineIsoMap inverse) (transformVector (affineIsoMap frame) vector)))
    frames
  traverse_
    (\(outer, inner) -> assertEqual "inverse reverses composition order"
      (composeAffineIso2 (inverseAffineIso2 inner) (inverseAffineIso2 outer))
      (inverseAffineIso2 (composeAffineIso2 outer inner)))
    [(outer, inner) | outer <- frames, inner <- frames]

testRegionTransport :: IO ()
testRegionTransport = do
  outer <- requireRight "outer loop" (exactLoop (rectangle 0 0 8 8))
  holeLeft <- requireRight "left clockwise hole" (exactLoop (NonEmpty.reverse (rectangle 1 1 3 3)))
  holeRight <- requireRight "right clockwise hole" (exactLoop (NonEmpty.reverse (rectangle 5 1 7 3)))
  component <- requireRight "pierced component" (polygonComponent outer [holeLeft, holeRight])
  islandLoop <- requireRight "separate island" (exactLoop (rectangle 10 0 12 2))
  island <- requireRight "island component" (polygonComponent islandLoop [])
  source <- requireRight "disjoint region" (planarRegion [component, island])
  reversing <- requireIso reflection
  preserving <- requireIso shear
  assertEqual "region transport identity" source (transformPlanarRegion identityAffineIso2 source)
  traverse_
    (\iso -> do
       let mapped = transformPlanarRegion iso source
           transform = affineIsoMap iso
           expectedLoop loop = exactLoop
             ((if affineDeterminant transform < 0 then NonEmpty.reverse else id)
               (fmap (transformPoint transform) (exactLoopPoints loop)))
       checkedComponents <- traverse
         (\original -> do
           checkedOuter <- requireRight "mapped outer admission" (expectedLoop (polygonOuterLoop original))
           checkedHoles <- traverse
             (requireRight "mapped hole admission" . expectedLoop)
             (polygonHoleLoops original)
           requireRight "mapped component admission" (polygonComponent checkedOuter checkedHoles))
         (planarRegionComponents source)
       checked <- requireRight "mapped region admission" (planarRegion checkedComponents)
       assertEqual "transport equals checked canonical region" checked mapped
       assertEqual "inverse restores canonical region with sorted holes and components" source
         (transformPlanarRegion (inverseAffineIso2 iso) mapped)
       traverse_
         (\query -> assertEqual "point location commutes with iso transport"
           (regionPointLocation source query)
           (regionPointLocation mapped (transformPoint transform query)))
         [exactPoint 0 0, exactPoint 1 1, exactPoint 1 2, exactPoint 2 2, exactPoint 4 4, exactPoint 9 9, exactPoint 11 1])
    [reversing, preserving]
  assertEqual "canonical region action composition"
    (transformPlanarRegion reversing (transformPlanarRegion preserving source))
    (transformPlanarRegion (composeAffineIso2 reversing preserving) source)

testConvexTransport :: IO ()
testConvexTransport = do
  source <- requireRight "convex rectangle" (convexPolygon (rectangle 0 0 4 3))
  reversing <- requireIso reflection
  let transformed = transformConvexPolygon reversing source
  expected <- requireRight "reflected convex admission"
    (convexPolygon (NonEmpty.reverse (fmap (transformPoint reflection) (convexPolygonPoints source))))
  assertEqual "reflection retains strict counterclockwise convexity" expected transformed
  assertEqual "convex transport identity" source (transformConvexPolygon identityAffineIso2 source)
  assertEqual "convex inverse restores canonical cycle" source
    (transformConvexPolygon (inverseAffineIso2 reversing) transformed)

rectangle :: ExactRational -> ExactRational -> ExactRational -> ExactRational -> NonEmpty ExactPoint
rectangle x0 y0 x1 y1 =
  exactPoint x0 y0 :| [exactPoint x1 y0, exactPoint x1 y1, exactPoint x0 y1]

requireIso :: Affine2 -> IO AffineIso2
requireIso = maybe (fail "fixture affine map is singular") pure . affineIso2