packages feed

moonlight-triangulation-0.1.0.0: src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs

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

-- | Canonical constraint segments and the atomic partial union of two
-- constrained meshes, retaining or combining their site annotations.
module Moonlight.Triangulation.Internal.Cdt.Union
  ( canonicalSegment
  , constraintSegments
  , unionConstrainedWith
  , unionConstrained
  , joinSeparatedConstrainedWith
  , extendConstrainedWith
  , segmentRequest
  , firstRejected
  , completeConstraintConflicts
  , orderedConflict
  , crossingIsRepresented
  ) where

import Control.Monad (foldM)
import Control.Monad.ST (ST)
import Data.Either (isRight)
import Data.List (sort)
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Vector as V
import qualified Moonlight.Triangulation.Dcel as Dcel
import Moonlight.Triangulation.Handles.HandleDefs
import Moonlight.Triangulation.IntersectionIterator (foldCorridorBetweenPoints)
import Moonlight.Triangulation.Internal.Canonical (canonicalize)
import Moonlight.Triangulation.Insertion (insertPointCombining)
import Moonlight.Triangulation.Internal.Cdt.Batch
  ( interpretConstraintRequests
  , recoverConstraints
  )
import Moonlight.Triangulation.Internal.Cdt.Build (fromDelaunay)
import Moonlight.Triangulation.Internal.Cdt.Combinators (foldWhileM, mapLeft)
import Moonlight.Triangulation.Internal.Cdt.Query (constraintEdges)
import Moonlight.Triangulation.Internal.Cdt.Types
import Moonlight.Triangulation.Internal.Join.Rebuild (rebuildCanonicalSiteSet)
import Moonlight.Triangulation.Internal.Join.Seam
  ( executeConstrainedSeam
  , planSeam
  , seamExecutionBuildStats
  , seamExecutionTriangulation
  )
import Moonlight.Triangulation.Internal.Join.SiteSet
  ( SiteSet
  , siteSetAssocs
  , siteSetFromTriangulation
  , siteSetPoints
  , siteSetSize
  , siteSetUnionWith
  )
import Moonlight.Triangulation.Internal.Paged (TransactionShape (DenseTransaction, LocalTransaction))
import Moonlight.Triangulation.Internal.Representation
import Moonlight.Triangulation.Internal.Mutable (MutableDcel)
import Moonlight.Triangulation.Internal.OperationState (OperationState)
import Moonlight.Triangulation.Internal.Transaction (runTransaction)
import Moonlight.Triangulation.Internal.Types
import Moonlight.Triangulation.Math

