moonlight-triangulation-1.4.0.4: 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
, exactPointBitWidth
, ExactAffineLine
, ExactHalfPlaneError (..)
, exactAffineLine
, exactAffineLineCoefficients
, oppositeExactAffineLine
, exactAffineLineIntersection
, ExactClosedHalfPlane
, exactClosedHalfPlane
, exactClosedHalfPlaneFromDirectedEdge
, exactClosedHalfPlaneLine
, classifyExactPoint
, ExactRetainedPolygon
, exactRetainedPolygon
, exactRetainedPolygonPoints
, ExactClipDisposition (..)
, ExactClipError (..)
, ExactClipReceipt (..)
, exactClipRetainedPolygon
, ExactVector (..)
, exactVectorFromPoints
, addExactVectors
, exactCross
, compareExactVectorAngle
, translateExactPoint
, ExactRay
, exactRay
, exactRayOrigin
, exactRayDirection
, ExactSegment
, ExactGeometryError (..)
, exactSegment
, exactSegmentEndpoints
, exactPointFromPoint
, exactPointFromQueryPoint
, exactPointToEmbeddingCandidate
, exactOrient2d
, exactOnClosedSegment
, SegmentRelation (..)
, allSegmentRelations
, exactSegmentRelation
, ExactIntersectionError (..)
, exactLineIntersection
, exactSupportingLineIntersection
) where
import Control.DeepSeq (NFData)
import Control.Monad (foldM)
import qualified Data.List as List
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import Data.Maybe (mapMaybe)
import qualified Data.Sequence as Sequence
import Data.Sequence (Seq, ViewL (..), ViewR (..), (|>))
import GHC.Generics (Generic)
import Moonlight.Triangulation.Internal.Dyadic (integerRatioToDouble)
import Moonlight.Triangulation.Internal.BoundaryCycle
( admitsSimpleCycleEdgeRelation
, cyclePairsNonEmpty
, firstNonCounterClockwiseTurn
, unorderedPairs
)
import Moonlight.Triangulation.Internal.ExactRational
( ExactArithmeticError (..)
, ExactRational
, exactDivide
, exactRational
, exactRationalBitWidth
, exactRationalDenominator
, exactRationalDenominatorBitWidth
, exactRationalFromFiniteDouble
, exactRationalIsZero
, 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)
-- | An exact affine line @a*x + b*y + c = 0@ with a nonzero normal.
data ExactAffineLine =
ExactAffineLine !ExactRational !ExactRational !ExactRational
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | The closed left half-plane of an oriented affine line. A point belongs
-- when the line evaluation is nonnegative.
newtype ExactClosedHalfPlane = ExactClosedHalfPlane ExactAffineLine
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | One polygon vertex together with the original affine line supporting its
-- incoming edge. Intersections therefore never reconstruct a line from
-- already-derived endpoints.
data ExactRetainedVertex =
ExactRetainedVertex !ExactPoint !ExactAffineLine
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | A strict counter-clockwise convex polygon whose incoming edges retain
-- their original supporting equations.
newtype ExactRetainedPolygon =
ExactRetainedPolygon (NonEmpty ExactRetainedVertex)
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Typed refusals from affine-line and retained-polygon admission.
data ExactHalfPlaneError
= ExactAffineLineZeroNormal
!ExactRational
!ExactRational
!ExactRational
| ExactRetainedPolygonTooFewVertices !Int
| ExactRetainedPolygonNonConvexTurn !Int !Ordering
| ExactRetainedPolygonSelfRelation !Int !Int !SegmentRelation
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Dimensional disposition after exact closed-half-plane clipping.
data ExactClipDisposition
= ExactClipFullDimensional !ExactRetainedPolygon
| ExactClipLowerDimensional !(NonEmpty ExactPoint)
| ExactClipEmpty
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Typed obstructions from an affine intersection that should be unique or
-- from an impossible orientation reversal during convex descent.
data ExactClipError
= ExactClipIntersection !ExactIntersectionError
| ExactClipOrientationReversed !ExactRational
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Structural work and exact-rational width observations for one angular
-- half-plane descent. Submitted half-planes exclude the domain; active
-- boundaries include domain edges admitted to the deque after
-- equal-direction coalescence; zero records a single-plane domain rejection
-- before deque construction.
-- Compatibility checks count deque endpoint predicates; intersections count
-- successful affine-line intersections actually evaluated. Widths describe
-- observed reduced values, not bounds.
data ExactClipReceipt = ExactClipReceipt
{ exactClipSubmittedHalfPlanes :: !Int
, exactClipActiveBoundaries :: !Int
, exactClipBoundaryCompatibilityChecks :: !Int
, exactClipExactIntersections :: !Int
, exactClipInputCoordinateBits :: !Int
, exactClipMaximumAffineCoefficientBits :: !Int
, exactClipPeakIntermediateCoordinateBits :: !Int
, exactClipFinalCoordinateBits :: !Int
, exactClipFinalDenominatorBits :: !Int
}
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
instance Semigroup ExactClipReceipt where
left <> right =
ExactClipReceipt
{ exactClipSubmittedHalfPlanes =
exactClipSubmittedHalfPlanes left + exactClipSubmittedHalfPlanes right
, exactClipActiveBoundaries =
exactClipActiveBoundaries left + exactClipActiveBoundaries right
, exactClipBoundaryCompatibilityChecks =
exactClipBoundaryCompatibilityChecks left
+ exactClipBoundaryCompatibilityChecks right
, exactClipExactIntersections =
exactClipExactIntersections left + exactClipExactIntersections right
, exactClipInputCoordinateBits =
max (exactClipInputCoordinateBits left) (exactClipInputCoordinateBits right)
, exactClipMaximumAffineCoefficientBits =
max
(exactClipMaximumAffineCoefficientBits left)
(exactClipMaximumAffineCoefficientBits right)
, exactClipPeakIntermediateCoordinateBits =
max
(exactClipPeakIntermediateCoordinateBits left)
(exactClipPeakIntermediateCoordinateBits right)
, exactClipFinalCoordinateBits =
max (exactClipFinalCoordinateBits left) (exactClipFinalCoordinateBits right)
, exactClipFinalDenominatorBits =
max (exactClipFinalDenominatorBits left) (exactClipFinalDenominatorBits right)
}
instance Monoid ExactClipReceipt where
mempty = ExactClipReceipt 0 0 0 0 0 0 0 0 0
-- | A strict exact segment whose endpoints are distinct.
data ExactSegment = ExactSegment !ExactPoint !ExactPoint
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | An exact half-line with an admitted nonzero direction.
data ExactRay = ExactRay !ExactPoint !ExactVector
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Witness-bearing refusals from exact segment construction.
data ExactGeometryError
= ExactSegmentEndpointsCoincide !ExactPoint
| ExactRayZeroDirection !ExactPoint
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | Admit a half-line, rejecting the only direction that cannot carry one.
exactRay
:: ExactPoint
-> ExactVector
-> Either ExactGeometryError ExactRay
exactRay originPoint direction@(ExactVector directionX directionY)
| exactRationalIsZero directionX && exactRationalIsZero directionY =
Left (ExactRayZeroDirection originPoint)
| otherwise = Right (ExactRay originPoint direction)
-- | Finite endpoint of an exact ray.
exactRayOrigin :: ExactRay -> ExactPoint
exactRayOrigin (ExactRay originPoint _) = originPoint
{-# INLINE exactRayOrigin #-}
-- | Nonzero direction of an exact ray.
exactRayDirection :: ExactRay -> ExactVector
exactRayDirection (ExactRay _ direction) = direction
{-# INLINE exactRayDirection #-}
-- | 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 #-}
-- | Admit an affine line, refusing precisely the zero normal. Coefficients
-- are retained verbatim because their source identity, rather than a chosen
-- scalar normalization, is the point of this carrier.
exactAffineLine
:: ExactRational
-> ExactRational
-> ExactRational
-> Either ExactHalfPlaneError ExactAffineLine
exactAffineLine coefficientX coefficientY constant
| exactRationalIsZero coefficientX
&& exactRationalIsZero coefficientY =
Left
( ExactAffineLineZeroNormal
coefficientX
coefficientY
constant
)
| otherwise =
Right (ExactAffineLine coefficientX coefficientY constant)
-- | Read the retained coefficients @(a,b,c)@ of @a*x+b*y+c=0@.
exactAffineLineCoefficients
:: ExactAffineLine
-> (ExactRational, ExactRational, ExactRational)
exactAffineLineCoefficients (ExactAffineLine coefficientX coefficientY constant) =
(coefficientX, coefficientY, constant)
{-# INLINE exactAffineLineCoefficients #-}
-- | Reverse the oriented normal without changing the geometric line.
-- Admission already proves the normal nonzero, so negation is total.
oppositeExactAffineLine :: ExactAffineLine -> ExactAffineLine
oppositeExactAffineLine (ExactAffineLine coefficientX coefficientY constant) =
ExactAffineLine (negate coefficientX) (negate coefficientY) (negate constant)
{-# INLINE oppositeExactAffineLine #-}
-- | Intersect two admitted affine lines directly from their retained source
-- coefficients. A parallel pair retains the exact determinant witness.
exactAffineLineIntersection
:: ExactAffineLine
-> ExactAffineLine
-> Either ExactIntersectionError ExactPoint
exactAffineLineIntersection
(ExactAffineLine firstX firstY firstConstant)
(ExactAffineLine secondX secondY secondConstant) =
-- Descend in homogeneous integer coordinates and normalize each published
-- coordinate once. Generic Ratio arithmetic would normalize every product
-- in Cramer's rule even though those intermediate rationals are invisible.
let (firstXNumerator, firstXDenominator) = exactRationalParts firstX
(firstYNumerator, firstYDenominator) = exactRationalParts firstY
(firstConstantNumerator, firstConstantDenominator) =
exactRationalParts firstConstant
(secondXNumerator, secondXDenominator) = exactRationalParts secondX
(secondYNumerator, secondYDenominator) = exactRationalParts secondY
(secondConstantNumerator, secondConstantDenominator) =
exactRationalParts secondConstant
determinantNumerator =
firstXNumerator
* secondYNumerator
* secondXDenominator
* firstYDenominator
- secondXNumerator
* firstYNumerator
* firstXDenominator
* secondYDenominator
xNumerator =
( firstYNumerator
* secondConstantNumerator
* secondYDenominator
* firstConstantDenominator
- secondYNumerator
* firstConstantNumerator
* firstYDenominator
* secondConstantDenominator
)
* firstXDenominator
* secondXDenominator
yNumerator =
( firstConstantNumerator
* secondXNumerator
* secondConstantDenominator
* firstXDenominator
- secondConstantNumerator
* firstXNumerator
* firstConstantDenominator
* secondXDenominator
)
* firstYDenominator
* secondYDenominator
coordinateDenominator =
determinantNumerator
* firstConstantDenominator
* secondConstantDenominator
in if determinantNumerator == 0
then Left (ExactIntersectionParallelOrDegenerate 0)
else do
x <- admitCoordinate xNumerator coordinateDenominator
y <- admitCoordinate yNumerator coordinateDenominator
pure (ExactPoint x y)
where
admitCoordinate numerator denominator =
case exactRational numerator denominator of
Left arithmeticError ->
Left (ExactIntersectionArithmetic arithmeticError)
Right coordinate -> Right coordinate
exactRationalParts :: ExactRational -> (Integer, Integer)
exactRationalParts value =
(exactRationalNumerator value, exactRationalDenominator value)
{-# INLINE exactRationalParts #-}
-- | Orient an admitted affine line so its nonnegative side is retained.
exactClosedHalfPlane :: ExactAffineLine -> ExactClosedHalfPlane
exactClosedHalfPlane = ExactClosedHalfPlane
{-# INLINE exactClosedHalfPlane #-}
-- | Construct the closed half-plane to the left of a directed edge.
exactClosedHalfPlaneFromDirectedEdge
:: ExactPoint
-> ExactPoint
-> Either ExactHalfPlaneError ExactClosedHalfPlane
exactClosedHalfPlaneFromDirectedEdge
(ExactPoint fromX fromY)
(ExactPoint toX toY) =
exactClosedHalfPlane
<$> exactAffineLine
(fromY - toY)
(toX - fromX)
(toY * fromX - toX * fromY)
-- | Read the original affine boundary retained by a closed half-plane.
exactClosedHalfPlaneLine :: ExactClosedHalfPlane -> ExactAffineLine
exactClosedHalfPlaneLine (ExactClosedHalfPlane line) = line
{-# INLINE exactClosedHalfPlaneLine #-}
-- | Classify a point against a closed half-plane. 'GT' is strict interior,
-- 'EQ' lies on the boundary, and 'LT' is exterior.
classifyExactPoint :: ExactClosedHalfPlane -> ExactPoint -> Ordering
classifyExactPoint
(ExactClosedHalfPlane (ExactAffineLine coefficientX coefficientY constant))
(ExactPoint x y) =
-- The common denominator is strictly positive, so its unnormalized integer
-- numerator is already the authoritative sign witness.
let (coefficientXNumerator, coefficientXDenominator) =
exactRationalParts coefficientX
(coefficientYNumerator, coefficientYDenominator) =
exactRationalParts coefficientY
(constantNumerator, constantDenominator) = exactRationalParts constant
(xNumerator, xDenominator) = exactRationalParts x
(yNumerator, yDenominator) = exactRationalParts y
evaluationNumerator =
coefficientXNumerator
* xNumerator
* coefficientYDenominator
* yDenominator
* constantDenominator
+ coefficientYNumerator
* yNumerator
* coefficientXDenominator
* xDenominator
* constantDenominator
+ constantNumerator
* coefficientXDenominator
* xDenominator
* coefficientYDenominator
* yDenominator
in compare evaluationNumerator 0
{-# INLINE classifyExactPoint #-}
-- | Admit strict counter-clockwise convex points and retain the original
-- supporting line of every incoming edge.
exactRetainedPolygon
:: NonEmpty ExactPoint
-> Either ExactHalfPlaneError ExactRetainedPolygon
exactRetainedPolygon points
| NonEmpty.length points < 3 =
Left (ExactRetainedPolygonTooFewVertices (NonEmpty.length points))
| otherwise =
case firstNonCounterClockwiseTurn exactOrient2d points of
Just (index, turn) ->
Left (ExactRetainedPolygonNonConvexTurn index turn)
Nothing -> do
validateSimpleRetainedCycle points
ExactRetainedPolygon
<$> traverse retainIncomingLine (cyclePairsNonEmpty points)
where
retainIncomingLine (from, to) = do
halfPlane <- exactClosedHalfPlaneFromDirectedEdge from to
pure (ExactRetainedVertex to (exactClosedHalfPlaneLine halfPlane))
validateSimpleRetainedCycle
:: NonEmpty ExactPoint
-> Either ExactHalfPlaneError ()
validateSimpleRetainedCycle points =
case
[ (leftIndex, rightIndex, relation)
| ( (leftIndex, (leftFrom, leftTo))
, (rightIndex, (rightFrom, rightTo))
) <- unorderedPairs indexedEdges
, let relation =
exactSegmentRelation
leftFrom
leftTo
rightFrom
rightTo
, not
( admitsSimpleCycleEdgeRelation
segmentCount
leftIndex
rightIndex
relation
)
] of
(leftIndex, rightIndex, relation) : _ ->
Left
( ExactRetainedPolygonSelfRelation
leftIndex
rightIndex
relation
)
[] -> Right ()
where
indexedEdges = zip [0 :: Int ..] (NonEmpty.toList (cyclePairsNonEmpty points))
segmentCount = NonEmpty.length points
-- | Project the retained-edge carrier to its authoritative point cycle.
exactRetainedPolygonPoints :: ExactRetainedPolygon -> NonEmpty ExactPoint
exactRetainedPolygonPoints (ExactRetainedPolygon vertices) =
fmap retainedVertexPoint vertices
-- | Intersect a retained convex polygon with exact closed half-planes.
-- Boundaries descend in exact angular order through one immutable deque;
-- every published edge retains one submitted source line, and derived
-- endpoints never become line coefficients.
exactClipRetainedPolygon
:: ExactRetainedPolygon
-> [ExactClosedHalfPlane]
-> Either ExactClipError (ExactClipDisposition, ExactClipReceipt)
exactClipRetainedPolygon polygon halfPlanes = do
let inputPoints = exactRetainedPolygonPoints polygon
inputBits = maximumPointBitWidth inputPoints
domainHalfPlanes = retainedPolygonHalfPlanes polygon
allHalfPlanes = domainHalfPlanes <> halfPlanes
initialReceipt =
ExactClipReceipt
{ exactClipSubmittedHalfPlanes = length halfPlanes
, exactClipActiveBoundaries =
if null halfPlanes then NonEmpty.length inputPoints else 0
, exactClipBoundaryCompatibilityChecks = 0
, exactClipExactIntersections = 0
, exactClipInputCoordinateBits = inputBits
, exactClipMaximumAffineCoefficientBits =
List.foldl'
(\bits -> max bits . affineLineBitWidth . exactClosedHalfPlaneLine)
0
allHalfPlanes
, exactClipPeakIntermediateCoordinateBits = inputBits
, exactClipFinalCoordinateBits = inputBits
, exactClipFinalDenominatorBits =
maximumPointDenominatorBitWidth inputPoints
}
if null halfPlanes
then pure (ExactClipFullDimensional polygon, initialReceipt)
else case List.find (`excludesExactDomain` inputPoints) halfPlanes of
Just _ ->
pure
( ExactClipEmpty
, initialReceipt
{ exactClipFinalCoordinateBits = 0
, exactClipFinalDenominatorBits = 0
}
)
Nothing -> do
let orderedBoundaries =
coalesceAngularBoundaries (fmap angularBoundary allHalfPlanes)
descentReceipt =
initialReceipt
{ exactClipActiveBoundaries = length orderedBoundaries
}
(openDeque, descendedReceipt) <-
foldM insertAngularBoundary (Sequence.empty, descentReceipt) orderedBoundaries
(closedDeque, accumulatedReceipt) <- closeAngularBoundaryDeque openDeque descendedReceipt
(finalState, finalizedReceipt) <-
angularBoundaryDequeState orderedBoundaries closedDeque accumulatedReceipt
let disposition = exactClipStateDisposition finalState
finalPoints = exactClipDispositionPoints disposition
finalReceipt =
finalizedReceipt
{ exactClipFinalCoordinateBits = maybe 0 maximumPointBitWidth finalPoints
, exactClipFinalDenominatorBits =
maybe 0 maximumPointDenominatorBitWidth finalPoints
}
pure (disposition, finalReceipt)
-- | A linear functional attains its maximum over a convex polygon at a
-- vertex. One half-plane whose closed side contains no domain vertex is
-- therefore an exact emptiness certificate; recognizing it before angular
-- descent avoids sorting a section that has no global point to glue.
excludesExactDomain
:: ExactClosedHalfPlane
-> NonEmpty ExactPoint
-> Bool
excludesExactDomain halfPlane =
all ((== LT) . classifyExactPoint halfPlane)
-- | One original source boundary together with its exact counter-clockwise
-- direction. Equal-direction gluing retains one of these values verbatim.
data ExactAngularBoundary = ExactAngularBoundary
{ angularBoundaryLine :: !ExactAffineLine
, angularBoundaryDirection :: !ExactVector
}
angularBoundary :: ExactClosedHalfPlane -> ExactAngularBoundary
angularBoundary halfPlane =
let line@(ExactAffineLine coefficientX coefficientY _) =
exactClosedHalfPlaneLine halfPlane
in ExactAngularBoundary
{ angularBoundaryLine = line
, angularBoundaryDirection = ExactVector coefficientY (negate coefficientX)
}
angularBoundaryHalfPlane :: ExactAngularBoundary -> ExactClosedHalfPlane
angularBoundaryHalfPlane = exactClosedHalfPlane . angularBoundaryLine
{-# INLINE angularBoundaryHalfPlane #-}
retainedPolygonHalfPlanes :: ExactRetainedPolygon -> [ExactClosedHalfPlane]
retainedPolygonHalfPlanes (ExactRetainedPolygon vertices) =
fmap
(exactClosedHalfPlane . retainedVertexIncomingLine)
(NonEmpty.toList vertices)
-- | Descent over one angular stalk. A stricter parallel boundary replaces a
-- weaker one; coincident boundaries retain the least original source line so
-- input permutations cannot alter the glued carrier.
coalesceAngularBoundaries :: [ExactAngularBoundary] -> [ExactAngularBoundary]
coalesceAngularBoundaries =
fmap strongestAngularBoundary
. mapMaybe NonEmpty.nonEmpty
. List.groupBy sameBoundaryDirection
. List.sortBy compareAngularBoundaries
where
sameBoundaryDirection left right =
compareExactVectorAngle
(angularBoundaryDirection left)
(angularBoundaryDirection right)
== EQ
compareAngularBoundaries :: ExactAngularBoundary -> ExactAngularBoundary -> Ordering
compareAngularBoundaries left right =
case
compareExactVectorAngle
(angularBoundaryDirection left)
(angularBoundaryDirection right) of
EQ -> compare (angularBoundaryLine left) (angularBoundaryLine right)
ordering -> ordering
strongestAngularBoundary :: NonEmpty ExactAngularBoundary -> ExactAngularBoundary
strongestAngularBoundary (initial :| remaining) =
List.foldl' chooseStrongerAngularBoundary initial remaining
chooseStrongerAngularBoundary
:: ExactAngularBoundary
-> ExactAngularBoundary
-> ExactAngularBoundary
chooseStrongerAngularBoundary selected candidate =
case compareParallelBoundaryStrength selected candidate of
LT -> candidate
GT -> selected
EQ ->
if angularBoundaryLine candidate < angularBoundaryLine selected
then candidate
else selected
-- | Compare same-direction boundaries. 'LT' means the right boundary is
-- stricter, 'GT' means the left is stricter, and 'EQ' means the closed
-- half-planes coincide. Cross multiplication avoids inventing a normalized
-- replacement line.
compareParallelBoundaryStrength
:: ExactAngularBoundary
-> ExactAngularBoundary
-> Ordering
compareParallelBoundaryStrength left right =
let ExactAffineLine leftX leftY leftConstant = angularBoundaryLine left
ExactAffineLine rightX rightY rightConstant = angularBoundaryLine right
compareScaled leftScale rightScale scaleSign =
let raw = compare (rightConstant * leftScale) (rightScale * leftConstant)
in if exactSignum scaleSign == LT then reverseOrdering raw else raw
in if exactRationalIsZero leftX
then compareScaled leftY rightY leftY
else compareScaled leftX rightX leftX
reverseOrdering :: Ordering -> Ordering
reverseOrdering LT = GT
reverseOrdering EQ = EQ
reverseOrdering GT = LT
-- | One active source boundary and its cached intersection with the preceding
-- deque boundary. Every adjacency point is derived once and reused by both
-- compatibility trims and final publication.
data ExactAngularBoundaryNode = ExactAngularBoundaryNode
{ angularNodeBoundary :: !ExactAngularBoundary
, angularNodePreviousIntersection :: !(Maybe ExactPoint)
}
type ExactAngularBoundaryDeque = Seq ExactAngularBoundaryNode
insertAngularBoundary
:: (ExactAngularBoundaryDeque, ExactClipReceipt)
-> ExactAngularBoundary
-> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
insertAngularBoundary (boundaries, receipt) incoming = do
(backTrimmed, backReceipt) <- trimAngularBack incoming boundaries receipt
(frontTrimmed, frontReceipt) <- trimAngularFront incoming backTrimmed backReceipt
appendAngularBoundary incoming frontTrimmed frontReceipt
appendAngularBoundary
:: ExactAngularBoundary
-> ExactAngularBoundaryDeque
-> ExactClipReceipt
-> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
appendAngularBoundary incoming boundaries receipt =
case Sequence.viewr boundaries of
EmptyR ->
Right
( Sequence.singleton (ExactAngularBoundaryNode incoming Nothing)
, receipt
)
_ :> finalNode -> do
(intersection, observedReceipt) <-
observeAdjacentIntersection
(angularNodeBoundary finalNode)
incoming
receipt
pure
( boundaries
|> ExactAngularBoundaryNode incoming intersection
, observedReceipt
)
trimAngularBack
:: ExactAngularBoundary
-> ExactAngularBoundaryDeque
-> ExactClipReceipt
-> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
trimAngularBack incoming boundaries receipt =
case Sequence.viewr boundaries of
EmptyR -> Right (boundaries, receipt)
remaining :> finalNode ->
case Sequence.viewr remaining of
EmptyR -> Right (boundaries, receipt)
_ :> _ ->
case angularNodePreviousIntersection finalNode of
Nothing -> Right (boundaries, receipt)
Just point ->
let checkedReceipt = observeBoundaryCompatibility receipt
in if classifyExactPoint (angularBoundaryHalfPlane incoming) point == LT
then trimAngularBack incoming remaining checkedReceipt
else Right (boundaries, checkedReceipt)
trimAngularFront
:: ExactAngularBoundary
-> ExactAngularBoundaryDeque
-> ExactClipReceipt
-> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
trimAngularFront incoming boundaries receipt =
case Sequence.viewl boundaries of
EmptyL -> Right (boundaries, receipt)
_ :< remaining ->
case Sequence.viewl remaining of
EmptyL -> Right (boundaries, receipt)
secondNode :< suffix ->
case angularNodePreviousIntersection secondNode of
Nothing -> Right (boundaries, receipt)
Just point ->
let checkedReceipt = observeBoundaryCompatibility receipt
in if classifyExactPoint (angularBoundaryHalfPlane incoming) point == LT
then
trimAngularFront
incoming
( secondNode
{ angularNodePreviousIntersection = Nothing
}
Sequence.<| suffix
)
checkedReceipt
else Right (boundaries, checkedReceipt)
closeAngularBoundaryDeque
:: ExactAngularBoundaryDeque
-> ExactClipReceipt
-> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
closeAngularBoundaryDeque boundaries receipt = do
(backClosed, backReceipt) <- closeAngularBack boundaries receipt
(frontClosed, frontReceipt) <- closeAngularFront backClosed backReceipt
if Sequence.length frontClosed == Sequence.length boundaries
then Right (frontClosed, frontReceipt)
else closeAngularBoundaryDeque frontClosed frontReceipt
closeAngularBack
:: ExactAngularBoundaryDeque
-> ExactClipReceipt
-> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
closeAngularBack boundaries receipt =
case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
(firstNode :< _, remaining :> finalNode) ->
case Sequence.viewr remaining of
_ :> _ ->
case angularNodePreviousIntersection finalNode of
Nothing -> Right (boundaries, receipt)
Just point ->
let checkedReceipt = observeBoundaryCompatibility receipt
in if
classifyExactPoint
(angularBoundaryHalfPlane (angularNodeBoundary firstNode))
point
== LT
then closeAngularBack remaining checkedReceipt
else Right (boundaries, checkedReceipt)
EmptyR -> Right (boundaries, receipt)
_ -> Right (boundaries, receipt)
closeAngularFront
:: ExactAngularBoundaryDeque
-> ExactClipReceipt
-> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
closeAngularFront boundaries receipt =
case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
(_ :< remaining, _ :> finalNode) ->
case Sequence.viewl remaining of
secondNode :< suffix ->
case angularNodePreviousIntersection secondNode of
Nothing -> Right (boundaries, receipt)
Just point ->
let checkedReceipt = observeBoundaryCompatibility receipt
in if
classifyExactPoint
(angularBoundaryHalfPlane (angularNodeBoundary finalNode))
point
== LT
then
closeAngularFront
( secondNode
{ angularNodePreviousIntersection = Nothing
}
Sequence.<| suffix
)
checkedReceipt
else Right (boundaries, checkedReceipt)
EmptyL -> Right (boundaries, receipt)
_ -> Right (boundaries, receipt)
observeAdjacentIntersection
:: ExactAngularBoundary
-> ExactAngularBoundary
-> ExactClipReceipt
-> Either ExactClipError (Maybe ExactPoint, ExactClipReceipt)
observeAdjacentIntersection left right receipt =
let leftLine = angularBoundaryLine left
rightLine = angularBoundaryLine right
in case exactAffineLineIntersection leftLine rightLine of
Left (ExactIntersectionParallelOrDegenerate _) ->
Right (Nothing, receipt)
Left obstruction -> Left (ExactClipIntersection obstruction)
Right point ->
pure
( Just point
, receipt
{ exactClipExactIntersections =
exactClipExactIntersections receipt + 1
, exactClipPeakIntermediateCoordinateBits =
max
(exactClipPeakIntermediateCoordinateBits receipt)
(exactPointBitWidth point)
}
)
observeBoundaryCompatibility :: ExactClipReceipt -> ExactClipReceipt
observeBoundaryCompatibility receipt =
receipt
{ exactClipBoundaryCompatibilityChecks =
exactClipBoundaryCompatibilityChecks receipt + 1
}
angularBoundaryDequeState
:: [ExactAngularBoundary]
-> ExactAngularBoundaryDeque
-> ExactClipReceipt
-> Either ExactClipError (ExactClipState, ExactClipReceipt)
angularBoundaryDequeState allBoundaries boundaries receipt = do
let nodeList = foldr (:) [] boundaries
linearVertices =
mapMaybe
retainedLinearVertex
(zip nodeList (drop 1 nodeList))
(closingVertices, finalizedReceipt) <-
case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
(firstNode :< _, _ :> finalNode) -> do
(intersection, observedReceipt) <-
observeAdjacentIntersection
(angularNodeBoundary finalNode)
(angularNodeBoundary firstNode)
receipt
pure
( maybe
[]
(\point ->
[ ExactRetainedVertex
point
(angularBoundaryLine (angularNodeBoundary finalNode))
])
intersection
, observedReceipt
)
_ -> Right ([], receipt)
state <-
retainedVerticesToClipState
(normalizeRetainedVertices (linearVertices <> closingVertices))
pure (validateLowerDimensionalState allBoundaries state, finalizedReceipt)
where
retainedLinearVertex (node, successor) =
fmap
(\point ->
ExactRetainedVertex
point
(angularBoundaryLine (angularNodeBoundary node)))
(angularNodePreviousIntersection successor)
validateLowerDimensionalState
:: [ExactAngularBoundary]
-> ExactClipState
-> ExactClipState
validateLowerDimensionalState boundaries state =
case state of
ExactClipStateSegment from to ->
if all (containsBoth from to) boundaries
then state
else ExactClipStateEmpty
ExactClipStatePoint point ->
if all
(\boundary ->
classifyExactPoint (angularBoundaryHalfPlane boundary) point /= LT)
boundaries
then state
else ExactClipStateEmpty
other -> other
where
containsBoth from to boundary =
let halfPlane = angularBoundaryHalfPlane boundary
in classifyExactPoint halfPlane from /= LT
&& classifyExactPoint halfPlane to /= LT
data ExactClipState
= ExactClipStatePolygon !ExactRetainedPolygon
| ExactClipStateSegment !ExactPoint !ExactPoint
| ExactClipStatePoint !ExactPoint
| ExactClipStateEmpty
exactSegmentClipState
:: ExactPoint
-> ExactPoint
-> ExactClipState
exactSegmentClipState from to
| from == to = ExactClipStatePoint from
| otherwise = ExactClipStateSegment from to
retainedVerticesToClipState
:: [ExactRetainedVertex]
-> Either ExactClipError ExactClipState
retainedVerticesToClipState [] = Right ExactClipStateEmpty
retainedVerticesToClipState [vertex] =
Right (ExactClipStatePoint (retainedVertexPoint vertex))
retainedVerticesToClipState [fromVertex, toVertex] =
Right
( exactSegmentClipState
(retainedVertexPoint fromVertex)
(retainedVertexPoint toVertex)
)
retainedVerticesToClipState (firstVertex : secondVertex : remaining) =
let retained = firstVertex :| (secondVertex : remaining)
twiceArea = retainedVerticesTwiceArea retained
in case exactSignum twiceArea of
GT -> Right (ExactClipStatePolygon (ExactRetainedPolygon retained))
LT -> Left (ExactClipOrientationReversed twiceArea)
EQ -> Right (lowerDimensionalClipState retained)
lowerDimensionalClipState
:: NonEmpty ExactRetainedVertex
-> ExactClipState
lowerDimensionalClipState vertices =
let points = fmap retainedVertexPoint vertices
initialPoint :| remainingPoints = points
leastPoint = List.foldl' min initialPoint remainingPoints
greatestPoint = List.foldl' max initialPoint remainingPoints
in if leastPoint == greatestPoint
then ExactClipStatePoint leastPoint
else ExactClipStateSegment leastPoint greatestPoint
retainedVerticesTwiceArea
:: NonEmpty ExactRetainedVertex
-> ExactRational
retainedVerticesTwiceArea =
List.foldl'
(\area (from, to) ->
area
+ exactPointCross
(retainedVertexPoint from)
(retainedVertexPoint to))
0
. cyclePairsNonEmpty
normalizeRetainedVertices
:: [ExactRetainedVertex]
-> [ExactRetainedVertex]
normalizeRetainedVertices = stripClosingDuplicate . deduplicateAdjacent
where
deduplicateAdjacent [] = []
deduplicateAdjacent (firstVertex : remaining) =
firstVertex : deduplicateAfter firstVertex remaining
deduplicateAfter _ [] = []
deduplicateAfter previous (candidate : remaining)
| retainedVertexPoint previous == retainedVertexPoint candidate =
deduplicateAfter previous remaining
| otherwise = candidate : deduplicateAfter candidate remaining
stripClosingDuplicate vertices@(firstVertex : remainingVertices) =
case reverse remainingVertices of
finalVertex : reversedInterior
| retainedVertexPoint finalVertex == retainedVertexPoint firstVertex ->
finalVertex : reverse reversedInterior
_ -> vertices
stripClosingDuplicate [] = []
exactClipStateDisposition :: ExactClipState -> ExactClipDisposition
exactClipStateDisposition state =
case state of
ExactClipStatePolygon polygon -> ExactClipFullDimensional polygon
ExactClipStateSegment from to ->
ExactClipLowerDimensional (from :| [to])
ExactClipStatePoint point -> ExactClipLowerDimensional (point :| [])
ExactClipStateEmpty -> ExactClipEmpty
exactClipDispositionPoints
:: ExactClipDisposition
-> Maybe (NonEmpty ExactPoint)
exactClipDispositionPoints disposition =
case disposition of
ExactClipFullDimensional polygon ->
Just (exactRetainedPolygonPoints polygon)
ExactClipLowerDimensional points -> Just points
ExactClipEmpty -> Nothing
retainedVertexPoint :: ExactRetainedVertex -> ExactPoint
retainedVertexPoint (ExactRetainedVertex point _) = point
{-# INLINE retainedVertexPoint #-}
retainedVertexIncomingLine :: ExactRetainedVertex -> ExactAffineLine
retainedVertexIncomingLine (ExactRetainedVertex _ line) = line
{-# INLINE retainedVertexIncomingLine #-}
affineLineBitWidth :: ExactAffineLine -> Int
affineLineBitWidth (ExactAffineLine coefficientX coefficientY constant) =
max
(exactRationalBitWidth coefficientX)
(max (exactRationalBitWidth coefficientY) (exactRationalBitWidth constant))
maximumPointBitWidth :: NonEmpty ExactPoint -> Int
maximumPointBitWidth =
List.foldl'
(\maximumBits (ExactPoint x y) ->
max maximumBits (max (exactRationalBitWidth x) (exactRationalBitWidth y)))
0
-- | Maximum reduced numerator-or-denominator width of either coordinate.
exactPointBitWidth :: ExactPoint -> Int
exactPointBitWidth (ExactPoint x y) =
max (exactRationalBitWidth x) (exactRationalBitWidth y)
{-# INLINE exactPointBitWidth #-}
maximumPointDenominatorBitWidth :: NonEmpty ExactPoint -> Int
maximumPointDenominatorBitWidth =
List.foldl'
(\maximumBits (ExactPoint x y) ->
max
maximumBits
( max
(exactRationalDenominatorBitWidth x)
(exactRationalDenominatorBitWidth y)
))
0
-- | 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 QueryPoint
exactPointToEmbeddingCandidate (ExactPoint x y) =
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 exactCrossSign 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 #-}
exactCrossSign :: ExactVector -> ExactVector -> Ordering
exactCrossSign (ExactVector leftX leftY) (ExactVector rightX rightY) =
-- Reduced rational denominators are positive; compare the two products in a
-- common integer scale without constructing and normalizing their ratios.
let (leftXNumerator, leftXDenominator) = exactRationalParts leftX
(leftYNumerator, leftYDenominator) = exactRationalParts leftY
(rightXNumerator, rightXDenominator) = exactRationalParts rightX
(rightYNumerator, rightYDenominator) = exactRationalParts rightY
in compare
( leftXNumerator
* rightYNumerator
* leftYDenominator
* rightXDenominator
)
( leftYNumerator
* rightXNumerator
* leftXDenominator
* rightYDenominator
)
{-# INLINE exactCrossSign #-}
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)