moonlight-planar-1.1.0.0: src-dcel/Moonlight/Planar/Internal/Incidence.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Immutable geometry-free planar incidence. Native meshes share their three
-- packed topology planes; exact arrangements admit the same paired-edge layout
-- once, with sparse additional boundary components and isolated vertices.
module Moonlight.Planar.Internal.Incidence
( PlanarIncidence
, PlanarIncidenceError (..)
, FaceBoundaryComponent (..)
, nativePlanarIncidence
, admitPlanarIncidence
, validatePlanarIncidence
, validateIncidenceEdgeLinks
, incidenceVertexCount
, incidenceDirectedEdgeCount
, incidenceEdgeCount
, incidenceFaceCount
, incidenceNonCellularFaces
, incidenceVertexOutEdge
, incidenceAdjacentEdge
, incidenceOrigin
, incidenceDestination
, incidenceNext
, incidencePrevious
, incidenceIncidentFace
, incidenceUndirectedEndpoints
, incidenceVertexOutgoingEdges
, faceBoundaryComponents
, faceInnerBoundaryComponents
, faceIsolatedVertices
, faceEulerContribution
, packedOptionalEdge
, packedOrigin
, packedNext
, packedPrevious
, packedIncidentFace
, circularEdgeWalk
, circularEdgeFold
) where
import Control.DeepSeq (NFData (..))
import Control.Monad (unless)
import Data.Foldable (traverse_)
import qualified Data.IntMap.Strict as IntMap
import qualified Data.IntSet as IntSet
import Data.Maybe (maybeToList)
import qualified Data.List as List
import Data.Word (Word32)
import GHC.Exts (build)
import GHC.Generics (Generic)
import Moonlight.Planar.Internal.HandleDefs
import Moonlight.Planar.Internal.PackedIndex (indexLimit, unpackOptionalIndex)
import Moonlight.Planar.Internal.Paged (Paged, pagedLength, pagedUnsafeIndex)
import Moonlight.Planar.Internal.Representation (Triangulation (..))
import Moonlight.Planar.Internal.Types (PlanarIncidenceError (..))
-- No Generic or exported record updates: packed admission is the only raw
-- construction boundary. The sparse maps describe components, not geometry.
data PlanarIncidence = PlanarIncidence
!(Paged Word32)
!(Paged Word32)
!(Paged Word32)
!(IntMap.IntMap [DirectedEdgeId])
!(IntMap.IntMap IntSet.IntSet)
deriving stock (Eq, Show)
instance NFData PlanarIncidence where
rnf (PlanarIncidence vertices topology faces components isolated) =
rnf vertices `seq` rnf topology `seq` rnf faces `seq` rnf components `seq` rnf isolated
data FaceBoundaryComponent
= BoundaryCycleRoot !DirectedEdgeId
| IsolatedBoundaryVertex !VertexId
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
-- | O(1) projection retaining the exact same immutable paged arrays. The only
-- native disconnected zero-cell is the singleton mesh, owned by face zero.
nativePlanarIncidence :: Triangulation mode vertex directed undirected face -> PlanarIncidence
nativePlanarIncidence mesh =
PlanarIncidence
(triVertexOut mesh)
(triHalfTopology mesh)
(triFaceEdge mesh)
IntMap.empty
( if pagedLength (triVertexOut mesh) == 1 && pagedLength (triHalfTopology mesh) == 0
then IntMap.singleton 0 (IntSet.singleton 0)
else IntMap.empty
)
{-# INLINE nativePlanarIncidence #-}
-- | Admit one exact arrangement's packed incidence. Geometry, angular order,
-- component orientation and containment are owned by arrangement construction;
-- this boundary checks only the stored combinatorial obligations.
admitPlanarIncidence
:: Paged Word32
-> Paged Word32
-> Paged Word32
-> IntMap.IntMap [DirectedEdgeId]
-> IntMap.IntMap IntSet.IntSet
-> Either PlanarIncidenceError PlanarIncidence
admitPlanarIncidence vertices topology faces components isolated = do
let incidence = PlanarIncidence vertices topology faces components isolated
validatePlanarIncidence incidence
pure incidence
-- | Discharge the same finite laws for either packed admission or the native
-- O(1) projection. Ranges precede every linked read and orbit traversal.
validatePlanarIncidence :: PlanarIncidence -> Either PlanarIncidenceError ()
validatePlanarIncidence incidence@(PlanarIncidence _ topology _ components isolated) = do
let vertexCount = incidenceVertexCount incidence
directedCount = incidenceDirectedEdgeCount incidence
faceCount = incidenceFaceCount incidence
topologyWords = pagedLength topology
vertexIds = fmap (VertexId . fromIntegral) [0 .. vertexCount - 1]
edgeIds = fmap (DirectedEdgeId . fromIntegral) [0 .. directedCount - 1]
faceIds = fmap (FaceId . fromIntegral) [0 .. faceCount - 1]
unless
( topologyWords `rem` 8 == 0 && faceCount > 0
&& vertexCount <= indexLimit && directedCount <= indexLimit && faceCount <= indexLimit
)
(Left (IncidencePackedLayoutInvalid vertexCount topologyWords faceCount))
traverse_ (validateEdgeRanges incidence) edgeIds
traverse_ (validateIncidenceEdgeLinks incidence) edgeIds
traverse_ (validateVertexRoot incidence) vertexIds
traverse_ (validateSparseFace incidence) (IntMap.keys components <> IntMap.keys isolated)
traverse_ (validateFaceRoots incidence) faceIds
let edgeUniverse = IntSet.fromDistinctAscList [0 .. directedCount - 1]
faceEdges = concatMap (concat . faceBoundaryComponents incidence) faceIds
starEdges = concatMap (incidenceVertexOutgoingEdges incidence) vertexIds
validateCoverage IncidenceBoundaryCoverageInvalid edgeUniverse (fmap directedEdgeIdIndex faceEdges)
validateCoverage IncidenceVertexStarCoverageInvalid edgeUniverse (fmap directedEdgeIdIndex starEdges)
let isolatedUniverse = IntSet.fromDistinctAscList
[vertexIdIndex vertex | vertex <- vertexIds, incidenceVertexOutEdge incidence vertex == Nothing]
validateCoverage IncidenceIsolatedOwnershipInvalid isolatedUniverse
(concatMap IntSet.toAscList (IntMap.elems isolated))
validateEdgeRanges :: PlanarIncidence -> DirectedEdgeId -> Either PlanarIncidenceError ()
validateEdgeRanges incidence edge = do
let vertex = incidenceOrigin incidence edge
nextEdge = incidenceNext incidence edge
previousEdge = incidencePrevious incidence edge
face = incidenceIncidentFace incidence edge
unless (vertexIdIndex vertex < incidenceVertexCount incidence)
(Left (IncidenceVertexOriginInvalid edge vertex))
unless (directedEdgeIdIndex nextEdge < incidenceDirectedEdgeCount incidence
&& directedEdgeIdIndex previousEdge < incidenceDirectedEdgeCount incidence)
(Left (IncidenceEdgeLinksInvalid edge nextEdge previousEdge))
unless (faceIdIndex face < incidenceFaceCount incidence)
(Left (IncidenceEdgeFaceInvalid edge face))
-- | Shared local link law after edge/next/previous/twin ranges are discharged.
-- Complete admission and certified native closures use this same predicate.
validateIncidenceEdgeLinks :: PlanarIncidence -> DirectedEdgeId -> Either PlanarIncidenceError ()
validateIncidenceEdgeLinks incidence edge = do
let nextEdge = incidenceNext incidence edge
unless (incidencePrevious incidence nextEdge == edge
&& incidenceNext incidence (incidencePrevious incidence edge) == edge
&& incidenceIncidentFace incidence nextEdge == incidenceIncidentFace incidence edge)
(Left (IncidenceEdgeLinkMismatch edge))
unless (incidenceDestination incidence edge == incidenceOrigin incidence nextEdge)
(Left (IncidenceEdgeEndpointMismatch edge))
validateVertexRoot :: PlanarIncidence -> VertexId -> Either PlanarIncidenceError ()
validateVertexRoot incidence vertex =
case incidenceVertexOutEdge incidence vertex of
Nothing -> Right ()
root@(Just edge) ->
unless (directedEdgeIdIndex edge < incidenceDirectedEdgeCount incidence
&& incidenceOrigin incidence edge == vertex)
(Left (IncidenceVertexRootInvalid vertex root))
validateSparseFace :: PlanarIncidence -> Int -> Either PlanarIncidenceError ()
validateSparseFace incidence index =
unless (index >= 0 && index < incidenceFaceCount incidence)
(Left (IncidenceSparseFaceInvalid index))
validateFaceRoots :: PlanarIncidence -> FaceId -> Either PlanarIncidenceError ()
validateFaceRoots incidence face = do
unless (face == FaceId 0 || incidenceAdjacentEdge incidence face /= Nothing)
(Left (IncidenceBoundedFaceWithoutBoundary face))
traverse_
(\root -> unless (directedEdgeIdIndex root < incidenceDirectedEdgeCount incidence
&& incidenceIncidentFace incidence root == face)
(Left (IncidenceFaceRootInvalid face root)))
(faceCycleRoots incidence face)
validateCoverage
:: (IntSet.IntSet -> IntSet.IntSet -> PlanarIncidenceError)
-> IntSet.IntSet
-> [Int]
-> Either PlanarIncidenceError ()
validateCoverage obstruction expected submitted =
let (observed, repeated) = List.foldl' recordOccurrence (IntSet.empty, IntSet.empty) submitted
in unless (observed == expected && IntSet.null repeated)
(Left (obstruction (IntSet.difference expected observed)
(IntSet.union repeated (IntSet.difference observed expected))))
where
-- Coverage needs membership and duplication, not an allocated integer count
-- at every edge. IntSet keeps dense handle universes in packed bitmap leaves.
recordOccurrence (!observed, !repeated) index =
if IntSet.member index observed
then (observed, IntSet.insert index repeated)
else (IntSet.insert index observed, repeated)
incidenceVertexCount :: PlanarIncidence -> Int
incidenceVertexCount (PlanarIncidence vertices _ _ _ _) = pagedLength vertices
{-# INLINE incidenceVertexCount #-}
incidenceDirectedEdgeCount :: PlanarIncidence -> Int
incidenceDirectedEdgeCount (PlanarIncidence _ topology _ _ _) = pagedLength topology `quot` 4
{-# INLINE incidenceDirectedEdgeCount #-}
incidenceEdgeCount :: PlanarIncidence -> Int
incidenceEdgeCount incidence = incidenceDirectedEdgeCount incidence `quot` 2
{-# INLINE incidenceEdgeCount #-}
incidenceFaceCount :: PlanarIncidence -> Int
incidenceFaceCount (PlanarIncidence _ _ faces _ _) = pagedLength faces
{-# INLINE incidenceFaceCount #-}
-- | Sparse support of bounded faces with holes or punctures. Native meshes
-- answer immediately from their empty maps, without a face scan.
incidenceNonCellularFaces :: PlanarIncidence -> [FaceId]
incidenceNonCellularFaces (PlanarIncidence _ _ _ components isolated) =
fmap (FaceId . fromIntegral) . IntSet.toAscList . IntSet.delete 0 $
IntSet.union
(IntMap.keysSet (IntMap.filter (not . null) components))
(IntMap.keysSet (IntMap.filter (not . IntSet.null) isolated))
incidenceVertexOutEdge :: PlanarIncidence -> VertexId -> Maybe DirectedEdgeId
incidenceVertexOutEdge (PlanarIncidence vertices _ _ _ _) = packedOptionalEdge vertices . vertexIdIndex
{-# INLINE incidenceVertexOutEdge #-}
incidenceAdjacentEdge :: PlanarIncidence -> FaceId -> Maybe DirectedEdgeId
incidenceAdjacentEdge (PlanarIncidence _ _ faces _ _) = packedOptionalEdge faces . faceIdIndex
{-# INLINE incidenceAdjacentEdge #-}
incidenceOrigin :: PlanarIncidence -> DirectedEdgeId -> VertexId
incidenceOrigin (PlanarIncidence _ topology _ _ _) = packedOrigin topology
{-# INLINE incidenceOrigin #-}
incidenceDestination :: PlanarIncidence -> DirectedEdgeId -> VertexId
incidenceDestination incidence = incidenceOrigin incidence . reverseEdge
{-# INLINE incidenceDestination #-}
incidenceNext :: PlanarIncidence -> DirectedEdgeId -> DirectedEdgeId
incidenceNext (PlanarIncidence _ topology _ _ _) = packedNext topology
{-# INLINE incidenceNext #-}
incidencePrevious :: PlanarIncidence -> DirectedEdgeId -> DirectedEdgeId
incidencePrevious (PlanarIncidence _ topology _ _ _) = packedPrevious topology
{-# INLINE incidencePrevious #-}
incidenceIncidentFace :: PlanarIncidence -> DirectedEdgeId -> FaceId
incidenceIncidentFace (PlanarIncidence _ topology _ _ _) = packedIncidentFace topology
{-# INLINE incidenceIncidentFace #-}
incidenceUndirectedEndpoints :: PlanarIncidence -> UndirectedEdgeId -> (VertexId, VertexId)
incidenceUndirectedEndpoints incidence edge =
let forward = normalizedDirected edge
in (incidenceOrigin incidence forward, incidenceDestination incidence forward)
{-# INLINE incidenceUndirectedEndpoints #-}
incidenceVertexOutgoingEdges :: PlanarIncidence -> VertexId -> [DirectedEdgeId]
incidenceVertexOutgoingEdges incidence vertex =
case incidenceVertexOutEdge incidence vertex of
Nothing -> []
Just root -> circularEdgeWalk (incidenceDirectedEdgeCount incidence) root
(reverseEdge . incidencePrevious incidence)
{-# INLINE incidenceVertexOutgoingEdges #-}
faceCycleRoots :: PlanarIncidence -> FaceId -> [DirectedEdgeId]
faceCycleRoots incidence@(PlanarIncidence _ _ _ components _) face =
maybeToList (incidenceAdjacentEdge incidence face)
<> IntMap.findWithDefault [] (faceIdIndex face) components
faceBoundaryComponents :: PlanarIncidence -> FaceId -> [[DirectedEdgeId]]
faceBoundaryComponents incidence =
fmap (\root -> circularEdgeWalk (incidenceDirectedEdgeCount incidence) root (incidenceNext incidence))
. faceCycleRoots incidence
faceIsolatedVertices :: PlanarIncidence -> FaceId -> [VertexId]
faceIsolatedVertices (PlanarIncidence _ _ _ _ isolated) face =
fmap (VertexId . fromIntegral)
(IntSet.toAscList (IntMap.findWithDefault IntSet.empty (faceIdIndex face) isolated))
faceInnerBoundaryComponents :: PlanarIncidence -> FaceId -> [FaceBoundaryComponent]
faceInnerBoundaryComponents incidence@(PlanarIncidence _ _ _ components _) face =
fmap BoundaryCycleRoot
( if face == FaceId 0
then faceCycleRoots incidence face
else IntMap.findWithDefault [] (faceIdIndex face) components
)
<> fmap IsolatedBoundaryVertex (faceIsolatedVertices incidence face)
-- | Compactly supported Euler contribution of the relatively open face.
faceEulerContribution :: PlanarIncidence -> FaceId -> Int
faceEulerContribution incidence face = 1 - length (faceInnerBoundaryComponents incidence face)
-- The fixed packed readers below were extracted from Dcel. Native hot-path
-- callers pass their resident planes directly: no incidence wrapper allocates.
packedOptionalEdge :: Paged Word32 -> Int -> Maybe DirectedEdgeId
packedOptionalEdge plane index = DirectedEdgeId . fromIntegral <$> unpackOptionalIndex (pagedUnsafeIndex plane index)
{-# INLINE packedOptionalEdge #-}
packedOrigin :: Paged Word32 -> DirectedEdgeId -> VertexId
packedOrigin topology edge = VertexId (pagedUnsafeIndex topology (4 * directedEdgeIdIndex edge))
{-# INLINE packedOrigin #-}
packedNext :: Paged Word32 -> DirectedEdgeId -> DirectedEdgeId
packedNext topology edge = DirectedEdgeId (pagedUnsafeIndex topology (4 * directedEdgeIdIndex edge + 1))
{-# INLINE packedNext #-}
packedPrevious :: Paged Word32 -> DirectedEdgeId -> DirectedEdgeId
packedPrevious topology edge = DirectedEdgeId (pagedUnsafeIndex topology (4 * directedEdgeIdIndex edge + 2))
{-# INLINE packedPrevious #-}
packedIncidentFace :: Paged Word32 -> DirectedEdgeId -> FaceId
packedIncidentFace topology edge = FaceId (pagedUnsafeIndex topology (4 * directedEdgeIdIndex edge + 3))
{-# INLINE packedIncidentFace #-}
-- | Shared fusible cycle producer, extracted unchanged from the native DCEL.
circularEdgeWalk :: Int -> DirectedEdgeId -> (DirectedEdgeId -> DirectedEdgeId) -> [DirectedEdgeId]
circularEdgeWalk directedCount start advance =
build
( \(link :: DirectedEdgeId -> result -> result) (stop :: result) ->
let go :: Int -> DirectedEdgeId -> Bool -> result
go !remaining !current !visited
| remaining <= 0 = stop
| visited && current == start = stop
| otherwise = link current (go (remaining - 1) (advance current) True)
in go (directedCount + 1) start False
)
{-# INLINE circularEdgeWalk #-}
circularEdgeFold
:: forall accumulator. Int -> DirectedEdgeId -> (DirectedEdgeId -> DirectedEdgeId)
-> (accumulator -> DirectedEdgeId -> accumulator) -> accumulator -> accumulator
circularEdgeFold directedCount start advance step =
go (directedCount + 1) start False
where
go :: Int -> DirectedEdgeId -> Bool -> accumulator -> accumulator
go !remaining !current !visited !accumulator
| remaining <= 0 = accumulator
| visited && current == start = accumulator
| otherwise =
let !nextAccumulator = step accumulator current
in go (remaining - 1) (advance current) True nextAccumulator
{-# INLINE circularEdgeFold #-}