packages feed

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

-- | Exact planar affine actions. General maps may collapse authored geometry;
-- only the non-singular refinement transports admitted polygon topology.
module Moonlight.Planar.Affine
  ( Affine2
  , affine2
  , affineColumns
  , identityAffine2
  , composeAffine2
  , transformPoint
  , transformVector
  , affineDeterminant
  , AffineIso2
  , affineIso2
  , affineIsoMap
  , identityAffineIso2
  , translationAffineIso2
  , composeAffineIso2
  , inverseAffineIso2
  , transformExactLoop
  , transformPolygonComponent
  , transformPlanarRegion
  , transformConvexPolygon
  ) where

import Control.DeepSeq (NFData (..))
import Data.List (sort)
import qualified Data.List.NonEmpty as NonEmpty
import Moonlight.Planar.Exact
  ( ExactPoint
  , ExactRational
  , ExactVector (..)
  , PositiveExact
  , addExactVectors
  , exactCross
  , exactPoint
  , exactPointCoordinates
  , divideByPositive
  , multiplyPositive
  , positiveExact
  , positiveOne
  , ratioPositive
  , translateExactPoint
  )
import Moonlight.Planar.Internal.BoundaryCycle (rotateCycleLeast)
import Moonlight.Planar.Internal.Region.Types
  ( ConvexPolygon (..)
  , ExactLoop (..)
  , PlanarRegion (..)
  , PolygonComponent (..)
  )

-- | Two linear columns followed by translation. No invertibility is assumed.
data Affine2 = Affine2 !ExactVector !ExactVector !ExactVector
  deriving stock (Eq, Ord, Show)

instance NFData Affine2 where
  rnf (Affine2 x y offset) = rnf x `seq` rnf y `seq` rnf offset

affine2 :: ExactVector -> ExactVector -> ExactVector -> Affine2
affine2 = Affine2

affineColumns :: Affine2 -> (ExactVector, ExactVector, ExactVector)
affineColumns (Affine2 x y offset) = (x, y, offset)

identityAffine2 :: Affine2
identityAffine2 = Affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector 0 0)

-- | @composeAffine2 outer inner@ acts as @outer . inner@.
composeAffine2 :: Affine2 -> Affine2 -> Affine2
composeAffine2 outer@(Affine2 _ _ offset) (Affine2 x y innerOffset) =
  Affine2
    (transformVector outer x)
    (transformVector outer y)
    (addExactVectors (transformVector outer innerOffset) offset)

-- | Translation does not act on vectors.
transformVector :: Affine2 -> ExactVector -> ExactVector
transformVector (Affine2 (ExactVector a b) (ExactVector c d) _) (ExactVector x y) =
  ExactVector (a * x + c * y) (b * x + d * y)

transformPoint :: Affine2 -> ExactPoint -> ExactPoint
transformPoint transform@(Affine2 _ _ offset) point =
  let (x, y) = exactPointCoordinates point
      ExactVector mappedX mappedY = transformVector transform (ExactVector x y)
   in translateExactPoint (exactPoint mappedX mappedY) offset

affineDeterminant :: Affine2 -> ExactRational
affineDeterminant (Affine2 x y _) = exactCross x y

data Orientation = Preserving | Reversing
  deriving stock (Eq, Ord, Show)

-- | A nonzero determinant, including its sign and positive magnitude, admitted
-- once. Inversion consumes the same divisor witness without readmission.
data AffineIso2 = AffineIso2 !Affine2 !Orientation !PositiveExact
  deriving stock (Eq, Ord, Show)

instance NFData AffineIso2 where
  rnf (AffineIso2 transform orientation magnitude) =
    rnf transform `seq` orientation `seq` rnf magnitude

affineIso2 :: Affine2 -> Maybe AffineIso2
affineIso2 transform =
  let determinant = affineDeterminant transform
      orientation = if determinant < 0 then Reversing else Preserving
   in case positiveExact (abs determinant) of
        Left _ -> Nothing
        Right magnitude -> Just (AffineIso2 transform orientation magnitude)

affineIsoMap :: AffineIso2 -> Affine2
affineIsoMap (AffineIso2 transform _ _) = transform

identityAffineIso2 :: AffineIso2
identityAffineIso2 = AffineIso2 identityAffine2 Preserving positiveOne

-- | Translation preserves the identity linear frame. Its determinant is one
-- by construction, so no dynamic admission is required for layout or ports.
translationAffineIso2 :: ExactVector -> AffineIso2
translationAffineIso2 offset = AffineIso2
  (Affine2 (ExactVector 1 0) (ExactVector 0 1) offset) Preserving positiveOne

-- | Composition preserves invertibility; orientation signs multiply.
composeAffineIso2 :: AffineIso2 -> AffineIso2 -> AffineIso2
composeAffineIso2 (AffineIso2 outer outerOrientation outerMagnitude) (AffineIso2 inner innerOrientation innerMagnitude) =
  AffineIso2
    (composeAffine2 outer inner)
    (composeOrientation outerOrientation innerOrientation)
    (multiplyPositive outerMagnitude innerMagnitude)
 where
  composeOrientation :: Orientation -> Orientation -> Orientation
  composeOrientation Preserving orientation = orientation
  composeOrientation Reversing Preserving = Reversing
  composeOrientation Reversing Reversing = Preserving

-- | Exact inverse using the admitted determinant magnitude as divisor. The
-- inverse has the same orientation and reciprocal determinant magnitude.
inverseAffineIso2 :: AffineIso2 -> AffineIso2
inverseAffineIso2
  (AffineIso2 (Affine2 (ExactVector a b) (ExactVector c d) (ExactVector x y)) orientation magnitude) =
    AffineIso2
      (Affine2
        (ExactVector (divide d) (divide (negate b)))
        (ExactVector (divide (negate c)) (divide a))
        (ExactVector (divide (c * y - d * x)) (divide (b * x - a * y))))
      orientation
      (ratioPositive positiveOne magnitude)
 where
  divide :: ExactRational -> ExactRational
  divide numerator = divideByPositive (orient numerator) magnitude
  orient :: ExactRational -> ExactRational
  orient = case orientation of
    Preserving -> id
    Reversing -> negate

-- | Preserve the source winding and canonical least starting vertex. This
-- polygonal action intentionally differs from traversal-preserving curve maps.
transformExactLoop :: AffineIso2 -> ExactLoop -> ExactLoop
transformExactLoop (AffineIso2 transform orientation _) (ExactLoop points) =
  ExactLoop (rotateCycleLeast (orient (fmap (transformPoint transform) points)))
 where
  orient :: NonEmpty.NonEmpty ExactPoint -> NonEmpty.NonEmpty ExactPoint
  orient = case orientation of
    Preserving -> id
    Reversing -> NonEmpty.reverse

-- | Invertibility preserves boundary separation and hole containment.
transformPolygonComponent :: AffineIso2 -> PolygonComponent -> PolygonComponent
transformPolygonComponent transform (PolygonComponent outer holes) =
  PolygonComponent
    (transformExactLoop transform outer)
    (sort (fmap (transformExactLoop transform) holes))

-- | Disjoint interiors remain disjoint; transformed canonical order may differ.
transformPlanarRegion :: AffineIso2 -> PlanarRegion -> PlanarRegion
transformPlanarRegion transform (PlanarRegion components) =
  PlanarRegion (sort (fmap (transformPolygonComponent transform) components))

-- | Strict convexity survives every non-singular affine map.
transformConvexPolygon :: AffineIso2 -> ConvexPolygon -> ConvexPolygon
transformConvexPolygon transform (ConvexPolygon loop) =
  ConvexPolygon (transformExactLoop transform loop)