packages feed

moonlight-planar-1.1.0.0: src-cell-complex/Moonlight/Planar/CellComplex.hs

{-# LANGUAGE ScopedTypeVariables #-}

-- | An admitted exact geometric cell selection as a generic
-- 'CellComplex2D', an integral cellular chain complex, and an exact filtered
-- alpha complex. The 'ExactCellSet' remains the semantic owner: this module
-- supplies only the incidence and Homology interpretations required by
-- downstream topology.
module Moonlight.Planar.CellComplex
  ( DCELComplex,
    DCELCellEdge (..),
    FaceCut,
    faceCutFace,
    faceCutComponent,
    DCELError (..),
    fromExactCellSet,
    finiteChainComplex,
    filteredAlphaComplex,
  )
where

import Data.Bifunctor (first)
import Data.IntMap.Strict qualified as IntMap
import Data.IntSet qualified as IntSet
import Data.List qualified as List
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Vector qualified as Vector
import GHC.Exts (build)
import Moonlight.Algebra.Pure.Orientation (Orientation (..))
import Moonlight.Homology.Boundary
  ( BoundaryIncidence
  , BoundaryIncidenceShapeError
  , BoundaryEntry
  , FiniteChainComplex
  , emptyBoundaryIncidence
  , emptyBoundaryIncidenceOf
  , mkBoundaryEntryFromInts
  , mkBoundaryIncidenceFromOrderedColumns
  , mkBoundaryIncidenceFromOrderedEntries
  , mkFiniteChainComplexChecked
  , targetIndex
  )
import Moonlight.Homology.Chain
  ( HomologicalDegree (..)
  , HomologyFailure
  )
import Moonlight.Homology.Persistence
  ( FilteredFiniteChainComplex
  , mkFilteredFiniteChainComplex
  )
import Moonlight.Homology.Pure.Topology.CellComplex
  ( CellComplex2D (..)
  , CellRef (..)
  , CellTypes (..)
  , OrientedEdge (..)
  , ValidateComplex2D (..)
  )
import Moonlight.Homology.Topology (BasisCellRef (..))
import Moonlight.Planar.Alpha
  ( AlphaBirth
  , AlphaFiltration
  , alphaEdgeBirth
  , alphaFaceBirth
  , alphaVertexBirth
  , withAlphaResidentTriangulation
  )
import Moonlight.Planar.Dcel qualified as Dcel
import Moonlight.Planar.Internal.HandleDefs
  ( DirectedEdgeId,
    FaceId (..),
    UndirectedEdgeId (..),
    VertexId (..),
    asUndirected,
    directedPair,
    isNormalized,
  )
import Moonlight.Planar.Internal.CellSet (ExactCellSet (..))
import Moonlight.Planar.Internal.Incidence
  ( FaceBoundaryComponent (..),
    PlanarIncidence,
    circularEdgeWalk,
    faceInnerBoundaryComponents,
    incidenceAdjacentEdge,
    incidenceDirectedEdgeCount,
    incidenceIncidentFace,
    incidenceNext,
    incidenceNonCellularFaces,
    incidenceOrigin,
    incidenceUndirectedEndpoints,
  )
import Moonlight.Planar.Types (Triangulation)

-- | The cellular refinement of one closed geometric selection. The two cut
-- indices are derived views of its face components, never a second selection.
-- Native triangulations have empty cut indices and retain every native handle.
data DCELComplex = DCELComplex
  !ExactCellSet
  !(IntMap.IntMap [FaceCut])
  !(IntMap.IntMap [FaceCut])

-- | Geometric atoms retain their original identities. A cut is an abstract
-- arc in one face, not a fabricated resident DCEL edge or a new exact segment.
data DCELCellEdge
  = GeometricCellEdge !UndirectedEdgeId
  | FaceBoundaryCut !FaceCut
  deriving stock (Eq, Ord, Show)

-- Construction fixes endpoints from the sealed face-component incidence.
-- The owning face and component root give restriction-stable cut identities.
data FaceCut = FaceCut !FaceId !FaceBoundaryComponent !VertexId !VertexId
  deriving stock (Eq, Ord, Show)

faceCutFace :: FaceCut -> FaceId
faceCutFace (FaceCut face _ _ _) = face

faceCutComponent :: FaceCut -> FaceBoundaryComponent
faceCutComponent (FaceCut _ component _ _) = component

-- | Incidence materialization, Homology's independent chain-law seal, and
-- the exact-birth join retain typed obstructions at their respective boundaries.
type DCELCellRef = CellRef VertexId DCELCellEdge FaceId

data DCELError
  = DCELBoundaryCellMissing !DCELCellRef !DCELCellRef
  | DCELBoundaryIncidenceInvalid !BoundaryIncidenceShapeError
  | DCELChainComplexInvalid !HomologyFailure
  | DCELAlphaBirthMissing !DCELCellRef
  | DCELFilteredComplexInvalid !HomologyFailure
  | DCELBoundedFaceAnchorMissing !FaceId
  deriving stock (Eq, Show)

-- | Cut each disconnected inner boundary component to its bounded face's
-- outer component. Planar arrangement admission supplies the genus-zero face
-- and its oriented boundary-component decomposition; the standard cut-system
-- construction then gives one disk characteristic map. A slit or cut appears
-- twice oppositely in its attaching walk. Chain nilpotence checks the resulting
-- coefficients, but is not the proof that a holed stratum was a disk.
--
-- Only the sparse noncellular-face support is visited. Native triangulations
-- have empty support, so their construction remains constant-time.
fromExactCellSet :: ExactCellSet -> Either DCELError DCELComplex
fromExactCellSet cells@(ExactCellSet incidence _ _ selectedFaces) =
  case filter (\(FaceId rawFace) -> IntSet.member (fromIntegral rawFace) selectedFaces) (incidenceNonCellularFaces incidence) of
    [] -> Right (DCELComplex cells IntMap.empty IntMap.empty)
    nonCellularFaces -> do
      cutsByFace <-
        IntMap.fromDistinctAscList <$> traverse
          (\face@(FaceId rawFace) -> do
            root <- maybe (Left (DCELBoundedFaceAnchorMissing face)) Right (incidenceAdjacentEdge incidence face)
            let sourceVertex = incidenceOrigin incidence root
                cuts = List.sort
                  (fmap
                    (\component -> FaceCut face component sourceVertex (componentBaseVertex incidence component))
                    (faceInnerBoundaryComponents incidence face))
            pure (fromIntegral rawFace, cuts))
          nonCellularFaces
      let cutsByVertex =
            fmap List.sort
              ( IntMap.fromListWith (<>)
                  [ (basisIndexOfVertex vertex, [cut])
                  | cuts <- IntMap.elems cutsByFace,
                    cut@(FaceCut _ _ sourceVertex targetVertex) <- cuts,
                    vertex <- [sourceVertex, targetVertex]
                  ]
              )
      pure (DCELComplex cells cutsByFace cutsByVertex)

componentBaseVertex :: PlanarIncidence -> FaceBoundaryComponent -> VertexId
componentBaseVertex incidence component =
  case component of
    BoundaryCycleRoot root -> incidenceOrigin incidence root
    IsolatedBoundaryVertex vertex -> vertex

-- | Canonical integral cellular chains in ascending resident-handle order.
-- Degree one uses target minus source; degree two uses the DCEL's oriented
-- face boundary. The Homology boundary seals the result only after checking
-- shape and @d . d = 0@; no unchecked chain constructor crosses the public
-- package boundary.
finiteChainComplex :: DCELComplex -> Either DCELError (FiniteChainComplex Int)
finiteChainComplex complexValue =
  let basis = dcelBasis complexValue
   in finiteChainComplexWithBasis complexValue basis

-- | Lower the exact alpha section into Homology without converting its birth
-- order through binary64. Persistence remains wholly owned by Homology.
filteredAlphaComplex
  :: AlphaFiltration
  -> Either DCELError (FilteredFiniteChainComplex AlphaBirth Int)
filteredAlphaComplex filtration =
  withAlphaResidentTriangulation
    (\triangulation -> do
      finite <- residentAlphaFiniteChainComplex triangulation
      vertexBirthAssignments <-
        traverse
          ( residentAlphaBirthAssignment
              CellVertexRef
              residentVertexBasisRef
              (alphaVertexBirth filtration)
          )
          (vertexHandlesOf triangulation)
      edgeBirthAssignments <-
        traverse
          ( residentAlphaBirthAssignment
              (CellEdgeRef . GeometricCellEdge)
              residentEdgeBasisRef
              (alphaEdgeBirth filtration)
          )
          (undirectedEdgesOf triangulation)
      faceBirthAssignments <-
        traverse
          ( residentAlphaBirthAssignment
              CellFaceRef
              residentFaceBasisRef
              (alphaFaceBirth filtration)
          )
          (innerFacesOf triangulation)
      first DCELFilteredComplexInvalid
        ( mkFilteredFiniteChainComplex
            finite
            (vertexBirthAssignments <> edgeBirthAssignments <> faceBirthAssignments)
        )
    )
    filtration

-- | The opaque 'AlphaFiltration' constructor admits the entire resident DCEL,
-- whose handle ranges are dense. This local section therefore lowers those
-- handles directly to basis indices while retaining Homology's independent
-- shape and nilpotence seal. Sparse 'ExactCellSet' values continue through the
-- generic map-indexed 'finiteChainComplex' path.
residentAlphaFiniteChainComplex
  :: Triangulation mode vertex directed undirected face
  -> Either DCELError (FiniteChainComplex Int)
residentAlphaFiniteChainComplex triangulation = do
  let degreeOneColumns =
        Vector.generate
          (Dcel.numUndirectedEdges triangulation)
          (residentEdgeBoundaryEntries triangulation . UndirectedEdgeId . fromIntegral)
      degreeTwoColumns =
        Vector.imap
          residentFaceBoundaryEntries
          (Dcel.innerFaceDirectedEdgeTriples triangulation)
  degreeOneBoundary <-
    first DCELBoundaryIncidenceInvalid
      ( mkBoundaryIncidenceFromOrderedColumns
          (fromIntegral (Dcel.numUndirectedEdges triangulation))
          (fromIntegral (Dcel.numVertices triangulation))
          degreeOneColumns
      )
  degreeTwoBoundary <-
    first DCELBoundaryIncidenceInvalid
      ( mkBoundaryIncidenceFromOrderedColumns
          (fromIntegral (Dcel.numInnerFaces triangulation))
          (fromIntegral (Dcel.numUndirectedEdges triangulation))
          degreeTwoColumns
      )
  let degreeZeroBoundary =
        emptyBoundaryIncidenceOf
          (fromIntegral (Dcel.numVertices triangulation))
          0
      boundaryAt (HomologicalDegree degreeValue) =
        case degreeValue of
          0 -> degreeZeroBoundary
          1 -> degreeOneBoundary
          2 -> degreeTwoBoundary
          _ -> emptyBoundaryIncidence
  first DCELChainComplexInvalid
    (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)

residentEdgeBoundaryEntries
  :: Triangulation mode vertex directed undirected face
  -> UndirectedEdgeId
  -> [BoundaryEntry Int]
residentEdgeBoundaryEntries triangulation edgeValue =
  let (sourceVertex, targetVertex) = Dcel.undirectedEndpoints triangulation edgeValue
      sourceEntry =
        mkBoundaryEntryFromInts
          (basisIndexOfEdge edgeValue)
          (basisIndexOfVertex sourceVertex)
          (-1)
      targetEntry =
        mkBoundaryEntryFromInts
          (basisIndexOfEdge edgeValue)
          (basisIndexOfVertex targetVertex)
          1
   in if targetIndex sourceEntry <= targetIndex targetEntry
        then [sourceEntry, targetEntry]
        else [targetEntry, sourceEntry]

residentFaceBoundaryEntries
  :: Int
  -> (DirectedEdgeId, DirectedEdgeId, DirectedEdgeId)
  -> [BoundaryEntry Int]
residentFaceBoundaryEntries faceIndexValue (firstEdge, secondEdge, thirdEdge) =
  sortThreeBoundaryEntries
    (residentFaceBoundaryEntry faceIndexValue firstEdge)
    (residentFaceBoundaryEntry faceIndexValue secondEdge)
    (residentFaceBoundaryEntry faceIndexValue thirdEdge)

sortThreeBoundaryEntries
  :: BoundaryEntry Int
  -> BoundaryEntry Int
  -> BoundaryEntry Int
  -> [BoundaryEntry Int]
sortThreeBoundaryEntries firstEntry secondEntry thirdEntry =
  let (firstLow, firstHigh) = orderedBoundaryPair firstEntry secondEntry
      (secondLow, finalHigh) = orderedBoundaryPair firstHigh thirdEntry
      (finalLow, finalMiddle) = orderedBoundaryPair firstLow secondLow
   in [finalLow, finalMiddle, finalHigh]

orderedBoundaryPair
  :: BoundaryEntry Int
  -> BoundaryEntry Int
  -> (BoundaryEntry Int, BoundaryEntry Int)
orderedBoundaryPair firstEntry secondEntry =
  if targetIndex firstEntry <= targetIndex secondEntry
    then (firstEntry, secondEntry)
    else (secondEntry, firstEntry)

residentFaceBoundaryEntry :: Int -> DirectedEdgeId -> BoundaryEntry Int
residentFaceBoundaryEntry faceIndexValue directedEdge =
  mkBoundaryEntryFromInts
    faceIndexValue
    (basisIndexOfEdge (asUndirected directedEdge))
    (if isNormalized directedEdge then 1 else -1)

residentAlphaBirthAssignment
  :: (cell -> DCELCellRef)
  -> (cell -> BasisCellRef)
  -> (cell -> Maybe AlphaBirth)
  -> cell
  -> Either DCELError (BasisCellRef, AlphaBirth)
residentAlphaBirthAssignment cellReference basisReference birthAt cell =
  maybe
    (Left (DCELAlphaBirthMissing (cellReference cell)))
    (Right . (,) (basisReference cell))
    (birthAt cell)

residentVertexBasisRef :: VertexId -> BasisCellRef
residentVertexBasisRef vertexValue =
  BasisCellRef (HomologicalDegree 0) (basisIndexOfVertex vertexValue)

residentEdgeBasisRef :: UndirectedEdgeId -> BasisCellRef
residentEdgeBasisRef edgeValue =
  BasisCellRef (HomologicalDegree 1) (basisIndexOfEdge edgeValue)

residentFaceBasisRef :: FaceId -> BasisCellRef
residentFaceBasisRef faceValue =
  BasisCellRef (HomologicalDegree 2) (basisIndexOfFace faceValue)

basisIndexOfVertex :: VertexId -> Int
basisIndexOfVertex (VertexId rawVertex) = fromIntegral rawVertex

basisIndexOfEdge :: UndirectedEdgeId -> Int
basisIndexOfEdge (UndirectedEdgeId rawEdge) = fromIntegral rawEdge

basisIndexOfFace :: FaceId -> Int
basisIndexOfFace (FaceId rawFace) = fromIntegral rawFace - 1

undirectedEdgesOf
  :: Triangulation mode vertex directed undirected face
  -> [UndirectedEdgeId]
undirectedEdgesOf triangulation =
  fmap (UndirectedEdgeId . fromIntegral) [0 .. Dcel.numUndirectedEdges triangulation - 1]

vertexHandlesOf
  :: Triangulation mode vertex directed undirected face
  -> [VertexId]
vertexHandlesOf triangulation =
  fmap (VertexId . fromIntegral) [0 .. Dcel.numVertices triangulation - 1]

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

data DCELBasis = DCELBasis
  { dcelVertexBasis :: !(Map VertexId BasisCellRef)
  , dcelEdgeBasis :: !(Map DCELCellEdge BasisCellRef)
  , dcelFaceBasis :: !(Map FaceId BasisCellRef)
  }

dcelBasis :: DCELComplex -> DCELBasis
dcelBasis complexValue =
  DCELBasis
    { dcelVertexBasis = basisMap 0 (vertices complexValue)
    , dcelEdgeBasis = basisMap 1 (edges complexValue)
    , dcelFaceBasis = basisMap 2 (faces complexValue)
    }

basisMap :: Ord cell => Int -> [cell] -> Map cell BasisCellRef
basisMap degreeValue cells =
  Map.fromAscList
    ( zipWith
        (\indexValue cell -> (cell, BasisCellRef (HomologicalDegree degreeValue) indexValue))
        [0 ..]
        cells
    )

finiteChainComplexWithBasis
  :: DCELComplex
  -> DCELBasis
  -> Either DCELError (FiniteChainComplex Int)
finiteChainComplexWithBasis complexValue basis = do
  degreeOneEntries <-
    concat
      <$> traverse
        (edgeBoundaryEntries complexValue basis)
        (Map.toAscList (dcelEdgeBasis basis))
  degreeTwoEntries <-
    concat
      <$> traverse
        (faceBoundaryEntries complexValue basis)
        (Map.toAscList (dcelFaceBasis basis))
  degreeOneBoundary <-
    first DCELBoundaryIncidenceInvalid
      ( mkBoundaryIncidenceFromOrderedEntries
          (fromIntegral (Map.size (dcelEdgeBasis basis)))
          (fromIntegral (Map.size (dcelVertexBasis basis)))
          degreeOneEntries
      )
  degreeTwoBoundary <-
    first DCELBoundaryIncidenceInvalid
      ( mkBoundaryIncidenceFromOrderedEntries
          (fromIntegral (Map.size (dcelFaceBasis basis)))
          (fromIntegral (Map.size (dcelEdgeBasis basis)))
          degreeTwoEntries
      )
  let degreeZeroBoundary =
        emptyBoundaryIncidenceOf
          (fromIntegral (Map.size (dcelVertexBasis basis)))
          0
      boundaryAt :: HomologicalDegree -> BoundaryIncidence Int
      boundaryAt (HomologicalDegree degreeValue) =
        case degreeValue of
          0 -> degreeZeroBoundary
          1 -> degreeOneBoundary
          2 -> degreeTwoBoundary
          _ -> emptyBoundaryIncidence
  first DCELChainComplexInvalid
    (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)

edgeBoundaryEntries
  :: DCELComplex
  -> DCELBasis
  -> (DCELCellEdge, BasisCellRef)
  -> Either DCELError [BoundaryEntry Int]
edgeBoundaryEntries complexValue basis (edgeValue, edgeBasisRef) = do
  let (sourceVertex, targetVertex) = edgeBoundary complexValue edgeValue
      sourceCell = CellEdgeRef edgeValue
  sourceBasisRef <-
    requireBoundaryCell
      sourceCell
      (CellVertexRef sourceVertex)
      sourceVertex
      (dcelVertexBasis basis)
  targetBasisRef <-
    requireBoundaryCell
      sourceCell
      (CellVertexRef targetVertex)
      targetVertex
      (dcelVertexBasis basis)
  pure
    ( case orderedBoundaryPair
        ( mkBoundaryEntryFromInts
            (cellIndex edgeBasisRef)
            (cellIndex sourceBasisRef)
            (-1)
        )
        ( mkBoundaryEntryFromInts
            (cellIndex edgeBasisRef)
            (cellIndex targetBasisRef)
            1
        ) of
        (lowerEntry, higherEntry) -> [lowerEntry, higherEntry]
    )

faceBoundaryEntries
  :: DCELComplex
  -> DCELBasis
  -> (FaceId, BasisCellRef)
  -> Either DCELError [BoundaryEntry Int]
faceBoundaryEntries complexValue basis (faceValue, faceBasisRef) =
  List.sortOn targetIndex
    <$> traverse
      boundaryEntry
      (faceBoundary complexValue faceValue)
 where
  boundaryEntry orientedBoundary = do
    let edgeValue = orientedEdge orientedBoundary
    edgeBasisRef <-
      requireBoundaryCell
        (CellFaceRef faceValue)
        (CellEdgeRef edgeValue)
        edgeValue
        (dcelEdgeBasis basis)
    pure
      ( mkBoundaryEntryFromInts
          (cellIndex faceBasisRef)
          (cellIndex edgeBasisRef)
          (orientationCoefficient (edgeOrientation orientedBoundary))
      )

orientationCoefficient :: Orientation -> Int
orientationCoefficient orientation =
  case orientation of
    Positive -> 1
    Negative -> -1

requireBoundaryCell
  :: Ord cell
  => DCELCellRef
  -> DCELCellRef
  -> cell
  -> Map cell BasisCellRef
  -> Either DCELError BasisCellRef
requireBoundaryCell sourceCell targetCell cell basis =
  maybe
    (Left (DCELBoundaryCellMissing sourceCell targetCell))
    Right
    (Map.lookup cell basis)

instance CellTypes DCELComplex where
  type Vertex DCELComplex = VertexId
  type Edge DCELComplex = DCELCellEdge
  type Face DCELComplex = FaceId

instance CellComplex2D DCELComplex where
  vertices (DCELComplex (ExactCellSet _ selectedVertices _ _) _ _) =
    build
      (\emit finish ->
        IntMap.foldrWithKey
          (\rawVertex _ remaining -> emit (VertexId (fromIntegral rawVertex)) remaining)
          finish
          selectedVertices)
  {-# INLINE vertices #-}

  edges (DCELComplex (ExactCellSet _ _ selectedEdges _) cutsByFace _) =
    if IntMap.null cutsByFace
      then build foldGeometricEdges
      else build
        (\emit finish ->
          foldGeometricEdges emit
            (IntMap.foldr (\cuts remaining -> foldr (emit . FaceBoundaryCut) remaining cuts) finish cutsByFace))
    where
      foldGeometricEdges :: (DCELCellEdge -> result -> result) -> result -> result
      foldGeometricEdges emit finish =
        IntSet.foldr
          (\rawEdge remaining -> emit (GeometricCellEdge (UndirectedEdgeId (fromIntegral rawEdge))) remaining)
          finish
          selectedEdges
      {-# INLINE foldGeometricEdges #-}
  {-# INLINE edges #-}

  faces (DCELComplex (ExactCellSet _ _ _ selectedFaces) _ _) =
    build
      (\emit finish ->
        IntSet.foldr
          (\rawFace remaining -> emit (FaceId (fromIntegral rawFace)) remaining)
          finish
          selectedFaces)
  {-# INLINE faces #-}

  edgeBoundary (DCELComplex (ExactCellSet incidence _ _ _) _ _) edgeValue =
    case edgeValue of
      GeometricCellEdge edge -> incidenceUndirectedEndpoints incidence edge
      FaceBoundaryCut (FaceCut _ _ sourceVertex targetVertex) -> (sourceVertex, targetVertex)
  {-# INLINE edgeBoundary #-}

  faceBoundary (DCELComplex (ExactCellSet incidence _ _ _) cutsByFace _) face@(FaceId rawFace) =
    build
      (\(emit :: OrientedEdge DCELCellEdge -> result -> result) (finish :: result) ->
        let geometricBoundary :: DirectedEdgeId -> result -> result
            geometricBoundary root remaining =
              foldr
                (emit . orientedBoundaryEdge)
                remaining
                (circularEdgeWalk (incidenceDirectedEdgeCount incidence) root (incidenceNext incidence))
            cutBoundary :: FaceCut -> result -> result
            cutBoundary cut remaining =
              let innerTail = emit (OrientedEdge (FaceBoundaryCut cut) Negative) remaining
               in emit (OrientedEdge (FaceBoundaryCut cut) Positive)
                    (case faceCutComponent cut of
                       BoundaryCycleRoot root -> geometricBoundary root innerTail
                       IsolatedBoundaryVertex _ -> innerTail)
            cutTail :: result
            cutTail = foldr cutBoundary finish (IntMap.findWithDefault [] (fromIntegral rawFace) cutsByFace)
         in maybe cutTail (\root -> geometricBoundary root cutTail) (incidenceAdjacentEdge incidence face))
    where
      orientedBoundaryEdge :: DirectedEdgeId -> OrientedEdge DCELCellEdge
      orientedBoundaryEdge directedEdge =
        OrientedEdge
          { orientedEdge = GeometricCellEdge (asUndirected directedEdge),
            edgeOrientation =
              if isNormalized directedEdge
                then Positive
                else Negative
          }
  {-# INLINE faceBoundary #-}

  edgesAtVertex (DCELComplex (ExactCellSet incidence _ selectedEdges _) _ cutsByVertex) vertex =
    case IntMap.lookup (basisIndexOfVertex vertex) cutsByVertex of
      Nothing -> build foldIncidentGeometricEdges
      Just cuts -> build
        (\emit finish -> foldIncidentGeometricEdges emit (foldr (emit . FaceBoundaryCut) finish cuts))
    where
      foldIncidentGeometricEdges :: (DCELCellEdge -> result -> result) -> result -> result
      foldIncidentGeometricEdges emit finish =
        IntSet.foldr
          (\rawEdge remaining ->
            let edge = UndirectedEdgeId (fromIntegral rawEdge)
             in if edgeContainsVertex incidence vertex edge
                  then emit (GeometricCellEdge edge) remaining
                  else remaining)
          finish
          selectedEdges
      {-# INLINE foldIncidentGeometricEdges #-}
  {-# INLINE edgesAtVertex #-}

  facesAtEdge (DCELComplex (ExactCellSet incidence _ _ selectedFaces) _ _) edgeValue =
    let selectedFace face@(FaceId rawFace) =
          if IntSet.member (fromIntegral rawFace) selectedFaces then Just face else Nothing
     in case edgeValue of
          GeometricCellEdge edge ->
            let (forward, backward) = directedPair edge
             in (selectedFace (incidenceIncidentFace incidence forward), selectedFace (incidenceIncidentFace incidence backward))
          FaceBoundaryCut cut ->
            let owner = selectedFace (faceCutFace cut)
             in (owner, owner)

instance ValidateComplex2D DCELComplex where
  type ValidationIssue DCELComplex = DCELError
  validateComplex _ = []

edgeContainsVertex ::
  PlanarIncidence ->
  VertexId ->
  UndirectedEdgeId ->
  Bool
edgeContainsVertex incidence vertex edge =
  let (sourceVertex, targetVertex) = incidenceUndirectedEndpoints incidence edge
   in vertex == sourceVertex || vertex == targetVertex