packages feed

moonlight-triangulation-1.0.0.0: src-dcel/Moonlight/Triangulation/Math.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}

-- | Robust planar predicates and derived Euclidean constructions.
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)

-- | Every segment relation in constructor order.
allSegmentRelations :: [SegmentRelation]
allSegmentRelations = [minBound .. maxBound]

-- | Exact relation between two closed segments.
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)

-- | Whether two closed segments share any point.
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

-- | Classify a coordinate outside the exact-predicate input domain.
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))

-- | Validate a construction point while retaining its optional input slot.
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

-- | Canonicalize both coordinate components for point identity.
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.
-- | Whether a scalar is neither infinite nor NaN.
isFinite :: Double -> Bool
isFinite value = value - value == 0
{-# INLINE isFinite #-}

-- | Fast approximate signed orientation determinant.
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 #-}

-- | Exact orientation ordering of three points.
orient2d :: Point -> Point -> Point -> Ordering
orient2d (Point ax ay) (Point bx by) (Point cx cy) =
  orient2dCoordinates ax ay bx by cx cy
{-# INLINE orient2d #-}

-- | Exact side of an oriented line.
sideQuery :: Point -> Point -> Point -> LineSideInfo
sideQuery a b point = fromOrdering (orient2d a b point)
{-# INLINE sideQuery #-}

-- | Fast approximate oriented in-circle determinant.
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 #-}

-- | Whether a point lies on a closed segment.
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 #-}

-- | Squared Euclidean distance.
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 #-}

-- | Squared distance from a point to a closed segment.
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 #-}

-- | Euclidean distance.
distance :: Point -> Point -> Double
distance left right = sqrt (squaredDistance left right)
{-# INLINE distance #-}

-- | Midpoint of two points.
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 of three points.
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 #-}

-- | Unsigned area of a triangle.
triangleArea :: Point -> Point -> Point -> Double
triangleArea a b c = 0.5 * abs (orientDetApprox a b c)
{-# INLINE triangleArea #-}

-- | Circumradius divided by shortest edge length.
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 of a nondegenerate triangle.
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

-- | Barycentric coordinates of a point in a nondegenerate triangle.
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 #-}

-- | Projection parameter of a point onto an oriented segment line.
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 #-}