packages feed

moonlight-triangulation-1.2.0.0: src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Normalize.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fllvm -optlo-O3 -optlc-O3 #-}

-- | The normalization procedure of the flip rewrite system.
module Moonlight.Triangulation.Internal.DcelOperations.Normalize
  ( drainLegalization
  , LegalizationDrain (..)
  , drainDenseUnconstrainedStarLegalization
  , drainDenseUnconstrainedGenericLegalization
  ) where

import Control.Monad (when)
import Control.Monad.ST (ST)
import Data.Bits (shiftR)
import Data.STRef (readSTRef)
import qualified Data.Vector.Unboxed.Mutable as MUV
import Data.Word (Word32)
import Moonlight.Triangulation.Internal.DcelOperations.CandidateArena
  ( CandidateDiscipline (..)
  , growLegalizationArena
  )
import Moonlight.Triangulation.Internal.DcelOperations.FlipRewrite (applyFlip)
import Moonlight.Triangulation.Internal.DcelOperations.FlipRule
  ( LegalizationLaw (..)
  , diagonalFires
  , illegalDiagonal
  )
import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
import Moonlight.Triangulation.Internal.Mutable
  ( DenseMutableDcel
  , MutableDcel (..)
  , MutableTopology (..)
  )
import Moonlight.Triangulation.Internal.OperationState
  ( Counter (..)
  , LegalizationArena (..)
  , OperationState
  , legalizationArena
  , storeLegalizationArena
  )
import Moonlight.Triangulation.Internal.PackedIndex (packIndex)
import Moonlight.Triangulation.Internal.Probe
  ( KnownProbe (..)
  , Probe (..)
  , ProbeCounter
  )

