packages feed

moonlight-triangulation-0.1.0.0: src-build/Moonlight/Triangulation/Internal/Join/Seam.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | Linear seam construction for separated Delaunay triangulations. Admission
-- returns an opaque proof carrying the exact source order and tangents; the
-- executor therefore has no untyped precondition and owns no fallback.
module Moonlight.Triangulation.Internal.Join.Seam
  ( SeamPlan
  , planSeam
  , executeSeam
  , ConstrainedSeamExecution
  , seamExecutionTriangulation
  , seamExecutionBuildStats
  , executeConstrainedSeam
  ) where

import Control.Monad.ST (ST, runST)
import Data.Bits (xor)
import Data.Foldable (traverse_)
import qualified Data.Vector.Unboxed as U
import Moonlight.Triangulation.Dcel
  ( adjacentEdge
  , faceDirectedEdges
  , numDirectedEdges
  , numFaces
  , numInnerFaces
  , numVertices
  , outerFace
  , vertexOutEdge
  , vertexData
  )
import Moonlight.Triangulation.Handles.HandleDefs
  ( DirectedEdgeId (..)
  , FaceId (..)
  , UndirectedEdgeId (..)
  , VertexId (..)
  )
import Moonlight.Triangulation.Internal.DcelOperations (closeOuterTurn, legalizeEdges)
import Moonlight.Triangulation.Internal.Cdt.Query (constraintEdges)
import Moonlight.Triangulation.Internal.Mutable
import Moonlight.Triangulation.Internal.OperationState
  ( OperationState
  , freezeBuildStats
  , newOperationState
  )
import Moonlight.Triangulation.Internal.Paged (pagedUnsafeIndex)
import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
import Moonlight.Triangulation.Internal.Types
  ( BuildError
  , BuildStats
  , ConstraintMode (Constrained, Unconstrained)
  , unitElementDefaults
  )
import Moonlight.Triangulation.Scalar (inCircleCoordinates, orient2dCoordinates)

-- | Proof that a particular pair can be copied and stitched by the seam
-- kernel. Constructors stay private so an unseparated pair cannot be handed to
-- the executor by accident.
data SeamPlan
  = SeamLeftBeforeRight !SeamTangents
  | SeamRightBeforeLeft !SeamTangents

planSeam
  :: Triangulation mode vertex () () ()
  -> Triangulation mode' vertex () () ()
  -> Maybe SeamPlan
planSeam left right =
  case separatedOrder left right of
    Just LeftBeforeRight ->
      Just (SeamLeftBeforeRight (seamTangents left right))
    Just RightBeforeLeft ->
      Just (SeamRightBeforeLeft (seamTangents right left))
    Nothing -> Nothing

