moonlight-triangulation-1.2.0.0: src-dcel/Moonlight/Triangulation/Exact.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
-- | Exact rational planar geometry over admitted binary64 points.
module Moonlight.Triangulation.Exact
( ExactPoint
, exactPoint
, exactPointCoordinates
, exactPointCross
, ExactVector (..)
, exactVectorFromPoints
, addExactVectors
, exactCross
, compareExactVectorAngle
, translateExactPoint
, ExactSegment
, ExactGeometryError (..)
, exactSegment
, exactSegmentEndpoints
, exactPointFromPoint
, exactPointFromQueryPoint
, exactPointToEmbeddingCandidate
, exactOrient2d
, exactOnClosedSegment
, SegmentRelation (..)
, allSegmentRelations
, exactSegmentRelation
, ExactIntersectionError (..)
, exactLineIntersection
, exactSupportingLineIntersection
) where
import Control.DeepSeq (NFData)
import GHC.Generics (Generic)
import Moonlight.Triangulation.Internal.Dyadic (integerRatioToDouble)
import Moonlight.Triangulation.Internal.ExactRational
( ExactArithmeticError (..)
, ExactRational
, exactDivide
, exactRationalDenominator
, exactRationalFromFiniteDouble
, exactRationalNumerator
, exactSignum
)
import Moonlight.Triangulation.Internal.SegmentRelation
( SegmentRelation (..)
, allSegmentRelations
, segmentRelationWith
)
import Moonlight.Triangulation.Math (mkQueryPoint)
import Moonlight.Triangulation.Types
( Point (..)
, PointValidationError
, QueryPoint
, queryPointValue
)
-- | A strict exact Cartesian point.
data ExactPoint = ExactPoint !ExactRational !ExactRational
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | A strict exact segment whose endpoints are distinct.
data ExactSegment = ExactSegment !ExactPoint !ExactPoint
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Witness-bearing refusals from exact segment construction.
data ExactGeometryError
= ExactSegmentEndpointsCoincide !ExactPoint
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Witness-bearing refusals from exact line intersection.
data ExactIntersectionError
= ExactIntersectionAbsent !SegmentRelation
| ExactIntersectionNonUnique !SegmentRelation
| ExactIntersectionParallelOrDegenerate !ExactRational
| ExactIntersectionArithmetic !ExactArithmeticError
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Construct an exact point from two exact coordinates.
exactPoint :: ExactRational -> ExactRational -> ExactPoint
exactPoint = ExactPoint
{-# INLINE exactPoint #-}
-- | Read both exact point coordinates.
exactPointCoordinates :: ExactPoint -> (ExactRational, ExactRational)
exactPointCoordinates (ExactPoint x y) = (x, y)
{-# INLINE exactPointCoordinates #-}
-- | Determinant of two points regarded as vectors from the Cartesian origin.
exactPointCross :: ExactPoint -> ExactPoint -> ExactRational
exactPointCross (ExactPoint ax ay) (ExactPoint bx by) = ax * by - ay * bx
{-# INLINE exactPointCross #-}
-- | Construct an exact segment, refusing coincident endpoints with their
-- shared point as the witness.
exactSegment
:: ExactPoint
-> ExactPoint
-> Either ExactGeometryError ExactSegment
exactSegment from to
| from == to = Left (ExactSegmentEndpointsCoincide from)
| otherwise = Right (ExactSegment from to)
-- | Read both distinct exact segment endpoints.
exactSegmentEndpoints :: ExactSegment -> (ExactPoint, ExactPoint)
exactSegmentEndpoints (ExactSegment from to) = (from, to)
{-# INLINE exactSegmentEndpoints #-}
-- | Validate and exactly embed a raw binary64 point.
exactPointFromPoint :: Point -> Either PointValidationError ExactPoint
exactPointFromPoint = fmap exactPointFromQueryPoint . mkQueryPoint
-- | Exactly embed an already-admitted query point without repeating
-- coordinate validation.
exactPointFromQueryPoint :: QueryPoint -> ExactPoint
exactPointFromQueryPoint queryPoint =
case queryPointValue queryPoint of
Point x y ->
ExactPoint
(exactRationalFromFiniteDouble x)
(exactRationalFromFiniteDouble y)
{-# INLINE exactPointFromQueryPoint #-}
-- | Deterministically project an exact point to a validated binary64
-- embedding candidate. This is not a correctly-rounded nearest-double claim;
-- callers must certify the candidate projection before relying on it.
exactPointToEmbeddingCandidate
:: ExactPoint
-> Either PointValidationError Point
exactPointToEmbeddingCandidate (ExactPoint x y) =
queryPointValue
<$> mkQueryPoint
( Point
(integerRatioToDouble (exactRationalNumerator x) (exactRationalDenominator x))
(integerRatioToDouble (exactRationalNumerator y) (exactRationalDenominator y))
)
-- | Exact orientation of an ordered triple. 'GT' is a positive determinant
-- and counter-clockwise turn, 'EQ' is collinear, and 'LT' is clockwise.
exactOrient2d :: ExactPoint -> ExactPoint -> ExactPoint -> Ordering
exactOrient2d
(ExactPoint ax ay)
(ExactPoint bx by)
(ExactPoint cx cy) =
exactSignum
((bx - ax) * (cy - ay) - (by - ay) * (cx - ax))
{-# INLINE exactOrient2d #-}
-- | Whether an exact point lies on an exact closed segment.
exactOnClosedSegment :: ExactPoint -> ExactPoint -> ExactPoint -> Bool
exactOnClosedSegment
from@(ExactPoint ax ay)
to@(ExactPoint bx by)
query@(ExactPoint qx qy) =
exactOrient2d from to query == EQ
&& qx >= min ax bx
&& qx <= max ax bx
&& qy >= min ay by
&& qy <= max ay by
{-# INLINE exactOnClosedSegment #-}
-- | Exact rational specialization of the one closed-segment relation policy.
exactSegmentRelation
:: ExactPoint
-> ExactPoint
-> ExactPoint
-> ExactPoint
-> SegmentRelation
exactSegmentRelation =
segmentRelationWith (==) compare exactOrient2d exactOnClosedSegment
{-# INLINE exactSegmentRelation #-}
-- | Return the unique exact intersection of two exact segments. Disjoint and
-- non-unique relations are refused with their relation witness; a zero line
-- cross product and arithmetic failure retain their exact witnesses.
exactLineIntersection
:: ExactSegment
-> ExactSegment
-> Either ExactIntersectionError ExactPoint
exactLineIntersection
(ExactSegment a b)
(ExactSegment c d) =
case exactSegmentRelation a b c d of
SegmentsDisjoint -> Left (ExactIntersectionAbsent SegmentsDisjoint)
SegmentsDuplicate -> Left (ExactIntersectionNonUnique SegmentsDuplicate)
SegmentsCollinearlyOverlap ->
Left (ExactIntersectionNonUnique SegmentsCollinearlyOverlap)
SegmentsShareEndpoint -> uniqueIntersection
SegmentsProperlyCross -> uniqueIntersection
SegmentEndpointTouchesInterior -> uniqueIntersection
where
uniqueIntersection =
exactSupportingLineIntersection (ExactSegment a b) (ExactSegment c d)
-- | Intersect the infinite supporting lines of two admitted exact segments.
-- Unlike 'exactLineIntersection', the intersection need not lie inside either
-- closed segment. Parallel supporting lines retain the exact zero denominator
-- witness.
exactSupportingLineIntersection
:: ExactSegment
-> ExactSegment
-> Either ExactIntersectionError ExactPoint
exactSupportingLineIntersection
(ExactSegment a b)
(ExactSegment c d) =
let directionAB = exactVectorFromPoints a b
directionCD = exactVectorFromPoints c d
fromAToC = exactVectorFromPoints a c
denominator = exactCross directionAB directionCD
numerator = exactCross fromAToC directionCD
in case exactDivide numerator denominator of
Left ExactZeroDivisor ->
Left (ExactIntersectionParallelOrDegenerate denominator)
Left arithmeticError -> Left (ExactIntersectionArithmetic arithmeticError)
Right parameter ->
Right (translateExactPoint a (scaleExactVector parameter directionAB))
-- | A strict exact displacement vector. Points and vectors remain distinct;
-- all exact planar algorithms share this single vector carrier.
data ExactVector = ExactVector !ExactRational !ExactRational
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
exactVectorFromPoints :: ExactPoint -> ExactPoint -> ExactVector
exactVectorFromPoints (ExactPoint ax ay) (ExactPoint bx by) =
ExactVector (bx - ax) (by - ay)
addExactVectors :: ExactVector -> ExactVector -> ExactVector
addExactVectors (ExactVector ax ay) (ExactVector bx by) =
ExactVector (ax + bx) (ay + by)
exactCross :: ExactVector -> ExactVector -> ExactRational
exactCross (ExactVector ax ay) (ExactVector bx by) =
ax * by - ay * bx
-- | Counter-clockwise angular order from the positive x-axis. Collinear
-- vectors on the same ray compare equal so convolution can merge them;
-- callers that need a total point order may add their own radial tie-break.
compareExactVectorAngle :: ExactVector -> ExactVector -> Ordering
compareExactVectorAngle left right =
case compare (vectorHalf left) (vectorHalf right) of
EQ ->
case exactSignum (exactCross left right) of
GT -> LT
LT -> GT
EQ -> EQ
ordering -> ordering
where
vectorHalf (ExactVector x y)
| exactSignum y == GT = False
| exactSignum y == EQ && exactSignum x /= LT = False
| otherwise = True
{-# INLINE compareExactVectorAngle #-}
scaleExactVector :: ExactRational -> ExactVector -> ExactVector
scaleExactVector scale (ExactVector x y) =
ExactVector (scale * x) (scale * y)
translateExactPoint :: ExactPoint -> ExactVector -> ExactPoint
translateExactPoint (ExactPoint x y) (ExactVector dx dy) =
ExactPoint (x + dx) (y + dy)