-- | The authoritative result of one normalization section. The arena is part
-- of the result because an adversarial frontier may grow it; callers that
-- borrow the section for several local rewrites can therefore compose those
-- rewrites without bouncing through the operation's reference between them.
data LegalizationDrain s counter = LegalizationDrain
  {-# UNPACK #-} !Int
  {-# UNPACK #-} !Int
  !counter
  !(LegalizationArena s)

-- | The normalization procedure of a confluent terminating rewrite system, and
-- one canonical legalization engine because a normalization procedure is what
-- it is.
--
-- The objects are the triangulations of a fixed point set. The single rule is
-- the Lawson flip: a locally illegal diagonal is replaced by the other
-- diagonal of its quadrilateral. A normal form is a mesh with no illegal
-- diagonal left to fire on.
--
-- /Termination/ is by the lifted-paraboloid potential. Send each point to
-- @(x, y, x² + y²)@ and read a triangulation as a piecewise-linear surface
-- over the point set; @illegalDiagonal@ is exactly the test that the flip
-- lowers that surface, so every rewrite strictly decreases it, and a finite
-- point set has finitely many triangulations. Exact cocircularity is the one
-- case where the surface does not move — the four lifted points are coplanar
-- and both diagonals give the same surface — so there the potential is the
-- diagonal's own key order, and the rule fires only downward in it. Without
-- that tie-break a cocircular quadrilateral flips forever.
--
-- /Confluence/ is Delaunay's theorem, in its strong form: a triangulation with
-- no locally illegal diagonal is globally Delaunay. Local normality is thus
-- global normality, the normal form is unique, and every rewrite order reaches
-- it. That is what licenses the arena below to be a LIFO stack rather than a
-- priority queue, and it is why callers may seed it in whatever order is
-- cheapest to produce — the fan first, or the hull turns first, or both
-- interleaved — without any of them changing the mesh that comes out.
--
-- All callers differ only in how they seed the arena; topology mutation and
-- propagation have exactly one owner.
-- The stack top, the maximum top, and the flip count are strict loop
-- variables, returned once when the drain finishes — the mesh reports nothing
-- per candidate, and 'applyFlip' reports nothing at all. The phantom
-- 'KnownProbe' parameter counts popped candidates for the instrumented lane
-- and is erased everywhere else.
--
-- A popped candidate is read once. Turning it against the star vertex, judging
-- it, and rewriting it are three questions about the same two half-edge
-- records, and the apex the turn looks for is the apex the judgement needs, so
-- one pass over the quadrilateral answers all three.
drainLegalization
  :: forall p mutable s vertex directed undirected face
   . (KnownProbe p, MutableTopology mutable)
  => mutable s vertex directed undirected face
  -> OperationState s
  -> Int
  -> CandidateDiscipline
  -> LegalizationLaw
  -> ST s (Int, Int)
drainLegalization topology operation seededTop discipline law = do
  let !mutable = topologyOwner topology
  -- No constraint can appear during a drain, so a mesh holding none at entry
  -- never needs the per-candidate protection read.
  constrained <- readSTRef (mdConstraintCount mutable)
  initialArena <- legalizationArena operation
  LegalizationDrain flips maxTop candidates finalArena <-
    drainLegalizationInArena
      @p
      topology
      initialArena
      (constrained /= 0)
      seededTop
      discipline
      law
  when (legalizationArenaLength finalArena /= legalizationArenaLength initialArena) $
    storeLegalizationArena operation finalArena
  probeCharge @p operation CounterDiagLegalizationCandidates candidates
  pure (flips, maxTop)
{-# INLINE drainLegalization #-}

-- | Interpret the single normalization law in an explicitly borrowed arena.
-- The boolean is the already-established constraint obstruction: a fresh
-- unconstrained build passes 'False', while the general entry derives it once
-- from the mutable owner above.
drainLegalizationInArena
  :: forall p mutable s vertex directed undirected face
   . (KnownProbe p, MutableTopology mutable)
  => mutable s vertex directed undirected face
  -> LegalizationArena s
  -> Bool
  -> Int
  -> CandidateDiscipline
  -> LegalizationLaw
  -> ST s (LegalizationDrain s (ProbeCounter p))
drainLegalizationInArena topology (LegalizationArena initialArena) guarded seededTop discipline law = do
  let -- Everything the cavity fan did not create is pinned. The border loop's
      -- legality is already decided by the outside triangle it keeps, and
      -- testing it against a neighbourhood that is still inverted could only
      -- produce a spurious verdict. 'ValidMesh' pins nothing, which is the
      -- floor no undirected index falls below.
      !floorPair = case law of
        ValidMesh -> 0
        CavityRepair floorEdge -> floorEdge

      eligible rawEdge
        | rawEdge `shiftR` 1 < floorPair = pure False
        | guarded = not <$> topologyReadConstraint topology rawEdge
        | otherwise = pure True

      loopStar !starVertex !starX !starY !arena !top !maxTop !flips !candidates
        | top <= 0 = pure (flips, maxTop, candidates, arena)
        | otherwise = do
            let !nextTop = top - 1
            packedWord <- MUV.unsafeRead arena nextTop
            let !rawEdge = fromIntegral packedWord :: Int
            mayFire <- eligible rawEdge
            if not mayFire
              then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
              else do
                let !rawTwin = reverseIndex rawEdge
                rawFace <- topologyReadFace topology rawEdge
                rawTwinFace <- topologyReadFace topology rawTwin
                if rawFace == 0 || rawTwinFace == 0
                  then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
                  else do
                    rawBefore <- topologyReadPrevious topology rawEdge
                    rawTwinBefore <- topologyReadPrevious topology rawTwin
                    rawTwinApex <- topologyReadOrigin topology rawTwinBefore
                    a <- topologyReadOrigin topology rawEdge
                    b <- topologyReadOrigin topology rawTwin
                    ax <- topologyReadPointX topology a
                    ay <- topologyReadPointY topology a
                    bx <- topologyReadPointX topology b
                    by <- topologyReadPointY topology b
                    dx <- topologyReadPointX topology rawTwinApex
                    dy <- topologyReadPointY topology rawTwinApex
                    -- A star candidate is oriented against the inserted apex,
                    -- exactly the premise used by the ordinary insertion
                    -- legalizer. The two convexity predicates in the generic
                    -- valid-mesh law merely re-prove that premise for every
                    -- pop; the in-circle rule alone owns this section.
                    if not (illegalDiagonal ax ay bx by starX starY dx dy)
                      then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
                      else do
                        -- Star seeding is directional: the inserted vertex is
                        -- the previous-origin apex of every candidate. A flip
                        -- preserves that proof for precisely these two
                        -- directed neighbours, so neither an undirected tag nor
                        -- a rediscovery read belongs in this epoch.
                        rawNext <- topologyReadNext topology rawEdge
                        rawTwinNext <- topologyReadNext topology rawTwin
                        applyFlip
                          topology
                          rawEdge
                          rawTwin
                          rawNext
                          rawBefore
                          rawTwinNext
                          rawTwinBefore
                          rawFace
                          rawTwinFace
                          a
                          b
                          starVertex
                          rawTwinApex
                        let !addedTop = nextTop + 2
                        grown <- growValues arena addedTop
                        MUV.unsafeWrite grown nextTop (packIndex rawTwinBefore)
                        MUV.unsafeWrite grown (nextTop + 1) (packIndex rawTwinNext)
                        loopStar
                          starVertex
                          starX
                          starY
                          grown
                          addedTop
                          (max maxTop addedTop)
                          (flips + 1)
                          (probeBump @p candidates)

      loopGeneric !arena !top !maxTop !flips !candidates
        | top <= 0 = pure (flips, maxTop, candidates, arena)
        | otherwise = do
            let !nextTop = top - 1
            packedWord <- MUV.unsafeRead arena nextTop
            let !edge = fromIntegral packedWord :: Int
            mayFire <- eligible edge
            if not mayFire
              then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
              else do
                let !twin = reverseIndex edge
                leftFace <- topologyReadFace topology edge
                rightFace <- topologyReadFace topology twin
                if leftFace == 0 || rightFace == 0
                  then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
                  else do
                    edgePrevious <- topologyReadPrevious topology edge
                    twinPrevious <- topologyReadPrevious topology twin
                    c <- topologyReadOrigin topology edgePrevious
                    d <- topologyReadOrigin topology twinPrevious
                    a <- topologyReadOrigin topology edge
                    b <- topologyReadOrigin topology twin
                    ax <- topologyReadPointX topology a
                    ay <- topologyReadPointY topology a
                    bx <- topologyReadPointX topology b
                    by <- topologyReadPointY topology b
                    cx <- topologyReadPointX topology c
                    cy <- topologyReadPointY topology c
                    dx <- topologyReadPointX topology d
                    dy <- topologyReadPointY topology d
                    if not (diagonalFires law ax ay bx by cx cy dx dy)
                      then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
                      else do
                        edgeNext <- topologyReadNext topology edge
                        twinNext <- topologyReadNext topology twin
                        applyFlip
                          topology
                          edge
                          twin
                          edgeNext
                          edgePrevious
                          twinNext
                          twinPrevious
                          leftFace
                          rightFace
                          a
                          b
                          c
                          d
                        let !addedTop = nextTop + 4
                        grown <- growValues arena addedTop
                        MUV.unsafeWrite grown nextTop (packIndex edgeNext)
                        MUV.unsafeWrite grown (nextTop + 1) (packIndex edgePrevious)
                        MUV.unsafeWrite grown (nextTop + 2) (packIndex twinNext)
                        MUV.unsafeWrite grown (nextTop + 3) (packIndex twinPrevious)
                        loopGeneric
                          grown
                          addedTop
                          (max maxTop addedTop)
                          (flips + 1)
                          (probeBump @p candidates)
  drained <-
    case discipline of
      StarCandidates starVertex -> do
        starX <- topologyReadPointX topology starVertex
        starY <- topologyReadPointY topology starVertex
        loopStar starVertex starX starY initialArena seededTop seededTop 0 (probeZero @p)
      GenericCandidates ->
        loopGeneric initialArena seededTop seededTop 0 (probeZero @p)
  let (!flips, !maxTop, !candidates, !finalArena) = drained
  pure (LegalizationDrain flips maxTop candidates (LegalizationArena finalArena))
 where
  growValues :: MUV.MVector s Word32 -> Int -> ST s (MUV.MVector s Word32)
  growValues values required = do
    LegalizationArena grown <-
      growLegalizationArena (LegalizationArena values) required
    pure grown
{-# INLINE drainLegalizationInArena #-}

-- | The dense, fresh-build interpreter for a star epoch. Fresh construction
-- proves the absence of constrained edges; the borrowed arena is returned so
-- the surrounding sweep can glue several epochs before restoring operation
-- ownership.
drainDenseUnconstrainedStarLegalization
  :: DenseMutableDcel s vertex directed undirected face
  -> LegalizationArena s
  -> Int
  -> Int
  -> ST s (LegalizationDrain s ())
drainDenseUnconstrainedStarLegalization dense arena top starVertex =
  drainLegalizationInArena
    @'ProbeOff
    dense
    arena
    False
    top
    (StarCandidates starVertex)
    ValidMesh
{-# INLINE drainDenseUnconstrainedStarLegalization #-}

-- | The matching dense interpreter for a generic legalization epoch. Keeping
-- both monomorphic boundaries out of the circle-sweep worker prevents the
-- normalizer and flip rewrite from being copied into every insertion
-- continuation; both still descend through 'drainLegalizationInArena'.
drainDenseUnconstrainedGenericLegalization
  :: DenseMutableDcel s vertex directed undirected face
  -> LegalizationArena s
  -> Int
  -> ST s (LegalizationDrain s ())
drainDenseUnconstrainedGenericLegalization dense arena top =
  drainLegalizationInArena
    @'ProbeOff
    dense
    arena
    False
    top
    GenericCandidates
    ValidMesh
{-# INLINE drainDenseUnconstrainedGenericLegalization #-}

legalizationArenaLength :: LegalizationArena s -> Int
legalizationArenaLength (LegalizationArena values) = MUV.length values
{-# INLINE legalizationArenaLength #-}