canonicalSegment :: Point -> Point -> CanonicalSegment
canonicalSegment from to
  | from <= to = CanonicalSegment from to
  | otherwise = CanonicalSegment to from
{-# INLINE canonicalSegment #-}

-- | Geometry of the constraint section, deduplicated and canonically ordered.
constraintSegments
  :: Triangulation 'Constrained vertex directed undirected face
  -> V.Vector (CanonicalSegment)
constraintSegments triangulation =
  V.fromList
    ( Set.toAscList
        ( Set.fromList
            [ canonicalSegment
                (Dcel.vertexPoint triangulation from)
                (Dcel.vertexPoint triangulation to)
            | edge <- constraintEdges triangulation
            , let (from, to) = Dcel.undirectedEndpoints triangulation edge
            ]
        )
    )

-- | Atomic partial union of constrained meshes. Coincident sites combine
-- their annotations before construction. Complete canonical conflict
-- witnesses descend first; a successful branch then reaches the existing
-- batch corridor interpreter exactly once.
unionConstrainedWith
  :: (annotation -> annotation -> annotation)
  -> Triangulation 'Constrained annotation () () ()
  -> Triangulation 'Constrained annotation () () ()
  -> Either
      (ConstrainedUnionError)
      (Triangulation 'Constrained annotation () () ())
unionConstrainedWith combine left right =
  case NonEmpty.nonEmpty (Set.toAscList conflicts) of
    Just witnesses -> Left (ConstraintUnionConflicts witnesses)
    Nothing -> do
      unconstrained <-
        mapLeft
          (ConstraintUnionConstructionFailed . CdtBuildError)
          (rebuildCanonicalSiteSet unionSites)
      requests <- traverse (segmentRequest unconstrained) (V.toList segments)
      recovered <-
        mapLeft ConstraintUnionConstructionFailed
          (recoverConstraints (fromDelaunay unconstrained) (V.fromList requests))
      case firstRejected (constraintBatchOutcomes recovered) of
        Just blocking ->
          Left
            ( ConstraintUnionConstructionFailed
                (ConstraintIntersection blocking)
            )
        Nothing ->
          mapLeft
            (ConstraintUnionConstructionFailed . CdtBuildError)
            (canonicalize (constraintBatchTriangulation recovered))
 where
  leftSites = siteSetFromTriangulation left
  rightSites = siteSetFromTriangulation right
  unionSites = siteSetUnionWith combine leftSites rightSites
  segments =
    V.fromList
      ( Set.toAscList
          ( Set.union
              (Set.fromList (V.toList (constraintSegments left)))
              (Set.fromList (V.toList (constraintSegments right)))
          )
      )
  conflicts = completeConstraintConflicts unionSites segments
{-# INLINE unionConstrainedWith #-}

-- | Atomic partial union specialized to geometry-only constrained meshes.
unionConstrained
  :: Triangulation 'Constrained () () () ()
  -> Triangulation 'Constrained () () () ()
  -> Either
      (ConstrainedUnionError)
      (Triangulation 'Constrained () () () ())
unionConstrained = unionConstrainedWith (\_ _ -> ())
{-# INLINE unionConstrained #-}

-- | Join two strictly separated constrained triangulations by copying both
-- source meshes and zippering only their common tangent corridor. The source
-- constraint planes are present during legalization, so contour edges are
-- immutable barriers rather than edges recovery tries to resurrect after a
-- solved face has already disappeared.
--
-- The annotation combiner is intentionally not evaluated: strict x-separation
-- proves that the two site sets have no coincident point. It remains in the
-- signature so this operation composes at the same annotation boundary as the
-- other constrained joins without inventing a geometry-only facade.
joinSeparatedConstrainedWith
  :: (annotation -> annotation -> annotation)
  -> Triangulation 'Constrained annotation () () ()
  -> Triangulation 'Constrained annotation () () ()
  -> Either
      (ConstrainedUnionError)
      (ConstrainedSeamResult annotation)
joinSeparatedConstrainedWith _combine left right = do
  seamPlan <- maybe (Left ConstraintUnionNotSeparated) Right (planSeam left right)
  seamExecution <-
    mapLeft
      (ConstraintUnionConstructionFailed . CdtBuildError)
      (executeConstrainedSeam seamPlan left right)
  let copied = seamExecutionTriangulation seamExecution
      leftSegments = constraintSegments left
      rightSegments = constraintSegments right
      copiedSegments = Set.fromList (V.toList (constraintSegments copied))
      missingSegments =
        V.filter (`Set.notMember` copiedSegments) (leftSegments V.++ rightSegments)
  requests <- traverse (segmentRequest copied) (V.toList missingSegments)
  recovered <-
    mapLeft ConstraintUnionConstructionFailed
      (recoverConstraints copied (V.fromList requests))
  case firstRejected (constraintBatchOutcomes recovered) of
    Just blocking ->
      Left
        ( ConstraintUnionConstructionFailed
            (ConstraintIntersection blocking)
        )
    Nothing -> do
      published <-
        mapLeft
          (ConstraintUnionConstructionFailed . CdtBuildError)
          (canonicalize (constraintBatchTriangulation recovered))
      targetFaces <- targetFaceIndex published
      leftFaces <-
        sourceFaceEvidence
          ConstrainedSeamLeftSource
          left
          targetFaces
      rightFaces <-
        sourceFaceEvidence
          ConstrainedSeamRightSource
          right
          targetFaces
      let preservedTargets =
            Set.fromList
              ( fmap constrainedSeamTargetFace (V.toList leftFaces)
                  <> fmap constrainedSeamTargetFace (V.toList rightFaces)
              )
          newFaces =
            V.fromList
              ( filter
                  (`Set.notMember` preservedTargets)
                  (innerFaceIds published)
              )
          recoveredOutcomes =
            Map.fromList
              (V.toList (V.zip missingSegments (constraintBatchOutcomes recovered)))
          finalSegments = Set.fromList (V.toList (constraintSegments published))
      leftConstraintEvidence <-
        sourceConstraintEvidence
          ConstrainedSeamLeftSource
          finalSegments
          recoveredOutcomes
          leftSegments
      rightConstraintEvidence <-
        sourceConstraintEvidence
          ConstrainedSeamRightSource
          finalSegments
          recoveredOutcomes
          rightSegments
      pure
        ConstrainedSeamResult
          { constrainedSeamResultTriangulation = published
          , constrainedSeamLeftFaceEvidence = leftFaces
          , constrainedSeamRightFaceEvidence = rightFaces
          , constrainedSeamNewFaces = newFaces
          , constrainedSeamLeftConstraintEvidence = leftConstraintEvidence
          , constrainedSeamRightConstraintEvidence = rightConstraintEvidence
          , constrainedSeamConstraintStats = constraintBatchStats recovered
          , constrainedSeamBuildStats = seamExecutionBuildStats seamExecution
          }

data CanonicalFaceKey = CanonicalFaceKey
  !(Point)
  !(Point)
  !(Point)
  deriving stock (Eq, Ord)

targetFaceIndex
  :: Triangulation mode vertex directed undirected face
  -> Either
      (ConstrainedUnionError)
      (Map.Map CanonicalFaceKey FaceId)
targetFaceIndex triangulation =
  foldM insertTarget Map.empty (innerFaceIds triangulation)
 where
  insertTarget
    :: Map.Map CanonicalFaceKey FaceId
    -> FaceId
    -> Either (ConstrainedUnionError) (Map.Map CanonicalFaceKey FaceId)
  insertTarget index face = do
    key <- targetFaceKey triangulation face
    case Map.lookup key index of
      Just existing -> Left (ConstraintUnionTargetFaceAmbiguous existing face)
      Nothing -> Right (Map.insert key face index)

sourceFaceEvidence
  :: ConstrainedSeamSource
  -> Triangulation mode vertex directed undirected face
  -> Map.Map CanonicalFaceKey FaceId
  -> Either
      (ConstrainedUnionError)
      (V.Vector (ConstrainedSeamFaceEvidence))
sourceFaceEvidence source triangulation targetFaces =
  V.fromList <$> traverse evidenceFor (innerFaceIds triangulation)
 where
  evidenceFor sourceFace = do
    key@(CanonicalFaceKey first second third) <-
      sourceFaceKey source triangulation sourceFace
    targetFace <-
      maybe
        (Left (ConstraintUnionSourceFaceNotPreserved source sourceFace))
        Right
        (Map.lookup key targetFaces)
    pure
      ConstrainedSeamFaceEvidence
        { constrainedSeamSourceFace = sourceFace
        , constrainedSeamTargetFace = targetFace
        , constrainedSeamFaceFirstPoint = first
        , constrainedSeamFaceSecondPoint = second
        , constrainedSeamFaceThirdPoint = third
        }

sourceConstraintEvidence
  :: ConstrainedSeamSource
  -> Set.Set (CanonicalSegment)
  -> Map.Map (CanonicalSegment) ConstraintOutcome
  -> V.Vector (CanonicalSegment)
  -> Either
      (ConstrainedUnionError)
      (V.Vector (ConstrainedSeamConstraintEvidence))
sourceConstraintEvidence source finalSegments recovered =
  traverse
    (\segment ->
       if Set.member segment finalSegments
         then
           Right
             ConstrainedSeamConstraintEvidence
               { constrainedSeamConstraintSegment = segment
               , constrainedSeamConstraintRecovery = Map.lookup segment recovered
               }
         else Left (ConstraintUnionSourceConstraintNotPreserved source segment)
    )

sourceFaceKey
  :: ConstrainedSeamSource
  -> Triangulation mode vertex directed undirected face
  -> FaceId
  -> Either (ConstrainedUnionError) CanonicalFaceKey
sourceFaceKey source triangulation face =
  case sort (fmap (Dcel.vertexPoint triangulation) (Dcel.faceVertices triangulation face)) of
    [first, second, third] -> Right (CanonicalFaceKey first second third)
    points ->
      Left
        ( ConstraintUnionSourceFaceNotTriangular
            source
            face
            (length points)
        )

targetFaceKey
  :: Triangulation mode vertex directed undirected face
  -> FaceId
  -> Either (ConstrainedUnionError) CanonicalFaceKey
targetFaceKey triangulation face =
  case sort (fmap (Dcel.vertexPoint triangulation) (Dcel.faceVertices triangulation face)) of
    [first, second, third] -> Right (CanonicalFaceKey first second third)
    points -> Left (ConstraintUnionTargetFaceNotTriangular face (length points))

innerFaceIds
  :: Triangulation mode vertex directed undirected face
  -> [FaceId]
innerFaceIds triangulation =
  fmap (FaceId . fromIntegral) [1 .. Dcel.numFaces triangulation - 1]

-- | Extend one already-resident constrained triangulation with one new
-- constrained section. This is intentionally asymmetric: the base mesh is
-- thawed once, extension sites are inserted into it, and only the extension's
-- constraint section is replayed. Unlike 'unionConstrainedWith', it neither
-- rebuilds a canonical site set nor replays base constraints, because both
-- would erase the physical distinction between solved base and new work.
--
-- Incoming constraint recovery is itself the spatial conflict authority. It
-- walks only the incoming corridors against the resident base and returns a
-- typed intersection obstruction. Re-running the canonical all-pairs union
-- preflight here would make a tiny extension quadratic in the base. Any
-- structural or recovery obstruction abandons the transaction before a
-- partially extended mesh can be published.
extendConstrainedWith
  :: (annotation -> annotation -> annotation)
  -> Triangulation 'Constrained annotation () () ()
  -> Triangulation 'Constrained annotation () () ()
  -> Either
      (ConstrainedUnionError)
      (ConstrainedExtensionResult annotation () () ())
extendConstrainedWith combine base extension = do
    (completed, extended, buildStats) <-
      runTransaction
        (ConstraintUnionConstructionFailed . CdtBuildError)
        transactionShape
        base
        (siteSetSize extensionSites)
        (insertAndRecoverExtension combine extensionSites extensionSegments)
    pure
      ConstrainedExtensionResult
        { constrainedExtensionTriangulation = extended
        , constrainedExtensionConstraintOutcomes = accumulatorOutcomes completed
        , constrainedExtensionConstraintStats = accumulatedConstraintStats completed
        , constrainedExtensionBuildStats = buildStats
        }
 where
  extensionSites = siteSetFromTriangulation extension
  extensionSegments = constraintSegments extension
  transactionShape =
    case V.uncons extensionSegments of
      Just (segment, remaining)
        | Dcel.numVertices base >= 200000
        , siteSetSize extensionSites <= 128
        , V.null remaining
        , residentCorridorIsEmpty segment -> LocalTransaction
      _ -> DenseTransaction
  residentCorridorIsEmpty segment =
    case (mkQueryPoint (segmentStart segment), mkQueryPoint (segmentEnd segment)) of
      (Right from, Right to) ->
        foldCorridorBetweenPoints base from to (\_ _ -> Left ()) () == Just (Right ())
      _ -> False
{-# INLINE extendConstrainedWith #-}

insertAndRecoverExtension
  :: (annotation -> annotation -> annotation)
  -> SiteSet annotation
  -> V.Vector (CanonicalSegment)
  -> MutableDcel s annotation () () ()
  -> OperationState s
  -> ST s (Either (ConstrainedUnionError) ConstraintBatchAccumulator)
insertAndRecoverExtension combine extensionSites extensionSegments mutable operation = do
  placed <- insertExtensionSites combine extensionSites mutable operation
  case placed of
    Left obstruction -> pure (Left obstruction)
    Right handles ->
      case traverse (segmentRequestFromHandles handles) (V.toList extensionSegments) of
        Left obstruction -> pure (Left obstruction)
        Right requests -> do
          interpreted <-
            fmap
              (mapLeft ConstraintUnionConstructionFailed)
              (interpretConstraintRequests (V.fromList requests) mutable operation)
          case interpreted of
            Left obstruction -> pure (Left obstruction)
            Right completed ->
              case firstRejected (accumulatorOutcomes completed) of
                Just blocking ->
                  pure
                    ( Left
                        ( ConstraintUnionConstructionFailed
                            (ConstraintIntersection blocking)
                        )
                    )
                Nothing -> pure (Right completed)

insertExtensionSites
  :: forall s annotation
   . (annotation -> annotation -> annotation)
  -> SiteSet annotation
  -> MutableDcel s annotation () () ()
  -> OperationState s
  -> ST s (Either (ConstrainedUnionError) (Map.Map (Point) VertexId))
insertExtensionSites combine extensionSites mutable operation =
  fmap
    (fmap (Map.fromDistinctAscList . reverse))
    ( foldWhileM
        isRight
        insertOne
        (Right [])
        (siteSetAssocs extensionSites)
    )
 where
  insertOne
    :: Either (ConstrainedUnionError) [(Point, VertexId)]
    -> (Point, annotation)
    -> ST s (Either (ConstrainedUnionError) [(Point, VertexId)])
  insertOne rejected@(Left _) _ = pure rejected
  insertOne (Right accumulated) (point, annotation) =
    fmap
      ( mapLeft (ConstraintUnionConstructionFailed . CdtBuildError)
          . fmap
            (\(vertex, _) -> (point, VertexId (fromIntegral vertex)) : accumulated)
      )
      (insertPointCombining combine Nothing mutable operation point annotation)

accumulatorOutcomes :: ConstraintBatchAccumulator -> V.Vector ConstraintOutcome
accumulatorOutcomes = V.fromList . reverse . accumulatedConstraintOutcomes
{-# INLINE accumulatorOutcomes #-}

segmentRequest
  :: Triangulation mode annotation () () ()
  -> CanonicalSegment
  -> Either (ConstrainedUnionError) (VertexId, VertexId)
segmentRequest triangulation segment =
  segmentRequestFromHandles handles segment
 where
  handles =
    Map.fromList
      [ ( Dcel.vertexPoint triangulation vertex
        , vertex
        )
      | raw <- [0 .. Dcel.numVertices triangulation - 1]
      , let vertex = VertexId (fromIntegral raw)
      ]

segmentRequestFromHandles
  :: Map.Map (Point) VertexId
  -> CanonicalSegment
  -> Either (ConstrainedUnionError) (VertexId, VertexId)
segmentRequestFromHandles handles segment =
  case
      ( Map.lookup (segmentStart segment) handles
      , Map.lookup (segmentEnd segment) handles
      ) of
    (Just from, Just to) -> Right (from, to)
    (Nothing, _) -> Left (ConstraintUnionSiteMissing (segmentStart segment))
    (_, Nothing) -> Left (ConstraintUnionSiteMissing (segmentEnd segment))

firstRejected :: V.Vector ConstraintOutcome -> Maybe UndirectedEdgeId
firstRejected =
  V.foldr
    (\outcome later ->
       case outcome of
         ConstraintAccepted _ _ -> later
         ConstraintRejected blocking -> Just blocking
    )
    Nothing

completeConstraintConflicts
  :: SiteSet annotation
  -> V.Vector (CanonicalSegment)
  -> Set.Set (ConstraintConflict)
completeConstraintConflicts unionSites segments =
  Set.fromList
    [ orderedConflict leftSegment rightSegment
    | leftIndex <- [0 .. V.length segments - 1]
    , rightIndex <- [leftIndex + 1 .. V.length segments - 1]
    , let leftSegment = segments V.! leftIndex
    , let rightSegment = segments V.! rightIndex
    , segmentsProperlyCross
        (segmentStart leftSegment)
        (segmentEnd leftSegment)
        (segmentStart rightSegment)
        (segmentEnd rightSegment)
    , not (crossingIsRepresented unionSites leftSegment rightSegment)
    ]

orderedConflict
  :: CanonicalSegment
  -> CanonicalSegment
  -> ConstraintConflict
orderedConflict left right
  | left <= right = ConstraintConflict left right
  | otherwise = ConstraintConflict right left

crossingIsRepresented
  :: SiteSet annotation
  -> CanonicalSegment
  -> CanonicalSegment
  -> Bool
crossingIsRepresented sites first second =
  V.any
    (\point ->
       onClosedSegment (segmentStart first) (segmentEnd first) point
         && onClosedSegment (segmentStart second) (segmentEnd second) point
    )
    (siteSetPoints sites)