-- | Execute a proved seam schedule. Numbering follows the schedule;
-- 'canonicalize' remains the explicit construction-independent observation.
executeSeam
  :: forall vertex
   . SeamPlan
  -> Triangulation 'Unconstrained vertex () () ()
  -> Triangulation 'Unconstrained vertex () () ()
  -> Either BuildError (Triangulation 'Unconstrained vertex () () ())
executeSeam plan left right =
  case plan of
    SeamLeftBeforeRight tangents ->
      fmap fst (mergeSeparated [] left right tangents)
    SeamRightBeforeLeft tangents ->
      fmap fst (mergeSeparated [] right left tangents)

-- | Result of the constrained seam kernel. Constraint flags are copied before
-- legalization, so source contour edges are immutable barriers while the
-- zipper constructs only the missing corridor.
data ConstrainedSeamExecution vertex = ConstrainedSeamExecution
  { seamExecutionTriangulation
      :: !(Triangulation 'Constrained vertex () () ())
  , seamExecutionBuildStats :: !BuildStats
  }

-- | Execute a proved seam while transporting both source constraint planes.
-- This is distinct from promoting the unconstrained result afterward: source
-- hull constraints must already be visible to seam legalization or the
-- legalization schedule could erase a solved source face before recovery had
-- a chance to mark it.
executeConstrainedSeam
  :: forall vertex
   . SeamPlan
  -> Triangulation 'Constrained vertex () () ()
  -> Triangulation 'Constrained vertex () () ()
  -> Either BuildError (ConstrainedSeamExecution vertex)
executeConstrainedSeam plan left right =
  fmap
    (uncurry ConstrainedSeamExecution)
    (case plan of
      SeamLeftBeforeRight tangents ->
        mergeSeparated
          [ (0, left)
          , (numDirectedEdges left, right)
          ]
          left
          right
          tangents
      SeamRightBeforeLeft tangents ->
        mergeSeparated
          [ (0, right)
          , (numDirectedEdges right, left)
          ]
          right
          left
          tangents
    )

data SeparatedOrder
  = LeftBeforeRight
  | RightBeforeLeft

separatedOrder
  :: Triangulation mode vertex directed undirected face
  -> Triangulation mode' vertex' directed' undirected' face'
  -> Maybe SeparatedOrder
separatedOrder left right
  | numInnerFaces left <= 0 || numInnerFaces right <= 0 = Nothing
  | otherwise = do
      (leftMinimum, leftMaximum) <- xBounds left
      (rightMinimum, rightMaximum) <- xBounds right
      if leftMaximum < rightMinimum
        then Just LeftBeforeRight
        else
          if rightMaximum < leftMinimum
            then Just RightBeforeLeft
            else Nothing

xBounds
  :: Triangulation mode vertex directed undirected face
  -> Maybe (Double, Double)
xBounds triangulation
  | total <= 0 = Nothing
  | otherwise = Just (go 1 first first)
 where
  !total = numVertices triangulation
  !coordinates = triPointX triangulation
  !first = coordinates `pagedUnsafeIndex` 0
  go !index !minimumX !maximumX
    | index >= total = (minimumX, maximumX)
    | otherwise =
        let !x = coordinates `pagedUnsafeIndex` index
         in go (index + 1) (min minimumX x) (max maximumX x)

-- The opposite-sign branch cannot overflow in its sum. The same-sign branch
-- cannot overflow in its difference.
data SeamTangents = SeamTangents
  {-# UNPACK #-} !Int
  {-# UNPACK #-} !Int
  {-# UNPACK #-} !Int
  {-# UNPACK #-} !Int

seamTangents
  :: Triangulation mode vertex directed undirected face
  -> Triangulation mode' vertex' directed' undirected' face'
  -> SeamTangents
seamTangents left right =
  let (lowerLeft, lowerRight) = lowerTangent left right leftHull rightHull
      (upperLeft, upperRight) = upperTangent left right leftHull rightHull
   in SeamTangents lowerLeft lowerRight upperLeft upperRight
 where
  !leftHull = outerEdges left
  !rightHull = outerEdges right

lowerTangent
  :: Triangulation mode vertex directed undirected face
  -> Triangulation mode' vertex' directed' undirected' face'
  -> U.Vector Int
  -> U.Vector Int
  -> (Int, Int)
-- The walk carries both endpoints' coordinates: a step replaces exactly one
-- endpoint, and the replacement is the neighbour whose coordinates the step's
-- own test already read.
lowerTangent left right leftHull rightHull =
  walk leftStart leftStartX leftStartY rightStart rightStartX rightStartY
 where
  !leftStart = extremeHullIndex preferRightmost left leftHull
  !rightStart = extremeHullIndex preferLeftmost right rightHull
  (!leftStartX, !leftStartY) = hullPoint left leftHull leftStart
  (!rightStartX, !rightStartY) = hullPoint right rightHull rightStart

  walk !leftIndex !leftX !leftY !rightIndex !rightX !rightY
    | leftBelow = walk nextLeft nextLeftX nextLeftY rightIndex rightX rightY
    | rightBelow = walk leftIndex leftX leftY previousRight previousRightX previousRightY
    | otherwise = (leftHull `U.unsafeIndex` leftIndex, rightHull `U.unsafeIndex` rightIndex)
   where
    !nextLeft = nextIndex (U.length leftHull) leftIndex
    (!nextLeftX, !nextLeftY) = hullPoint left leftHull nextLeft
    !leftBelow =
      orient2dCoordinates leftX leftY rightX rightY nextLeftX nextLeftY == LT
    previousRight = previousIndex (U.length rightHull) rightIndex
    (previousRightX, previousRightY) = hullPoint right rightHull previousRight
    rightBelow =
      orient2dCoordinates leftX leftY rightX rightY previousRightX previousRightY == LT

upperTangent
  :: Triangulation mode vertex directed undirected face
  -> Triangulation mode' vertex' directed' undirected' face'
  -> U.Vector Int
  -> U.Vector Int
  -> (Int, Int)
upperTangent left right leftHull rightHull =
  walk leftStart leftStartX leftStartY rightStart rightStartX rightStartY
 where
  !leftStart = extremeHullIndex preferRightmostUpper left leftHull
  !rightStart = extremeHullIndex preferLeftmostUpper right rightHull
  (!leftStartX, !leftStartY) = hullPoint left leftHull leftStart
  (!rightStartX, !rightStartY) = hullPoint right rightHull rightStart

  walk !leftIndex !leftX !leftY !rightIndex !rightX !rightY
    | leftAbove = walk previousLeft previousLeftX previousLeftY rightIndex rightX rightY
    | rightAbove = walk leftIndex leftX leftY nextRight nextRightX nextRightY
    | otherwise = (leftHull `U.unsafeIndex` leftIndex, rightHull `U.unsafeIndex` rightIndex)
   where
    !previousLeft = previousIndex (U.length leftHull) leftIndex
    (!previousLeftX, !previousLeftY) = hullPoint left leftHull previousLeft
    !leftAbove =
      orient2dCoordinates leftX leftY rightX rightY previousLeftX previousLeftY == GT
    nextRight = nextIndex (U.length rightHull) rightIndex
    (nextRightX, nextRightY) = hullPoint right rightHull nextRight
    rightAbove =
      orient2dCoordinates leftX leftY rightX rightY nextRightX nextRightY == GT

type ExtremePreference = Double -> Double -> Double -> Double -> Bool

preferRightmost :: ExtremePreference
preferRightmost bestX bestY candidateX candidateY =
  candidateX > bestX || (candidateX == bestX && candidateY < bestY)
{-# INLINE preferRightmost #-}

preferLeftmost :: ExtremePreference
preferLeftmost bestX bestY candidateX candidateY =
  candidateX < bestX || (candidateX == bestX && candidateY < bestY)
{-# INLINE preferLeftmost #-}

preferRightmostUpper :: ExtremePreference
preferRightmostUpper bestX bestY candidateX candidateY =
  candidateX > bestX || (candidateX == bestX && candidateY > bestY)
{-# INLINE preferRightmostUpper #-}

preferLeftmostUpper :: ExtremePreference
preferLeftmostUpper bestX bestY candidateX candidateY =
  candidateX < bestX || (candidateX == bestX && candidateY > bestY)
{-# INLINE preferLeftmostUpper #-}

extremeHullIndex
  :: ExtremePreference
  -> Triangulation mode vertex directed undirected face
  -> U.Vector Int
  -> Int
-- The running best carries its own coordinates. Re-reading them per candidate
-- read the same immutable slots @size@ times over instead of once.
extremeHullIndex prefer triangulation hull
  | size <= 0 = 0
  | otherwise =
      let (!firstX, !firstY) = hullPoint triangulation hull 0
       in go 1 0 firstX firstY
 where
  !size = U.length hull
  go !index !best !bestX !bestY
    | index >= size = best
    | otherwise =
        let (!candidateX, !candidateY) = hullPoint triangulation hull index
         in if prefer bestX bestY candidateX candidateY
              then go (index + 1) index candidateX candidateY
              else go (index + 1) best bestX bestY

outerEdges
  :: Triangulation mode vertex directed undirected face
  -> U.Vector Int
outerEdges =
  U.fromList
    . fmap (\(DirectedEdgeId edge) -> fromIntegral edge)
    . (`faceDirectedEdges` outerFace)

hullPoint
  :: Triangulation mode vertex directed undirected face
  -> U.Vector Int
  -> Int
  -> (Double, Double)
hullPoint triangulation hull index =
  let !edge = hull `U.unsafeIndex` index
      !vertex = topologyAt triangulation (4 * edge)
   in ( triPointX triangulation `pagedUnsafeIndex` vertex
      , triPointY triangulation `pagedUnsafeIndex` vertex
      )
{-# INLINE hullPoint #-}

nextIndex :: Int -> Int -> Int
nextIndex size index
  | index + 1 == size = 0
  | otherwise = index + 1
{-# INLINE nextIndex #-}

previousIndex :: Int -> Int -> Int
previousIndex size index
  | index == 0 = size - 1
  | otherwise = index - 1
{-# INLINE previousIndex #-}

mergeSeparated
  :: forall outputMode leftMode rightMode vertex
   . [(Int, Triangulation 'Constrained vertex () () ())]
  -> Triangulation leftMode vertex () () ()
  -> Triangulation rightMode vertex () () ()
  -> SeamTangents
  -> Either
      BuildError
      (Triangulation outputMode vertex () () (), BuildStats)
mergeSeparated constraintSections left right (SeamTangents lowerLeft lowerRight upperLeft upperRight) = runST $ do
  mutable <- newMutableDcel unitElementDefaults totalVertices
  pointCapacityOutcome <- ensurePointCapacity mutable totalVertices
  cellCapacityOutcome <-
    ensureCellCapacity
      mutable
      ((leftDirected + rightDirected) `quot` 2 + 1)
      (leftFaces + rightFaces - 2)
  case (pointCapacityOutcome, cellCapacityOutcome) of
    (Left obstruction, _) -> pure (Left obstruction)
    (_, Left obstruction) -> pure (Left obstruction)
    (Right (), Right ()) -> do
      appendSourceVertices mutable left
      appendSourceVertices mutable right
      _ <- addEdgeBlock mutable ((leftDirected + rightDirected) `quot` 2)
      _ <- addFaceBlock mutable (leftFaces + rightFaces - 2)
      copySource mutable left 0 0 0
      copySource mutable right leftVertices leftDirected (leftFaces - 1)
      traverse_ (uncurry (copySourceConstraints mutable)) constraintSections
      base <- spliceLowerTangent mutable leftDirected lowerLeft lowerRight
      operation <- newOperationState (halfEdgeCapacity mutable)
      stitched <-
        stitchSeam
          mutable
          operation
          base
          (topologyAt left (4 * upperLeft))
          (leftVertices + topologyAt right (4 * upperRight))
          []
      case stitched of
        Left obstruction -> pure (Left obstruction)
        Right () -> do
          statistics <- freezeBuildStats operation
          fmap (\triangulation -> (triangulation, statistics))
            <$> freezeTriangulation mutable
 where
  !leftVertices = numVertices left
  !rightVertices = numVertices right
  !totalVertices = leftVertices + rightVertices
  !leftDirected = numDirectedEdges left
  !rightDirected = numDirectedEdges right
  !leftFaces = numFaces left
  !rightFaces = numFaces right

stitchSeam
  :: MutableDcel s vertex directed undirected face
  -> OperationState s
  -> Int
  -> Int
  -> Int
  -> [Int]
  -> ST s (Either BuildError ())
stitchSeam mutable operation base upperLeft upperRight seeds = do
  leftVertex <- readOrigin mutable base
  rightVertex <- readOrigin mutable (base `xor` 1)
  if leftVertex == upperLeft && rightVertex == upperRight
    then legalizeEdges mutable operation seeds >> pure (Right ())
    else do
      leftEdge <- readPrevious mutable base
      rightEdge <- readNext mutable base
      nextLeft <- readOrigin mutable leftEdge
      nextRight <- readOrigin mutable (rightEdge `xor` 1)
      leftTurn <- vertexOrientation mutable nextLeft leftVertex rightVertex
      rightTurn <- vertexOrientation mutable leftVertex rightVertex nextRight
      chooseRight <-
        if rightVertex == upperRight
          then pure False
          else
            if leftVertex == upperLeft
              then pure True
              else
                case (leftTurn == GT, rightTurn == GT) of
                  (True, True) ->
                    (== GT) <$> vertexInCircle mutable nextLeft leftVertex rightVertex nextRight
                  (False, True) -> pure True
                  _ -> pure False
      if chooseRight
        then do
          closed <- closeOuterTurn mutable base
          case closed of
            Left obstruction -> pure (Left obstruction)
            Right nextBase ->
              stitchSeam mutable operation nextBase upperLeft upperRight (base : rightEdge : seeds)
        else do
          closed <- closeOuterTurn mutable leftEdge
          case closed of
            Left obstruction -> pure (Left obstruction)
            Right nextBase ->
              stitchSeam mutable operation nextBase upperLeft upperRight (leftEdge : base : seeds)

vertexOrientation
  :: MutableDcel s vertex directed undirected face
  -> Int
  -> Int
  -> Int
  -> ST s Ordering
vertexOrientation mutable a b c = do
  ax <- readPointX mutable a
  ay <- readPointY mutable a
  bx <- readPointX mutable b
  by <- readPointY mutable b
  cx <- readPointX mutable c
  cy <- readPointY mutable c
  pure $! orient2dCoordinates ax ay bx by cx cy
{-# INLINE vertexOrientation #-}

vertexInCircle
  :: MutableDcel s vertex directed undirected face
  -> Int
  -> Int
  -> Int
  -> Int
  -> ST s Ordering
vertexInCircle mutable a b c d = do
  ax <- readPointX mutable a
  ay <- readPointY mutable a
  bx <- readPointX mutable b
  by <- readPointY mutable b
  cx <- readPointX mutable c
  cy <- readPointY mutable c
  dx <- readPointX mutable d
  dy <- readPointY mutable d
  pure $! inCircleCoordinates ax ay bx by cx cy dx dy
{-# INLINE vertexInCircle #-}

appendSourceVertices
  :: MutableDcel s vertex () () ()
  -> Triangulation mode vertex () () ()
  -> ST s ()
appendSourceVertices mutable source =
  forRange 0 (numVertices source) $ \vertex -> do
    _ <-
      appendVertexCoordinates
        mutable
        (triPointX source `pagedUnsafeIndex` vertex)
        (triPointY source `pagedUnsafeIndex` vertex)
        (vertexData source (VertexId (fromIntegral vertex)))
    pure ()

copySource
  :: MutableDcel s vertex () () ()
  -> Triangulation mode vertex () () ()
  -> Int
  -> Int
  -> Int
  -> ST s ()
copySource mutable source vertexOffset edgeOffset faceOffset = do
  forRange 0 (numDirectedEdges source) $ \edge -> do
    let !target = edgeOffset + edge
        !sourceBase = 4 * edge
        !sourceFace = topologyAt source (sourceBase + 3)
        !targetFace = if sourceFace == 0 then 0 else faceOffset + sourceFace
    writeOrigin mutable target (vertexOffset + topologyAt source sourceBase)
    writeNext mutable target (edgeOffset + topologyAt source (sourceBase + 1))
    writePrevious mutable target (edgeOffset + topologyAt source (sourceBase + 2))
    writeFace mutable target targetFace
  forRange 0 (numVertices source) $ \vertex ->
    case vertexOutEdge source (VertexId (fromIntegral vertex)) of
      Nothing -> markConnected mutable (vertexOffset + vertex) (-1)
      Just (DirectedEdgeId edge) ->
        markConnected mutable (vertexOffset + vertex) (edgeOffset + fromIntegral edge)
  forRange 1 (numFaces source) $ \face ->
    case adjacentEdge source (FaceId (fromIntegral face)) of
      Nothing -> writeFaceEdge mutable (faceOffset + face) (-1)
      Just (DirectedEdgeId edge) ->
        writeFaceEdge mutable (faceOffset + face) (edgeOffset + fromIntegral edge)

copySourceConstraints
  :: MutableDcel s vertex () () ()
  -> Int
  -> Triangulation 'Constrained vertex () () ()
  -> ST s ()
copySourceConstraints mutable directedEdgeOffset source =
  traverse_
    (\(UndirectedEdgeId edge) ->
       ()
         <$ setConstraint
           mutable
           (directedEdgeOffset + 2 * fromIntegral edge)
    )
    (constraintEdges source)

spliceLowerTangent
  :: MutableDcel s vertex () () ()
  -> Int
  -> Int
  -> Int
  -> ST s Int
spliceLowerTangent mutable rightEdgeOffset leftOuter rightOuterSource = do
  let !rightOuter = rightEdgeOffset + rightOuterSource
  leftVertex <- readOrigin mutable leftOuter
  rightVertex <- readOrigin mutable rightOuter
  leftPrevious <- readPrevious mutable leftOuter
  rightPrevious <- readPrevious mutable rightOuter
  (forward, backward) <- addEdge mutable leftVertex rightVertex
  writeFace mutable forward 0
  writeFace mutable backward 0
  linkEdges mutable leftPrevious forward
  linkEdges mutable forward rightOuter
  linkEdges mutable rightPrevious backward
  linkEdges mutable backward leftOuter
  writeFaceEdge mutable 0 forward
  writeVertexOut mutable leftVertex forward
  writeVertexOut mutable rightVertex backward
  pure forward
{-# INLINE spliceLowerTangent #-}

topologyAt
  :: Triangulation mode vertex directed undirected face
  -> Int
  -> Int
topologyAt triangulation slot =
  fromIntegral (triHalfTopology triangulation `pagedUnsafeIndex` slot)
{-# INLINE topologyAt #-}


forRange :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
forRange from to action = go from
 where
  go !index
    | index >= to = pure ()
    | otherwise = action index >> go (index + 1)
{-# INLINE forRange #-}