{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} module Moonlight.Triangulation.Math ( orient2d , sideQuery , inCircle , orientDetApprox , inCircleDetApprox , onClosedSegment , SegmentRelation (..) , allSegmentRelations , segmentRelation , segmentsProperlyCross , segmentsIntersect , squaredDistance , squaredDistanceWide , segmentDistanceSquared , segmentDistanceSquaredWide , distance , midpoint , centroid , triangleArea , triangleRadiusEdgeRatio , triangleRadiusEdgeRatioSquaredWithArea , circumcenter , barycentricCoordinates , inDiametralCircle , projectionFactor , canonicalPoint , canonicalCoordinate , validateCoordinate , mkQueryPoint , validatePoint , mitigateUnderflow , isFinite ) where import Control.DeepSeq (NFData) import GHC.Generics (Generic) import Moonlight.Triangulation.Internal.Dyadic ( exactBarycentricDeterminants , exactDiametralDot , integerRatioToDouble ) import Moonlight.Triangulation.LineSideInfo (LineSideInfo, fromOrdering) import Moonlight.Triangulation.Scalar ( canonicalScalarZero , inCircleCoordinates , maximumAllowedCoordinate , minimumAllowedCoordinate , orient2dCoordinates , scalarCcwErrorBound ) import Moonlight.Triangulation.Internal.Types ( BuildError (..) , CoordinateError (..) , Point (..) , PointValidationError (..) , QueryPoint (..) ) -- | The complete exact-predicate relation between two closed segments. There -- is one vocabulary owner; traversal and constrained-union consumers derive -- their booleans and obstruction policy from it rather than cloning slightly -- different orientation formulae. data SegmentRelation = SegmentsDisjoint | SegmentsDuplicate | SegmentsShareEndpoint | SegmentsProperlyCross | SegmentEndpointTouchesInterior | SegmentsCollinearlyOverlap deriving stock (Bounded, Enum, Eq, Ord, Show, Generic) deriving anyclass (NFData) allSegmentRelations :: [SegmentRelation] allSegmentRelations = [minBound .. maxBound] segmentRelation :: Point -> Point -> Point -> Point -> SegmentRelation segmentRelation a b c d | sameUndirectedSegment = SegmentsDuplicate | sharesEndpoint = SegmentsShareEndpoint | opposite abC abD && opposite cdA cdB = SegmentsProperlyCross | abC == EQ && abD == EQ && cdA == EQ && cdB == EQ = collinearRelation | endpointTouches = SegmentEndpointTouchesInterior | otherwise = SegmentsDisjoint where !abC = orient2d a b c !abD = orient2d a b d !cdA = orient2d c d a !cdB = orient2d c d b sameUndirectedSegment = (a == c && b == d) || (a == d && b == c) sharesEndpoint = a == c || a == d || b == c || b == d endpointTouches = (abC == EQ && onClosedSegment a b c) || (abD == EQ && onClosedSegment a b d) || (cdA == EQ && onClosedSegment c d a) || (cdB == EQ && onClosedSegment c d b) collinearRelation = let !overlapLower = max (min a b) (min c d) !overlapUpper = min (max a b) (max c d) in case compare overlapLower overlapUpper of LT -> SegmentsCollinearlyOverlap EQ -> SegmentEndpointTouchesInterior GT -> SegmentsDisjoint opposite left right = (left == LT && right == GT) || (left == GT && right == LT) segmentsIntersect :: Point -> Point -> Point -> Point -> Bool segmentsIntersect a b c d = segmentRelation a b c d /= SegmentsDisjoint -- | The proper-crossing section of 'segmentRelation'. Consumers which reject -- only that constructor need not compute the collinear and endpoint-touch -- distinctions required by the complete ADT after either side already proves -- separation. segmentsProperlyCross :: Point -> Point -> Point -> Point -> Bool segmentsProperlyCross a b c d = opposite (orient2d a b c) (orient2d a b d) && opposite (orient2d c d a) (orient2d c d b) where opposite LT GT = True opposite GT LT = True opposite _ _ = False validateCoordinate :: Double -> Maybe CoordinateError validateCoordinate value | isNaN value = Just CoordinateNaN | isInfinite value = Just CoordinateInfinite | value /= 0 && abs value < minimumAllowedCoordinate = Just CoordinateTooSmall | abs value > maximumAllowedCoordinate = Just CoordinateTooLarge | otherwise = Nothing -- | Admit and normalize a finite point for read-only geometric queries. mkQueryPoint :: Point -> Either PointValidationError (QueryPoint) mkQueryPoint point@(Point x y) = do maybe (Right ()) (Left . InvalidPointX) (validateCoordinate x) maybe (Right ()) (Left . InvalidPointY) (validateCoordinate y) Right (QueryPoint (canonicalPoint point)) validatePoint :: Maybe Int -> Point -> Either BuildError (QueryPoint) validatePoint slot point@(Point x y) = case mkQueryPoint point of Left (InvalidPointX reason) -> Left (InvalidCoordinate slot x reason) Left (InvalidPointY reason) -> Left (InvalidCoordinate slot y reason) Right queryPoint -> Right queryPoint -- | Round coordinates below the robust-predicate input floor toward zero. -- The operation never changes a coordinate already accepted by -- 'validateCoordinate'. mitigateUnderflow :: Point -> Point mitigateUnderflow (Point x y) = Point (mitigate x) (mitigate y) where mitigate :: Double -> Double mitigate value | value /= 0 && abs value < minimumAllowedCoordinate = 0 | otherwise = value canonicalPoint :: Point -> Point canonicalPoint (Point x y) = Point (canonicalCoordinate x) (canonicalCoordinate y) {-# INLINE canonicalPoint #-} -- | Round a signed zero to the canonical zero. The law that makes two points -- at the same position compare equal lives here; @canonicalPoint@ is its -- component-wise form and coordinate-carrying callers use it directly so no -- t'Point' is built only to be taken apart again. canonicalCoordinate :: Double -> Double canonicalCoordinate = canonicalScalarZero {-# INLINE canonicalCoordinate #-} -- base's 'isNaN' and 'isInfinite' are FFI calls in this GHC (static -- ccall to isDoubleNaN/isDoubleInfinite), and a circumcentre pays four of -- them per call. @value - value == 0@ is the same predicate in pure Double -- arithmetic: finite values subtract to zero, while NaN and the infinities -- subtract to NaN, which never compares equal. isFinite :: Double -> Bool isFinite value = value - value == 0 {-# INLINE isFinite #-} orientDetApprox :: Point -> Point -> Point -> Double orientDetApprox (Point ax ay) (Point bx by) (Point cx cy) = (ax - cx) * (by - cy) - (ay - cy) * (bx - cx) {-# INLINE orientDetApprox #-} orient2d :: Point -> Point -> Point -> Ordering orient2d (Point ax ay) (Point bx by) (Point cx cy) = orient2dCoordinates ax ay bx by cx cy {-# INLINE orient2d #-} sideQuery :: Point -> Point -> Point -> LineSideInfo sideQuery a b point = fromOrdering (orient2d a b point) {-# INLINE sideQuery #-} inCircleDetApprox :: Point -> Point -> Point -> Point -> Double inCircleDetApprox (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) = alift * bcdet + blift * cadet + clift * abdet where !adx = ax - dx !ady = ay - dy !bdx = bx - dx !bdy = by - dy !cdx = cx - dx !cdy = cy - dy !abdet = adx * bdy - bdx * ady !bcdet = bdx * cdy - cdx * bdy !cadet = cdx * ady - adx * cdy !alift = adx * adx + ady * ady !blift = bdx * bdx + bdy * bdy !clift = cdx * cdx + cdy * cdy {-# INLINE inCircleDetApprox #-} -- | Ordering of the oriented incircle determinant. For a counter-clockwise -- triangle, 'GT' means the fourth point lies strictly inside its circumcircle. inCircle :: Point -> Point -> Point -> Point -> Ordering inCircle (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) = inCircleCoordinates ax ay bx by cx cy dx dy {-# INLINE inCircle #-} onClosedSegment :: Point -> Point -> Point -> Bool onClosedSegment a@(Point ax ay) b@(Point bx by) query@(Point qx qy) = orient2d a b query == EQ && qx >= min ax bx && qx <= max ax bx && qy >= min ay by && qy <= max ay by {-# INLINE onClosedSegment #-} squaredDistance :: Point -> Point -> Double squaredDistance (Point ax ay) (Point bx by) = let !dx = ax - bx !dy = ay - by in dx * dx + dy * dy {-# INLINE squaredDistance #-} -- | Squared Euclidean distance in the mesh's Binary64 coordinate domain. squaredDistanceWide :: Point -> Point -> Double squaredDistanceWide = squaredDistance {-# INLINE squaredDistanceWide #-} segmentDistanceSquared :: Point -> Point -> Point -> Double segmentDistanceSquared from@(Point ax ay) to@(Point bx by) point@(Point px py) | lengthSquared == 0 = squaredDistance from point | factor <= 0 = squaredDistance from point | factor >= 1 = squaredDistance to point | otherwise = squaredDistance point (Point (ax + factor * dx) (ay + factor * dy)) where !dx = bx - ax !dy = by - ay !lengthSquared = dx * dx + dy * dy !factor = ((px - ax) * dx + (py - ay) * dy) / lengthSquared {-# INLINE segmentDistanceSquared #-} -- | Comparison form retained beside 'segmentDistanceSquared' for callers that -- state metric intent explicitly. segmentDistanceSquaredWide :: Point -> Point -> Point -> Double segmentDistanceSquaredWide = segmentDistanceSquared {-# INLINE segmentDistanceSquaredWide #-} distance :: Point -> Point -> Double distance left right = sqrt (squaredDistance left right) {-# INLINE distance #-} midpoint :: Point -> Point -> Point midpoint (Point ax ay) (Point bx by) = Point (0.5 * ax + 0.5 * bx) (0.5 * ay + 0.5 * by) {-# INLINE midpoint #-} centroid :: Point -> Point -> Point -> Point centroid (Point ax ay) (Point bx by) (Point cx cy) = Point (ax + (bx - ax) / 3 + (cx - ax) / 3) (ay + (by - ay) / 3 + (cy - ay) / 3) {-# INLINE centroid #-} triangleArea :: Point -> Point -> Point -> Double triangleArea a b c = 0.5 * abs (orientDetApprox a b c) {-# INLINE triangleArea #-} triangleRadiusEdgeRatio :: Point -> Point -> Point -> Maybe Double triangleRadiusEdgeRatio p0 p1 p2 | area <= 0 || shortest <= 0 = Nothing | not (isFinite ratio) = Nothing | otherwise = Just ratio where !area = triangleArea p0 p1 p2 !side01 = distance p0 p1 !side12 = distance p1 p2 !side20 = distance p2 p0 !shortest = min side01 (min side12 side20) !otherProduct | side01 <= side12 && side01 <= side20 = side12 * side20 | side12 <= side20 = side20 * side01 | otherwise = side01 * side12 !ratio = otherProduct / (4 * area) -- | The square of 'triangleRadiusEdgeRatio', for a triangle whose area the -- caller already has. -- -- The ratio is only ever compared against a bound, and both sides are -- non-negative, so the comparison can be made between squares. That is the -- whole reason to have this: it settles the same question without the three -- square roots the lengths would need, on the path taken by every face -- refinement considers. -- -- The area is a parameter and a degenerate triangle answers with an infinity -- rather than an absence, because the caller on that path has already computed -- the area to ask the area question and does nothing with the absence but -- compare an infinity in its place. triangleRadiusEdgeRatioSquaredWithArea :: Double -> Point -> Point -> Point -> Double triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2 | area <= 0 || shortest <= 0 = 1 / 0 | not (isFinite ratio) = 1 / 0 | otherwise = ratio where !side01 = squaredDistance p0 p1 !side12 = squaredDistance p1 p2 !side20 = squaredDistance p2 p0 !shortest = min side01 (min side12 side20) !otherProduct | side01 <= side12 && side01 <= side20 = side12 * side20 | side12 <= side20 = side20 * side01 | otherwise = side01 * side12 !ratio = otherProduct / (16 * area * area) -- The scale the determinants are divided by cancels out of the quotient -- exactly, so the computation works on the unscaled differences and divides -- once per coordinate. Scaling would only matter against overflow, and the -- validated coordinate domain (|x| <= 3.3e60) keeps every intermediate below -- 1e183, five orders below the Double ceiling; the four divisions it cost -- are the circumcentre's hot-path price. Identical points answer through the -- denominator, which is exactly zero exactly when they are collinear. circumcenter :: Point -> Point -> Point -> Maybe (Point) circumcenter (Point ax ay) (Point bx by) (Point cx cy) | denominator == 0 = Nothing | not (isFinite resultX && isFinite resultY) = Nothing | otherwise = Just (canonicalPoint (Point resultX resultY)) where !bax = bx - ax !bay = by - ay !cax = cx - ax !cay = cy - ay !bLength = bax * bax + bay * bay !cLength = cax * cax + cay * cay !denominator = 2 * (bax * cay - bay * cax) !offsetX = (cay * bLength - bay * cLength) / denominator !offsetY = (bax * cLength - cax * bLength) / denominator !resultX = ax + offsetX !resultY = ay + offsetY barycentricCoordinates :: Point -> Point -> Point -> Point -> Maybe (Double, Double, Double) barycentricCoordinates a@(Point ax ay) b@(Point bx by) c@(Point cx cy) query@(Point qx qy) | all reliable [denominatorInfo, weightAInfo, weightBInfo, weightCInfo] = if denominator == 0 then Nothing else Just (weightA / denominator, weightB / denominator, weightC / denominator) | exactDenominator == 0 = Nothing | otherwise = Just ( integerRatioToDouble exactWeightA exactDenominator , integerRatioToDouble exactWeightB exactDenominator , integerRatioToDouble exactWeightC exactDenominator ) where !denominatorInfo@(denominator, _) = determinantInfo a b c !weightAInfo@(weightA, _) = determinantInfo query b c !weightBInfo@(weightB, _) = determinantInfo a query c !weightCInfo@(weightC, _) = determinantInfo a b query (!exactDenominator, !exactWeightA, !exactWeightB, !exactWeightC) = exactBarycentricDeterminants ax ay bx by cx cy qx qy reliable (determinant, determinantSum) = isFinite determinant && abs determinant > scalarCcwErrorBound * determinantSum determinantInfo :: Point -> Point -> Point -> (Double, Double) determinantInfo (Point px py) (Point rx ry) (Point sx sy) = let !left = (px - sx) * (ry - sy) !right = (py - sy) * (rx - sx) in (left - right, abs left + abs right) -- | Whether a point lies in the closed diametral disk of a segment, decided by -- the sign of @(a-p)·(b-p)@. -- -- The approximation is two coordinate differences and one product per term -- combined additively, which is the arithmetic shape 'orient2d' is analysed -- under: the forward error of @fl(fl(a⊖b) ⊗ fl(c⊖d))@ combined by one rounded -- addition does not depend on the sign of that combination. The orientation -- coefficient therefore transfers unchanged, and the exact dot product remains -- the oracle for the uncertain band. inDiametralCircle :: Point -> Point -> Point -> Bool inDiametralCircle (Point ax ay) (Point bx by) (Point px py) | isFinite dot && abs dot > scalarCcwErrorBound * dotSum = dot < 0 | otherwise = exactDiametralDot ax ay bx by px py <= 0 where !left = (ax - px) * (bx - px) !right = (ay - py) * (by - py) !dot = left + right !dotSum = abs left + abs right {-# INLINE inDiametralCircle #-} projectionFactor :: Point -> Point -> Point -> Double projectionFactor (Point ax ay) (Point bx by) (Point qx qy) | lengthSquared == 0 = 0 | otherwise = ((qx - ax) * dx + (qy - ay) * dy) / lengthSquared where !dx = bx - ax !dy = by - ay !lengthSquared = dx * dx + dy * dy {-# INLINE projectionFactor #-}