moonlight-triangulation-0.1.0.0: src-dcel/Moonlight/Triangulation/Internal/Mutable.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
module Moonlight.Triangulation.Internal.Mutable
( MutableDcel (..)
, newMutableDcel
, thawTriangulation
, thawTriangulationDense
, freezeTriangulation
, pointCapacity
, halfEdgeCapacity
, pointCount
, connectedCount
, directedEdgeCount
, faceCount
, pointAt
, lookupPointVertex
, activatePointIndex
, activateBatchPointIndex
, discardBatchPointIndex
, identityIndexActive
, readPointX
, readPointY
, writePoint
, vertexDataAt
, writeVertexData
, payloadsPristine
, resetEdgeData
, resetFaceData
, edgeOriginPoint
, appendVertex
, appendVertexCoordinates
, ensurePointCapacity
, markConnected
, isConnected
, addEdge
, addEdgeBlock
, addFace
, addFaceBlock
, ensureCellCapacity
, truncatePoints
, truncateDirectedEdges
, truncateFaces
, swapRemoveUndirectedEdge
, swapRemoveFace
, swapRemoveVertex
, linkEdges
, setCycle3
, faceEdges
, readOrigin
, writeOrigin
, readNext
, writeNext
, readPrevious
, writePrevious
, readFace
, writeFace
, readVertexOut
, writeVertexOut
, readFaceEdge
, writeFaceEdge
, readConstraint
, setConstraint
, clearConstraint
) where
import Control.Monad (foldM, forM_, unless, when)
import Data.Bits (xor)
import Control.Monad.ST (ST)
import qualified Data.IntSet as IntSet
import Data.STRef
( STRef
, modifySTRef'
, newSTRef
, readSTRef
, writeSTRef
)
import Data.Word (Word8, Word32)
import Moonlight.Triangulation.Handles.HandleDefs
( DirectedEdgeId (..)
, FaceId (..)
, UndirectedEdgeId (..)
, VertexId (..)
)
import Moonlight.Triangulation.Internal.BoxedPaged
import Moonlight.Triangulation.Internal.PackedIndex (noIndex, packIndex)
import Moonlight.Triangulation.Internal.Paged
import Moonlight.Triangulation.Internal.PointIndex
( MutablePointIndex
, MutablePointIndexUpdate (..)
, PointIndex
, buildPointIndex
, emptyPointIndex
, insertPointIndex
, lookupMutablePoint
, newMutablePointIndex
, pointIndexCandidates
, relocateMutablePoint
, relocatePointIndex
, removeMutablePoint
, removePointIndex
, seedMutablePointIndex
)
import Moonlight.Triangulation.Math (canonicalPoint)
import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
import Moonlight.Triangulation.Internal.Types (BuildError (..), ElementDefaults (..), Point (..))
-- | Dormant carries the inherited index through mutations as unforced pure
-- updates; persistent Active is the same index forced and kept strict because
-- a singleton lookup already proved someone is asking. Batch Active is the
-- existing open-addressed owner scoped to one dense identity program. Missing
-- means publication owes a lazy whole-mesh rebuild to any eventual asker.
data MutablePointIndexState s
= DormantPointIndex PointIndex
| ActivePersistentPointIndex !PointIndex
| ActiveBatchPointIndex !(MutablePointIndex s)
| MissingPointIndex
data MutableDcel s vertex directed undirected face = MutableDcel
{ mdPointX :: !(MutablePaged s Double)
, mdPointY :: !(MutablePaged s Double)
, mdPointIndex :: !(STRef s (MutablePointIndexState s))
, mdVertexOut :: !(MutablePaged s Word32)
, mdVertexData :: !(MutableBoxedPaged s vertex)
, mdNewConnected :: !(MutablePaged s Word8)
, mdRecycledNew :: !(STRef s IntSet.IntSet)
, mdHalfTopology :: !(MutablePaged s Word32)
, mdDirectedData :: !(MutableBoxedPaged s directed)
, mdUndirectedData :: !(MutableBoxedPaged s undirected)
, mdConstraint :: !(MutablePaged s Word8)
, mdFaceEdge :: !(MutablePaged s Word32)
, mdFaceData :: !(MutableBoxedPaged s face)
, mdPointCount :: !(STRef s Int)
, mdConnectedCount :: !(STRef s Int)
, mdHalfCount :: !(STRef s Int)
, mdFaceCount :: !(STRef s Int)
, mdConstraintCount :: !(STRef s Int)
, mdConstraintEdges :: !(STRef s IntSet.IntSet)
, mdLastFace :: !(STRef s Int)
, mdInitialPointCount :: {-# UNPACK #-} !Int
, mdPointCapacity :: {-# UNPACK #-} !Int
, mdHalfCapacity :: {-# UNPACK #-} !Int
, mdFaceCapacity :: {-# UNPACK #-} !Int
, -- | No element payload plane held a materialized page at thaw. Nothing
-- inside a transaction can write one: the three planes are written only
-- through the persistent setters, which run outside one, and both the
-- rewrite reset and the swap-compaction relocation below are no-ops while
-- this holds. So it is constant for the transaction's whole life, and a
-- rewrite decides in a predictable branch that it has no label to move.
mdPayloadsPristine :: !Bool
, mdElementDefaults :: !(ElementDefaults directed undirected face)
}
newMutableDcel :: ElementDefaults directed undirected face -> Int -> ST s (MutableDcel s vertex directed undirected face)
newMutableDcel defaults maximumVertices = newMutableDcelFrom DenseTransaction defaults maximumVertices Nothing
-- | Open a local-edit transaction: copy-on-write pages, publication
-- proportional to dirtied pages. The section for singleton persistent verbs.
thawTriangulation
:: Int
-> Triangulation mode vertex directed undirected face
-> ST s (MutableDcel s vertex directed undirected face)
thawTriangulation maximumVertices triangulation =
newMutableDcelFrom
LocalTransaction
(triElementDefaults triangulation)
maximumVertices
(Just triangulation)
-- | Open a batch transaction: one dense copy up front, flat reads and writes
-- thereafter. The section for sessions and every other many-edit operation.
thawTriangulationDense
:: Int
-> Triangulation mode vertex directed undirected face
-> ST s (MutableDcel s vertex directed undirected face)
thawTriangulationDense maximumVertices triangulation =
newMutableDcelFrom
DenseTransaction
(triElementDefaults triangulation)
maximumVertices
(Just triangulation)
newMutableDcelFrom
:: TransactionShape
-> ElementDefaults directed undirected face
-> Int
-> Maybe (Triangulation mode vertex directed undirected face)
-> ST s (MutableDcel s vertex directed undirected face)
newMutableDcelFrom shape mdElementDefaults maximumVertices source = do
let !existingVertices = maybe 0 (pagedLength . triPointX) source
!existingHalfEdges = maybe 0 ((`quot` 4) . pagedLength . triHalfTopology) source
!existingFaces = maybe 1 (pagedLength . triFaceEdge) source
!growthVertexCapacity = max 1 (max existingVertices maximumVertices)
!vertexCapacity = growthVertexCapacity
-- Euler bounds a planar triangulation's LIVE cells at 3n - 6 undirected
-- edges and 2n - 4 faces, but these arenas are append-only with no free
-- list, so what must be reserved is CUMULATIVE allocation, not the live
-- maximum. Measured: a complete 5-vertex triangulation (9 edges, 6 faces,
-- V - E + F = 2) requests a seventh face without gaining a vertex, so
-- some operation abandons a slot it will never reclaim. The slack below
-- is headroom over that leakage, not a bound derived from it.
!halfCapacity = max existingHalfEdges (max 2 (8 * growthVertexCapacity + 16))
!faceCapacity = max existingFaces (max 1 (3 * growthVertexCapacity + 8))
vertexDataBase = maybe (emptyBoxedPaged Nothing) triVertexData source
directedDataBase = maybe (emptyBoxedPaged (Just (defaultDirectedEdgeData mdElementDefaults))) triDirectedData source
undirectedDataBase = maybe (emptyBoxedPaged (Just (defaultUndirectedEdgeData mdElementDefaults))) triUndirectedData source
faceDataBase = maybe (emptyBoxedPaged (Just (defaultFaceData mdElementDefaults))) triFaceData source
constraintBaseCount = maybe 0 triConstraintCount source
constraintBaseEdges = maybe IntSet.empty triConstraintEdges source
pointIndexBase = maybe MissingPointIndex (DormantPointIndex . triPointIndex) source
mdPointX <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triPointX) source
mdPointY <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triPointY) source
mdVertexOut <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triVertexOut) source
mdVertexData <- thawBoxedPaged vertexDataBase
mdNewConnected <- newLocalMutablePaged (vertexCapacity - existingVertices)
mdRecycledNew <- newSTRef IntSet.empty
mdHalfTopology <- maybe (newMutablePaged (4 * halfCapacity)) (thawPagedShaped shape (4 * halfCapacity) . triHalfTopology) source
mdDirectedData <- thawBoxedPaged directedDataBase
mdUndirectedData <- thawBoxedPaged undirectedDataBase
mdConstraint <- maybe (newMutablePaged (halfCapacity `quot` 2)) (thawPagedShaped shape (halfCapacity `quot` 2) . triConstraint) source
mdFaceEdge <-
case source of
Just triangulation -> thawPagedShaped shape faceCapacity (triFaceEdge triangulation)
Nothing -> do
freshFaceEdges <- newLocalMutablePaged faceCapacity
writePaged freshFaceEdges 0 noIndex
pure freshFaceEdges
mdFaceData <- thawBoxedPaged faceDataBase
mdPointCount <- newSTRef existingVertices
mdPointIndex <- newSTRef pointIndexBase
mdConnectedCount <- newSTRef existingVertices
mdHalfCount <- newSTRef existingHalfEdges
mdFaceCount <- newSTRef existingFaces
mdConstraintCount <- newSTRef constraintBaseCount
mdConstraintEdges <- newSTRef constraintBaseEdges
mdLastFace <- newSTRef (if existingFaces > 1 then 1 else 0)
let !mdPayloadsPristine =
boxedThawPristine mdDirectedData
&& boxedThawPristine mdUndirectedData
&& boxedThawPristine mdFaceData
pure
MutableDcel
{ mdInitialPointCount = existingVertices
, mdPointCapacity = vertexCapacity
, mdHalfCapacity = halfCapacity
, mdFaceCapacity = faceCapacity
, ..
}
freezeTriangulation :: MutableDcel s vertex directed undirected face -> ST s (Either BuildError (Triangulation mode vertex directed undirected face))
freezeTriangulation MutableDcel
{ mdPointX
, mdPointY
, mdPointIndex
, mdVertexOut
, mdVertexData
, mdHalfTopology
, mdDirectedData
, mdUndirectedData
, mdConstraint
, mdFaceEdge
, mdFaceData
, mdPointCount
, mdHalfCount
, mdFaceCount
, mdConstraintCount
, mdConstraintEdges
, mdElementDefaults
} = do
vertices <- readSTRef mdPointCount
halfEdges <- readSTRef mdHalfCount
faces <- readSTRef mdFaceCount
triConstraintCount <- readSTRef mdConstraintCount
triConstraintEdges <- readSTRef mdConstraintEdges
vertexDataOutcome <- freezeBoxedPaged vertices mdVertexData
directedDataOutcome <- freezeBoxedPaged halfEdges mdDirectedData
undirectedDataOutcome <- freezeBoxedPaged (halfEdges `quot` 2) mdUndirectedData
faceDataOutcome <- freezeBoxedPaged faces mdFaceData
case
(,,,)
<$> vertexDataOutcome
<*> directedDataOutcome
<*> undirectedDataOutcome
<*> faceDataOutcome
of
Left obstruction -> pure (Left (PayloadStorageFailure obstruction))
Right (triVertexData, triDirectedData, triUndirectedData, triFaceData) -> do
triPointX <- freezePaged vertices mdPointX
triPointY <- freezePaged vertices mdPointY
pointIndexState <- readSTRef mdPointIndex
let triPointIndex =
case pointIndexState of
DormantPointIndex residentIndex -> residentIndex
ActivePersistentPointIndex residentIndex -> residentIndex
ActiveBatchPointIndex _ -> buildPointIndex triPointX triPointY
MissingPointIndex -> buildPointIndex triPointX triPointY
triVertexOut <- freezePaged vertices mdVertexOut
triHalfTopology <- freezePaged (4 * halfEdges) mdHalfTopology
triConstraint <- freezePaged (halfEdges `quot` 2) mdConstraint
triFaceEdge <- freezePaged faces mdFaceEdge
let triElementDefaults = mdElementDefaults
pure (Right Triangulation{..})
pointCapacity :: MutableDcel s vertex directed undirected face -> Int
pointCapacity = mdPointCapacity
{-# INLINE pointCapacity #-}
halfEdgeCapacity :: MutableDcel s vertex directed undirected face -> Int
halfEdgeCapacity = mdHalfCapacity
{-# INLINE halfEdgeCapacity #-}
pointCount :: MutableDcel s vertex directed undirected face -> ST s Int
pointCount = readSTRef . mdPointCount
{-# INLINE pointCount #-}
connectedCount :: MutableDcel s vertex directed undirected face -> ST s Int
connectedCount = readSTRef . mdConnectedCount
{-# INLINE connectedCount #-}
directedEdgeCount :: MutableDcel s vertex directed undirected face -> ST s Int
directedEdgeCount = readSTRef . mdHalfCount
{-# INLINE directedEdgeCount #-}
faceCount :: MutableDcel s vertex directed undirected face -> ST s Int
faceCount = readSTRef . mdFaceCount
{-# INLINE faceCount #-}
pointAt :: MutableDcel s vertex directed undirected face -> Int -> ST s (Point)
pointAt MutableDcel{mdPointX, mdPointY} index =
Point <$> readPaged mdPointX index <*> readPaged mdPointY index
{-# INLINE pointAt #-}
-- | Resolve a canonical site through the derived handle index, confirming
-- every hash candidate against the authoritative coordinate planes. A mesh
-- created from scratch derives the index only if a caller actually asks; a
-- thawed published mesh inherits its structurally shared index.
lookupPointVertex
:: MutableDcel s vertex directed undirected face
-> Point
-> ST s (Maybe Int)
lookupPointVertex mutable@MutableDcel{mdPointIndex} rawPoint = do
indexState <- readSTRef mdPointIndex
case canonicalPoint rawPoint of
Point x y ->
case indexState of
ActiveBatchPointIndex table ->
lookupMutablePoint table (readPointX mutable) (readPointY mutable) x y
ActivePersistentPointIndex residentIndex ->
resolvePersistent x y residentIndex
DormantPointIndex residentIndex -> do
writeSTRef mdPointIndex (ActivePersistentPointIndex residentIndex)
resolvePersistent x y residentIndex
MissingPointIndex -> do
derived <- deriveMutablePointIndex mutable
writeSTRef mdPointIndex (ActivePersistentPointIndex derived)
resolvePersistent x y derived
where
resolvePersistent x y pointIndex =
foldM
(confirmCandidate mutable x y)
Nothing
(pointIndexCandidates x y pointIndex)
-- | Whether the transaction has already committed to incremental identity
-- transport. Answering does not force a dormant index's lazy rebuild, which
-- is the point: a per-question caller must not buy a whole-mesh build.
identityIndexActive :: MutableDcel s vertex directed undirected face -> ST s Bool
identityIndexActive MutableDcel{mdPointIndex} = do
indexState <- readSTRef mdPointIndex
pure $ case indexState of
ActivePersistentPointIndex _ -> True
ActiveBatchPointIndex _ -> True
_ -> False
{-# INLINE identityIndexActive #-}
-- | Declare that a singleton handle-keyed rewrite must transport the resident
-- immutable identity section strictly. Dense point-keyed removal uses
-- 'activateBatchPointIndex' instead, so it neither forces nor incrementally
-- allocates the published 'PointIndex'.
activatePointIndex
:: MutableDcel s vertex directed undirected face
-> ST s ()
activatePointIndex mutable@MutableDcel{mdPointIndex} = do
indexState <- readSTRef mdPointIndex
case indexState of
ActivePersistentPointIndex _ -> pure ()
ActiveBatchPointIndex _ -> pure ()
DormantPointIndex residentIndex ->
writeSTRef mdPointIndex (ActivePersistentPointIndex residentIndex)
MissingPointIndex -> do
derived <- deriveMutablePointIndex mutable
writeSTRef mdPointIndex (ActivePersistentPointIndex derived)
-- | Open an identity section for one dense removal program. The table derives
-- only from coordinate authority, and its extent is deliberately narrower
-- than the surrounding session: publication returns to the lazy immutable
-- derivation instead of retaining a second mutable identity owner.
activateBatchPointIndex
:: MutableDcel s vertex directed undirected face
-> ST s (Either BuildError ())
activateBatchPointIndex mutable@MutableDcel{mdPointIndex} = do
vertices <- pointCount mutable
table <- newMutablePointIndex vertices
seeded <-
seedMutablePointIndex
table
vertices
(readPointX mutable)
(readPointY mutable)
case seeded of
Left failure -> pure (Left failure)
Right () -> do
writeSTRef mdPointIndex (ActiveBatchPointIndex table)
pure (Right ())
-- | Close the batch-local identity section after its removal program. Its
-- contents cannot escape @ST@; marking the cache missing makes freeze glue a
-- lazy immutable derivation from the final coordinate arenas.
discardBatchPointIndex
:: MutableDcel s vertex directed undirected face
-> ST s ()
discardBatchPointIndex MutableDcel{mdPointIndex} =
modifySTRef'
mdPointIndex
(\indexState ->
case indexState of
ActiveBatchPointIndex _ -> MissingPointIndex
retained -> retained
)
deriveMutablePointIndex
:: MutableDcel s vertex directed undirected face
-> ST s PointIndex
deriveMutablePointIndex mutable = do
vertices <- pointCount mutable
foldM insertResident emptyPointIndex [0 .. vertices - 1]
where
insertResident pointIndex vertex = do
x <- readPointX mutable vertex
y <- readPointY mutable vertex
pure (insertPointIndex x y vertex pointIndex)
confirmCandidate
:: MutableDcel s vertex directed undirected face
-> Double
-> Double
-> Maybe Int
-> Int
-> ST s (Maybe Int)
confirmCandidate _ _ _ resident@(Just _) _ = pure resident
confirmCandidate mutable x y Nothing candidate = do
heldX <- readPointX mutable candidate
heldY <- readPointY mutable candidate
pure (if heldX == x && heldY == y then Just candidate else Nothing)
{-# INLINE confirmCandidate #-}
-- | Read one stored coordinate without building a t'Point'. Coordinates are
-- the authoritative state owned by 'mdPointX'/'mdPointY'; the t'Point'
-- constructor is the cold accessor's packaging, and the construction kernel
-- reads these arenas directly so a specialized sweep never boxes one.
readPointX :: MutableDcel s vertex directed undirected face -> Int -> ST s Double
readPointX MutableDcel{mdPointX} index = readPaged mdPointX index
{-# INLINE readPointX #-}
readPointY :: MutableDcel s vertex directed undirected face -> Int -> ST s Double
readPointY MutableDcel{mdPointY} index = readPaged mdPointY index
{-# INLINE readPointY #-}
writePoint :: MutableDcel s vertex directed undirected face -> Int -> Point -> ST s ()
writePoint MutableDcel{mdPointX, mdPointY} index rawPoint =
case canonicalPoint rawPoint of
Point x y -> do
writePaged mdPointX index x
writePaged mdPointY index y
{-# INLINE writePoint #-}
vertexDataAt :: MutableDcel s vertex directed undirected face -> Int -> ST s vertex
vertexDataAt MutableDcel{mdVertexData} = readBoxedPaged mdVertexData
{-# INLINE vertexDataAt #-}
writeVertexData :: MutableDcel s vertex directed undirected face -> Int -> vertex -> ST s ()
writeVertexData MutableDcel{mdVertexData} = writeBoxedPaged mdVertexData
{-# INLINE writeVertexData #-}
-- | Whether no element payload plane can be holding anything. Constant for the
-- transaction: see 'mdPayloadsPristine'. A rewrite site that performs several
-- resets together tests this once rather than paying the test inside each.
payloadsPristine :: MutableDcel s vertex directed undirected face -> Bool
payloadsPristine = mdPayloadsPristine
{-# INLINE payloadsPristine #-}
-- | Return one undirected edge and both its half-edges to the element
-- defaults. A payload labels the element occupying a slot, and an element is
-- its geometry: a rewrite that gives a slot new endpoints has put a different
-- edge there, and the label the old one carried does not describe it. Leaving
-- it would also make the payload plane depend on the flip order that reached
-- the normal form, while the topology does not.
--
-- The constraint flag is deliberately not reset with it. A flag states that a
-- segment of the input is present, and a segment that gets split is still
-- present as its two halves; a payload states what an element is.
resetEdgeData :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
resetEdgeData MutableDcel{mdDirectedData, mdUndirectedData, mdPayloadsPristine, mdElementDefaults} pair =
unless mdPayloadsPristine $ do
resetBoxedRange mdDirectedData (defaultDirectedEdgeData mdElementDefaults) (2 * pair) 2
resetBoxedRange mdUndirectedData (defaultUndirectedEdgeData mdElementDefaults) pair 1
{-# INLINE resetEdgeData #-}
-- | Return one face to the element default. See 'resetEdgeData'.
resetFaceData :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
resetFaceData MutableDcel{mdFaceData, mdPayloadsPristine, mdElementDefaults} face =
unless mdPayloadsPristine (resetBoxedRange mdFaceData (defaultFaceData mdElementDefaults) face 1)
{-# INLINE resetFaceData #-}
edgeOriginPoint :: MutableDcel s vertex directed undirected face -> Int -> ST s (Point)
edgeOriginPoint mutable edge = readOrigin mutable edge >>= pointAt mutable
{-# INLINE edgeOriginPoint #-}
appendVertex
:: MutableDcel s vertex directed undirected face
-> Point
-> vertex
-> ST s Int
appendVertex mutable rawPoint vertexData =
case canonicalPoint rawPoint of
Point x y -> appendVertexCoordinates mutable x y vertexData
{-# INLINE appendVertex #-}
-- | Append a vertex whose coordinates are already canonical, by components.
-- This is the one owner of the append record; 'appendVertex' is its
-- t'Point'-carrying form for callers holding a point. Raw capacity means the
-- appender initializes every field of the record it exposes.
appendVertexCoordinates
:: MutableDcel s vertex directed undirected face
-> Double
-> Double
-> vertex
-> ST s Int
appendVertexCoordinates mutable@MutableDcel{mdPointCount, mdPointX, mdPointY, mdPointIndex, mdNewConnected, mdRecycledNew} x y vertexData = do
vertex <- readSTRef mdPointCount
writePaged mdPointX vertex x
writePaged mdPointY vertex y
writeVertexData mutable vertex vertexData
writeVertexOut mutable vertex (-1)
if vertex >= mdInitialPointCount mutable
then writePaged mdNewConnected (vertex - mdInitialPointCount mutable) 0
else modifySTRef' mdRecycledNew (IntSet.insert vertex)
modifySTRef'
mdPointIndex
(\pointIndexState ->
case pointIndexState of
ActivePersistentPointIndex pointIndex ->
ActivePersistentPointIndex (insertPointIndex x y vertex pointIndex)
-- The batch table is only lawful over the removal subprogram that
-- opened it. An insertion before that scope is closed invalidates the
-- derived cache rather than pretending an unregistered handle exists.
ActiveBatchPointIndex _ -> MissingPointIndex
-- Transported lazily: the field holds a pure update thunk, so a batch
-- that never asks an identity question pays one allocation per append,
-- while a persistent chain that asks every publication forces a
-- depth-one thunk instead of rebuilding the index over the whole mesh.
DormantPointIndex pointIndex ->
DormantPointIndex (insertPointIndex x y vertex pointIndex)
MissingPointIndex -> MissingPointIndex
)
writeSTRef mdPointCount (vertex + 1)
pure vertex
-- | Check the point arena before a local rewrite materializes vertices. The
-- caller performs this before the first write, so refusal needs no rollback.
ensurePointCapacity
:: MutableDcel s vertex directed undirected face
-> Int
-> ST s (Either BuildError ())
ensurePointCapacity MutableDcel{mdPointCount, mdPointCapacity} additional
| additional < 0 = pure (Left (CapacityExceeded additional))
| otherwise = do
current <- readSTRef mdPointCount
let !required = current + additional
pure $
if required > mdPointCapacity
then Left (CapacityExceeded required)
else Right ()
{-# INLINE ensurePointCapacity #-}
markConnected :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
markConnected mutable@MutableDcel{mdInitialPointCount, mdNewConnected, mdRecycledNew, mdConnectedCount} vertex outgoing = do
if vertex >= mdInitialPointCount
then do
let !newVertex = vertex - mdInitialPointCount
connected <- readPaged mdNewConnected newVertex
unless (connected /= 0) $ do
writePaged mdNewConnected newVertex 1
modifySTRef' mdConnectedCount (+ 1)
else do
recycled <- readSTRef mdRecycledNew
when (IntSet.member vertex recycled) $ do
writeSTRef mdRecycledNew (IntSet.delete vertex recycled)
modifySTRef' mdConnectedCount (+ 1)
writeVertexOut mutable vertex outgoing
{-# INLINE markConnected #-}
isConnected :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
isConnected MutableDcel{mdInitialPointCount, mdNewConnected, mdRecycledNew} vertex
| vertex < mdInitialPointCount = IntSet.notMember vertex <$> readSTRef mdRecycledNew
| otherwise = (/= 0) <$> readPaged mdNewConnected (vertex - mdInitialPointCount)
{-# INLINE isConnected #-}
addEdge :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s (Int, Int)
addEdge mutable from to = do
base <- addEdgeBlock mutable 1
writeOrigin mutable base from
writeOrigin mutable (base + 1) to
pure (base, base + 1)
{-# INLINE addEdge #-}
-- Each appended pair owns the initialization of its exposed topology and
-- constraint records; reserved capacity remains untouched.
addEdgeBlock :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
addEdgeBlock mutable@MutableDcel{mdHalfCount, mdConstraint} pairs = do
base <- readSTRef mdHalfCount
let !required = base + 2 * pairs
!firstUndirected = base `quot` 2
!lastUndirected = firstUndirected + pairs - 1
forM_ [4 * base .. 4 * required - 1] $ \slot ->
writePaged (mdHalfTopology mutable) slot noIndex
forM_ [firstUndirected .. lastUndirected] $ \edge -> writePaged mdConstraint edge 0
writeSTRef mdHalfCount required
pure base
{-# INLINE addEdgeBlock #-}
addFace :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
addFace mutable anchor = do
base <- addFaceBlock mutable 1
writeFaceEdge mutable base anchor
pure base
{-# INLINE addFace #-}
addFaceBlock :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
addFaceBlock mutable@MutableDcel{mdFaceCount} count = do
base <- readSTRef mdFaceCount
let !required = base + count
mapM_ (\face -> writeFaceEdge mutable face (-1)) [base .. required - 1]
writeSTRef mdFaceCount required
pure base
{-# INLINE addFaceBlock #-}
-- | Check the local allocation section before any topology rewrite begins.
-- A refusal leaves every mutable plane untouched, so the enclosing transaction
-- can abandon publication without rollback machinery.
ensureCellCapacity
:: MutableDcel s vertex directed undirected face
-> Int
-> Int
-> ST s (Either BuildError ())
ensureCellCapacity MutableDcel{mdHalfCount, mdHalfCapacity, mdFaceCount, mdFaceCapacity} additionalPairs additionalFaces
| additionalPairs < 0 = pure (Left (HalfEdgeCapacityExceeded additionalPairs mdHalfCapacity))
| additionalFaces < 0 = pure (Left (FaceCapacityExceeded additionalFaces mdFaceCapacity))
| otherwise = do
currentHalfEdges <- readSTRef mdHalfCount
currentFaces <- readSTRef mdFaceCount
let !requiredHalfEdges = currentHalfEdges + 2 * additionalPairs
!requiredFaces = currentFaces + additionalFaces
if requiredHalfEdges > mdHalfCapacity
then pure (Left (HalfEdgeCapacityExceeded requiredHalfEdges mdHalfCapacity))
else
if requiredFaces > mdFaceCapacity
then pure (Left (FaceCapacityExceeded requiredFaces mdFaceCapacity))
else pure (Right ())
{-# INLINE ensureCellCapacity #-}
truncatePoints :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
truncatePoints MutableDcel{mdPointCount, mdConnectedCount} count = do
writeSTRef mdPointCount count
connected <- readSTRef mdConnectedCount
when (connected > count) (writeSTRef mdConnectedCount count)
truncateDirectedEdges :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
truncateDirectedEdges MutableDcel{mdHalfCount} = writeSTRef mdHalfCount
truncateFaces :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
truncateFaces MutableDcel{mdFaceCount, mdLastFace} count = do
writeSTRef mdFaceCount count
lastFace <- readSTRef mdLastFace
when (lastFace >= count) (writeSTRef mdLastFace (if count > 1 then 1 else 0))
-- | Remove one undirected edge by moving the last pair into its slot. Only the
-- two neighboring links, one vertex representative, and one face
-- representative per moved half-edge can reference the old handles.
--
-- The slot the tail vacates is returned to the defaults here rather than when
-- the allocator hands it back out. That keeps one invariant — every slot at or
-- above the live count holds the fill — which the thaw establishes, this
-- preserves, and 'addEdgeBlock' may therefore assume without testing anything.
-- The cost lands on removal, which is where the element was retired, instead of
-- on every allocation a pure insertion makes.
swapRemoveUndirectedEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
swapRemoveUndirectedEdge mutable@MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} pair = do
halfEdges <- directedEdgeCount mutable
let !pairs = halfEdges `quot` 2
!lastPair = pairs - 1
if pair < 0 || pair > lastPair
then
pure
( Left
( RemovalEdgeOutOfRange
(UndirectedEdgeId (fromIntegral pair))
pairs
)
)
else do
swapRemoveUndirectedEdgeInRange lastPair
pure (Right ())
where
swapRemoveUndirectedEdgeInRange lastPair = do
removedFlag <- readPaged mdConstraint pair
lastFlag <- readPaged mdConstraint lastPair
when (removedFlag /= 0) (modifySTRef' mdConstraintCount (subtract 1))
when (pair /= lastPair) $ do
let !oldBase = 2 * lastPair
!newBase = 2 * pair
remap !handle
| handle == oldBase = newBase
| handle == oldBase + 1 = newBase + 1
| otherwise = handle
-- Both records are taken before either is republished: the pair's two
-- half-edges can name each other, so a read after the first write would
-- see the new handle where the old one belongs.
!forwardOrigin <- readOrigin mutable oldBase
!forwardNext <- remap <$> readNext mutable oldBase
!forwardPrevious <- remap <$> readPrevious mutable oldBase
!forwardFace <- readFace mutable oldBase
!backwardOrigin <- readOrigin mutable (oldBase + 1)
!backwardNext <- remap <$> readNext mutable (oldBase + 1)
!backwardPrevious <- remap <$> readPrevious mutable (oldBase + 1)
!backwardFace <- readFace mutable (oldBase + 1)
writePaged mdConstraint pair lastFlag
unless (mdPayloadsPristine mutable) $ do
readBoxedPaged (mdUndirectedData mutable) lastPair >>= writeBoxedPaged (mdUndirectedData mutable) pair
readBoxedPaged (mdDirectedData mutable) oldBase >>= writeBoxedPaged (mdDirectedData mutable) newBase
readBoxedPaged (mdDirectedData mutable) (oldBase + 1) >>= writeBoxedPaged (mdDirectedData mutable) (newBase + 1)
writeOrigin mutable newBase forwardOrigin
writeNext mutable newBase forwardNext
writePrevious mutable newBase forwardPrevious
writeFace mutable newBase forwardFace
writeOrigin mutable (newBase + 1) backwardOrigin
writeNext mutable (newBase + 1) backwardNext
writePrevious mutable (newBase + 1) backwardPrevious
writeFace mutable (newBase + 1) backwardFace
writeNext mutable forwardPrevious newBase
writePrevious mutable forwardNext newBase
writeVertexOut mutable forwardOrigin newBase
writeFaceEdge mutable forwardFace newBase
writeNext mutable backwardPrevious (newBase + 1)
writePrevious mutable backwardNext (newBase + 1)
writeVertexOut mutable backwardOrigin (newBase + 1)
writeFaceEdge mutable backwardFace (newBase + 1)
modifySTRef'
mdConstraintEdges
(\edges ->
let withoutRetired = IntSet.delete pair (IntSet.delete lastPair edges)
in if pair /= lastPair && lastFlag /= 0
then IntSet.insert pair withoutRetired
else withoutRetired
)
resetEdgeData mutable lastPair
truncateDirectedEdges mutable (2 * lastPair)
swapRemoveFace :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
swapRemoveFace mutable face = do
faces <- faceCount mutable
let !lastFace = faces - 1
if face <= 0 || face > lastFace
then
pure
( Left
(RemovalFaceOutOfRange (FaceId (fromIntegral face)) faces)
)
else do
relocated <-
if face == lastFace
then pure (Right ())
else relocateLastFace lastFace
case relocated of
Left obstruction -> pure (Left obstruction)
Right () -> do
resetFaceData mutable lastFace
truncateFaces mutable lastFace
pure (Right ())
where
relocateLastFace lastFace = do
start <- readFaceEdge mutable lastFace
writeFaceEdge mutable face start
-- Swap-compaction moves the last face's index, not the face. When the
-- locator's cached start is that face, following it here is the difference
-- between a batch of removals resuming where the previous one settled and
-- 'truncateFaces' finding the cached index out of range and resetting it to
-- the first inner face. The cache is a start, never an answer.
cachedFace <- readSTRef (mdLastFace mutable)
when (cachedFace == lastFace) (writeSTRef (mdLastFace mutable) face)
unless (mdPayloadsPristine mutable) $
readBoxedPaged (mdFaceData mutable) lastFace >>= writeBoxedPaged (mdFaceData mutable) face
halfEdges <- directedEdgeCount mutable
let go !remaining !current !seen
| remaining <= 0 =
pure
( Left
( RemovalFaceCycleDidNotTerminate
(FaceId (fromIntegral lastFace))
(DirectedEdgeId (fromIntegral current))
(halfEdges + 1)
)
)
| seen && current == start = pure (Right ())
| otherwise = do
writeFace mutable current face
nextEdge <- readNext mutable current
go (remaining - 1) nextEdge True
go (halfEdges + 1) start False
-- | Retire a vertex by moving the arena's last into its slot. The relocation is
-- reported as the slot together with the position now standing in it: the two
-- are one fact, and a caller told only the slot has to consult the mesh to
-- learn what landed there.
swapRemoveVertex :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
swapRemoveVertex mutable@MutableDcel{mdConnectedCount, mdPointIndex, mdRecycledNew, mdNewConnected} vertex = do
vertices <- pointCount mutable
let !lastVertex = vertices - 1
if vertex < 0 || vertex > lastVertex
then
pure
( Left
(RemovalVertexOutOfRange (VertexId (fromIntegral vertex)) vertices)
)
else swapRemoveVertexInRange lastVertex
where
swapRemoveVertexInRange lastVertex = do
removedPoint <- pointAt mutable vertex
removedPayload <- vertexDataAt mutable vertex
movedOutcome <-
if vertex == lastVertex
then pure (Right Nothing)
else moveTailVertex lastVertex
case movedOutcome of
Left obstruction -> pure (Left obstruction)
Right moved -> do
indexState <- readSTRef mdPointIndex
updatedIndexState <-
updatePointIndexAfterSwap mutable indexState removedPoint vertex lastVertex moved
writeSTRef mdPointIndex updatedIndexState
-- The connectivity companions must agree with the aggregate assertion two
-- lines down: after a swap removal every surviving vertex is connected. The
-- retired slot's entry and the relocated occupant's old entry are both
-- stale, and a relocated occupant landing in the appended region must read
-- connected through the offset store, not through its predecessor's bit.
modifySTRef' mdRecycledNew (IntSet.delete vertex . IntSet.delete lastVertex)
case moved of
Just _
| vertex >= mdInitialPointCount mutable ->
writePaged mdNewConnected (vertex - mdInitialPointCount mutable) 1
_ -> pure ()
truncatePoints mutable lastVertex
writeSTRef mdConnectedCount lastVertex
pure (Right (removedPoint, removedPayload, moved))
moveTailVertex lastVertex = do
movedPoint <- pointAt mutable lastVertex
movedPayload <- vertexDataAt mutable lastVertex
movedOut <- readVertexOut mutable lastVertex
writePoint mutable vertex movedPoint
writeVertexData mutable vertex movedPayload
writeVertexOut mutable vertex movedOut
relocated <-
if movedOut < 0
then pure (Right ())
else relocateOutgoingCycle lastVertex movedOut
pure (Just (vertex, movedPoint) <$ relocated)
relocateOutgoingCycle lastVertex movedOut = do
halfEdges <- directedEdgeCount mutable
let go !remaining !current !seen
| remaining <= 0 =
pure
( Left
( RemovalOutgoingCycleDidNotTerminate
(VertexId (fromIntegral lastVertex))
(DirectedEdgeId (fromIntegral current))
(halfEdges + 1)
)
)
| seen && current == movedOut = pure (Right ())
| otherwise = do
writeOrigin mutable current vertex
previousEdge <- readPrevious mutable current
go (remaining - 1) (previousEdge `xor` 1) True
go (halfEdges + 1) movedOut False
-- | Transport the identity view across one vertex swap. Persistent sections
-- retain their existing pure update law. A batch table mutates in place, then
-- deliberately falls back to @MissingPointIndex@ if either local proof cannot
-- be completed; geometry remains authoritative and subsequent operations walk
-- rather than observe a stale cache.
updatePointIndexAfterSwap
:: MutableDcel s vertex directed undirected face
-> MutablePointIndexState s
-> Point
-> Int
-> Int
-> Maybe (Int, Point)
-> ST s (MutablePointIndexState s)
updatePointIndexAfterSwap mutable indexState removedPoint vertex lastVertex moved =
case indexState of
DormantPointIndex pointIndex ->
pure (DormantPointIndex (updatePersistentPointIndex pointIndex))
ActivePersistentPointIndex pointIndex ->
pure (ActivePersistentPointIndex (updatePersistentPointIndex pointIndex))
ActiveBatchPointIndex table ->
transportBatchPointIndex table
MissingPointIndex -> pure MissingPointIndex
where
updatePersistentPointIndex pointIndex =
case removedPoint of
Point removedX removedY ->
let withoutRemoved = removePointIndex removedX removedY vertex pointIndex
in case moved of
Nothing -> withoutRemoved
Just (_, Point movedX movedY) ->
relocatePointIndex movedX movedY lastVertex vertex withoutRemoved
transportBatchPointIndex table =
case removedPoint of
Point removedX removedY -> do
removed <-
removeMutablePoint
table
(readPointX mutable)
(readPointY mutable)
removedX
removedY
vertex
case (removed, moved) of
(MutablePointIndexUpdated, Nothing) ->
pure (ActiveBatchPointIndex table)
(MutablePointIndexUpdated, Just (_, Point movedX movedY)) -> do
relocated <- relocateMutablePoint table movedX movedY lastVertex vertex
pure $
case relocated of
MutablePointIndexUpdated -> ActiveBatchPointIndex table
MutablePointIndexInvalidated -> MissingPointIndex
(MutablePointIndexInvalidated, _) -> pure MissingPointIndex
-- 'swapRemoveVertex' calls us before truncation. The tail's coordinate cells
-- still carry the moved point, so backward-shift repair can derive every
-- occupant home from the same canonical storage that the table indexes.
linkEdges :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
linkEdges mutable left right = do
writeNext mutable left right
writePrevious mutable right left
{-# INLINE linkEdges #-}
setCycle3 :: MutableDcel s vertex directed undirected face -> Int -> Int -> Int -> Int -> ST s ()
setCycle3 mutable face e0 e1 e2 = do
writeNext mutable e0 e1
writeNext mutable e1 e2
writeNext mutable e2 e0
writePrevious mutable e0 e2
writePrevious mutable e1 e0
writePrevious mutable e2 e1
writeFace mutable e0 face
writeFace mutable e1 face
writeFace mutable e2 face
writeFaceEdge mutable face e0
{-# INLINE setCycle3 #-}
faceEdges :: MutableDcel s vertex directed undirected face -> Int -> ST s (Int, Int, Int)
faceEdges mutable face = do
e0 <- readFaceEdge mutable face
e1 <- readNext mutable e0
e2 <- readNext mutable e1
pure (e0, e1, e2)
{-# INLINE faceEdges #-}
readOrigin :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
readOrigin MutableDcel{mdHalfTopology} index = fromIntegral <$> readPaged mdHalfTopology (4 * index)
readNext :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
readNext MutableDcel{mdHalfTopology} index = fromIntegral <$> readPaged mdHalfTopology (4 * index + 1)
readPrevious :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
readPrevious MutableDcel{mdHalfTopology} index = fromIntegral <$> readPaged mdHalfTopology (4 * index + 2)
readFace :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
readFace MutableDcel{mdHalfTopology} index = fromIntegral <$> readPaged mdHalfTopology (4 * index + 3)
{-# INLINE readOrigin #-}
{-# INLINE readNext #-}
{-# INLINE readPrevious #-}
{-# INLINE readFace #-}
writeOrigin :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
writeOrigin MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index) (packIndex value)
writeNext :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
writeNext MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 1) (packIndex value)
writePrevious :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
writePrevious MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 2) (packIndex value)
writeFace :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
writeFace MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 3) (packIndex value)
{-# INLINE writeOrigin #-}
{-# INLINE writeNext #-}
{-# INLINE writePrevious #-}
{-# INLINE writeFace #-}
readVertexOut :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
readVertexOut MutableDcel{mdVertexOut} index = do
value <- readPaged mdVertexOut index
pure (if value == noIndex then -1 else fromIntegral value)
{-# INLINE readVertexOut #-}
writeVertexOut :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
writeVertexOut MutableDcel{mdVertexOut} index value =
writePaged mdVertexOut index (if value < 0 then noIndex else packIndex value)
{-# INLINE writeVertexOut #-}
readFaceEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
readFaceEdge MutableDcel{mdFaceEdge} index = do
value <- readPaged mdFaceEdge index
pure (if value == noIndex then -1 else fromIntegral value)
{-# INLINE readFaceEdge #-}
writeFaceEdge :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
writeFaceEdge MutableDcel{mdFaceEdge} index value =
writePaged mdFaceEdge index (if value < 0 then noIndex else packIndex value)
{-# INLINE writeFaceEdge #-}
readConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
readConstraint MutableDcel{mdConstraint} directed = (/= 0) <$> readPaged mdConstraint (directed `quot` 2)
{-# INLINE readConstraint #-}
setConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
setConstraint MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} directed = do
let !index = directed `quot` 2
current <- readPaged mdConstraint index
if current /= 0
then pure False
else do
writePaged mdConstraint index 1
modifySTRef' mdConstraintCount (+ 1)
modifySTRef' mdConstraintEdges (IntSet.insert index)
pure True
{-# INLINE setConstraint #-}
clearConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
clearConstraint MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} directed = do
let !index = directed `quot` 2
current <- readPaged mdConstraint index
if current == 0
then pure False
else do
writePaged mdConstraint index 0
modifySTRef' mdConstraintCount (subtract 1)
modifySTRef' mdConstraintEdges (IntSet.delete index)
pure True
{-# INLINE clearConstraint #-}