packages feed

moonlight-triangulation-1.3.0.0: src-dcel/Moonlight/Triangulation/FloodFillIterator.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}

-- | Shape queries and face flood fills over immutable triangulations.
module Moonlight.Triangulation.FloodFillIterator
  ( DistanceMetric (..)
  , CircleMetric
  , CircleMetricError (..)
  , RadiusSquaredError (..)
  , RectangleMetric
  , RectangleMetricError (..)
  , circleMetric
  , rectangleMetric
  , edgesInShape
  , verticesInShape
  , edgesInCircle
  , verticesInCircle
  , edgesInRectangle
  , verticesInRectangle
  , floodFillFaces
  , outerFaceFloodFill
  , facesAtEvenBarrierDepth
  , FaceComponent
  , faceComponentFaces
  , BoundaryOrientation (..)
  , BoundaryLoop
  , boundaryLoopOrientation
  , boundaryLoopVertices
  , RegionBoundary
  , regionBoundaryOuterLoop
  , regionBoundaryHoleLoops
  , BoundaryObstruction (..)
  , faceComponents
  , faceComponentsBy
  , labelledRegionBoundaries
  , componentBoundaryLoops
  , componentBoundary
  , RadiusSquared
  , mkRadiusSquared
  , alphaShapeContainsFace
  ) where

import Control.DeepSeq (NFData)
import Data.Bifunctor (first)
import qualified Data.IntMap.Strict as IntMap
import qualified Data.IntSet as IntSet
import Data.List (partition, unfoldr)
import qualified Data.List as List
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map.Strict as Map
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import qualified Data.Vector as V
import GHC.Generics (Generic)
import Moonlight.Triangulation.Dcel
import Moonlight.Triangulation.Handles.HandleDefs
import Moonlight.Triangulation.Handles.Iterators.FixedIterators (undirectedEdges)
import Moonlight.Triangulation.Internal.BoundaryCycle
  ( simplifyBoundaryCycle
  , traceOrientedBoundaryCircuits
  )
import Moonlight.Triangulation.Math
import Moonlight.Triangulation.PointLocation
import Moonlight.Triangulation.Scalar (circumradiusSquaredWithinCoordinates)
import Moonlight.Triangulation.Types

-- | One non-empty connected set of equally labelled bounded face indices.
newtype FaceComponent = FaceComponent IntSet.IntSet
  deriving stock (Eq, Show, Generic)
  deriving anyclass (NFData)

-- | Component faces in ascending DCEL order.
faceComponentFaces :: FaceComponent -> [FaceId]
faceComponentFaces (FaceComponent faces) =
  fmap (FaceId . fromIntegral) (IntSet.toAscList faces)

-- | Winding carried explicitly by a simple boundary loop.
data BoundaryOrientation
  = BoundaryCounterClockwise
  | BoundaryClockwise
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (NFData)

-- | One non-empty simple boundary loop. Counter-clockwise loops contribute
-- filled area under nonzero winding; clockwise loops subtract holes.
data BoundaryLoop = BoundaryLoop
  { boundaryLoopOrientation :: !BoundaryOrientation
  , boundaryLoopVertices :: !(NonEmpty VertexId)
  }
  deriving stock (Eq, Show, Generic)
  deriving anyclass (NFData)

-- | The authoritative boundary of one face component.
data RegionBoundary = RegionBoundary
  { regionBoundaryOuterLoop :: !BoundaryLoop
  , regionBoundaryHoleLoops :: ![BoundaryLoop]
  }
  deriving stock (Eq, Show, Generic)
  deriving anyclass (NFData)

-- | Typed failure to descend boundary half-edges or project their oriented
-- loops into one strict polygon component.
data BoundaryObstruction
  = BoundaryComponentFaceOutOfRange !FaceId {-# UNPACK #-} !Int
  | BoundaryPinch !VertexId !DirectedEdgeId !DirectedEdgeId
  | BoundaryCycleDidNotClose !DirectedEdgeId !DirectedEdgeId
  | BoundaryLoopDegenerate ![VertexId]
  | BoundaryOuterLoopCardinality !Int
  deriving stock (Eq, Show, Generic)
  deriving anyclass (NFData)

-- | Complete boundary edges refined by whether each vertex has one successor.
data BoundaryGraph
  = SimpleBoundaryGraph !IntSet.IntSet !(IntMap.IntMap DirectedEdgeId)
  | PinchedBoundaryGraph
      !IntSet.IntSet
      !VertexId
      !DirectedEdgeId
      !DirectedEdgeId

-- | An admitted finite, non-negative squared radius.
newtype RadiusSquared = RadiusSquared Double
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (NFData)

-- | Typed refusal shared by every squared-radius constructor.
data RadiusSquaredError
  = NonFiniteRadiusSquared !NonFiniteValue
  | NegativeRadiusSquared !Double
  deriving stock (Eq, Ord, Show)

-- | Admit a finite, non-negative squared radius.
mkRadiusSquared :: Double -> Either RadiusSquaredError RadiusSquared
mkRadiusSquared value =
  case classifyNonFinite value of
    Just nonFinite -> Left (NonFiniteRadiusSquared nonFinite)
    Nothing
      | value < 0 -> Left (NegativeRadiusSquared value)
      | otherwise -> Right (RadiusSquared value)

-- | A query shape that can admit points, test edges, and supply a location
-- seed.
class DistanceMetric metric where
  metricContainsPoint :: metric -> Point -> Bool
  metricIntersectsEdge :: metric -> Point -> Point -> Bool
  metricStartPoint :: metric -> QueryPoint

-- | An admitted center and squared radius.
data CircleMetric = CircleMetric !QueryPoint !RadiusSquared
  deriving stock (Eq, Ord, Show)

-- | Typed refusal for an invalid circle query.
data CircleMetricError
  = InvalidCircleCenter !PointValidationError
  | InvalidCircleRadius !RadiusSquaredError
  deriving stock (Eq, Ord, Show)

-- | Admitted lower corner, upper corner, and center of an axis-aligned box.
data RectangleMetric = RectangleMetric !(QueryPoint) !(QueryPoint) !(QueryPoint)
  deriving stock (Eq, Ord, Show)

-- | Typed refusal for an invalid rectangle query.
data RectangleMetricError
  = InvalidRectangleLower !PointValidationError
  | InvalidRectangleUpper !PointValidationError
  | InvalidRectangleCenter !PointValidationError
  deriving stock (Eq, Ord, Show)

-- | A circle metric, or why the radius is unusable.
circleMetric :: Point -> Double -> Either CircleMetricError CircleMetric
circleMetric center radiusSquared =
  CircleMetric
    <$> first InvalidCircleCenter (mkQueryPoint center)
    <*> first InvalidCircleRadius (mkRadiusSquared radiusSquared)

-- | An axis-aligned rectangle metric, or why the corners are unusable.
rectangleMetric :: Point -> Point -> Either RectangleMetricError RectangleMetric
rectangleMetric lower@(Point lowerX lowerY) upper@(Point upperX upperY) = do
  queryLower <- either (Left . InvalidRectangleLower) Right (mkQueryPoint lower)
  queryUpper <- either (Left . InvalidRectangleUpper) Right (mkQueryPoint upper)
  queryCenter <-
    either
      (Left . InvalidRectangleCenter)
      Right
      (mkQueryPoint (Point ((lowerX + upperX) * 0.5) ((lowerY + upperY) * 0.5)))
  Right (RectangleMetric queryLower queryUpper queryCenter)

instance DistanceMetric CircleMetric where
  metricContainsPoint (CircleMetric center (RadiusSquared radiusSquared)) point =
    squaredDistanceWide (queryPointValue center) point <= radiusSquared
  metricIntersectsEdge (CircleMetric center (RadiusSquared radiusSquared)) from to =
    segmentDistanceSquaredWide from to (queryPointValue center) <= radiusSquared
  metricStartPoint (CircleMetric center _) = center

instance DistanceMetric RectangleMetric where
  metricContainsPoint (RectangleMetric lower upper _) (Point x y) =
    lowerX <= upperX && lowerY <= upperY && x >= lowerX && x <= upperX && y >= lowerY && y <= upperY
   where
    Point lowerX lowerY = queryPointValue lower
    Point upperX upperY = queryPointValue upper
  metricIntersectsEdge rectangle from to =
    metricContainsPoint rectangle from
      || metricContainsPoint rectangle to
      || segmentRectangleIntersection rectangle from to
  metricStartPoint (RectangleMetric _ _ center) = center

-- | Edges meeting a circle.
edgesInCircle :: Triangulation mode vertex directed undirected face -> Point -> Double -> Either CircleMetricError [UndirectedEdgeId]
edgesInCircle triangulation center radiusSquared =
  edgesInShape triangulation <$> circleMetric center radiusSquared

-- | Vertices inside a circle.
verticesInCircle :: Triangulation mode vertex directed undirected face -> Point -> Double -> Either CircleMetricError [VertexId]
verticesInCircle triangulation center radiusSquared =
  verticesInShape triangulation <$> circleMetric center radiusSquared

-- | Edges meeting an axis-aligned rectangle.
edgesInRectangle :: Triangulation mode vertex directed undirected face -> Point -> Point -> Either RectangleMetricError [UndirectedEdgeId]
edgesInRectangle triangulation lower upper = edgesInShape triangulation <$> rectangleMetric lower upper

-- | Vertices inside an axis-aligned rectangle.
verticesInRectangle :: Triangulation mode vertex directed undirected face -> Point -> Point -> Either RectangleMetricError [VertexId]
verticesInRectangle triangulation lower upper = verticesInShape triangulation <$> rectangleMetric lower upper

-- | Edges meeting any metric shape.
edgesInShape :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [UndirectedEdgeId]
edgesInShape triangulation metric
  | numVertices triangulation <= 1 = []
  | numInnerFaces triangulation == 0 =
      [edge | edge <- undirectedEdges triangulation, edgeInside edge]
  | otherwise =
      let starts = shapeStartFaces triangulation metric
          (_, accepted) = floodFillFacesWithEdges triangulation starts edgeInside
       in map (UndirectedEdgeId . fromIntegral) (IntSet.toAscList accepted)
 where
  edgeInside edge =
    let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
     in metricIntersectsEdge metric (vertexPoint triangulation fromVertex) (vertexPoint triangulation toVertex)

-- | Vertices inside any metric shape.
verticesInShape :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [VertexId]
verticesInShape triangulation metric =
  [ vertex
  | vertex <- candidateVertices
  , metricContainsPoint metric (vertexPoint triangulation vertex)
  ]
 where
  edges = edgesInShape triangulation metric
  set = List.foldl' addEndpoints IntSet.empty edges
  addEndpoints acc edge =
    let (VertexId from, VertexId to) = undirectedEndpoints triangulation edge
     in IntSet.insert (fromIntegral from) (IntSet.insert (fromIntegral to) acc)
  candidateVertices
    | numVertices triangulation == 1 = [VertexId 0]
    | otherwise = map (VertexId . fromIntegral) (IntSet.toAscList set)

-- | Reach inner faces from the supplied seeds by crossing only admitted edges.
floodFillFaces
  :: Triangulation mode vertex directed undirected face -> [FaceId]
  -> (UndirectedEdgeId -> Bool)
  -> [FaceId]
floodFillFaces triangulation starts canCross =
  fst (floodFillFacesWithEdges triangulation starts canCross)

floodFillFacesWithEdges
  :: Triangulation mode vertex directed undirected face -> [FaceId]
  -> (UndirectedEdgeId -> Bool)
  -> ([FaceId], IntSet.IntSet)
floodFillFacesWithEdges triangulation starts canCross =
  let (faces, accepted, _) = go initialStack initialVisited IntSet.empty IntSet.empty []
   in (reverse faces, accepted)
 where
  valid face@(FaceId value) = face /= outerFace && fromIntegral value < numFaces triangulation
  (initialStack, initialVisited) = List.foldl' enqueueStart ([], IntSet.empty) starts

  enqueueStart state face
    | valid face = enqueue face state
    | otherwise = state

  go [] _ accepted rejected result = (result, accepted, rejected)
  go (face : stack) visited accepted rejected result =
    let (stack', visited', accepted', rejected') =
          foldFaceDirectedEdges'
            triangulation
            face
            expand
            (stack, visited, accepted, rejected)
     in go stack' visited' accepted' rejected' (face : result)

  expand (stack, visited, accepted, rejected) edge =
    let undirected@(UndirectedEdgeId raw) = asUndirected edge
        edgeIndex = fromIntegral raw
        adjacent = incidentFace triangulation (reverseEdge edge)
        edgeAdmission
          | IntSet.member edgeIndex accepted = (True, accepted, rejected)
          | IntSet.member edgeIndex rejected = (False, accepted, rejected)
          | canCross undirected = (True, IntSet.insert edgeIndex accepted, rejected)
          | otherwise = (False, accepted, IntSet.insert edgeIndex rejected)
        (crosses, accepted', rejected') = edgeAdmission
        (stack', visited') =
          if crosses && valid adjacent
            then enqueue adjacent (stack, visited)
            else (stack, visited)
     in (stack', visited', accepted', rejected')

  enqueue face@(FaceId value) (stack, visited)
    | IntSet.member index visited = (stack, visited)
    | otherwise = (face : stack, IntSet.insert index visited)
   where
    index = fromIntegral value

-- | Connected components of equally labelled bounded faces. Labels are
-- evaluated once; the component carrier is the same 'IntSet' used by descent.
faceComponents
  :: Eq label
  => Triangulation mode vertex directed undirected face
  -> (FaceId -> label)
  -> [(label, FaceComponent)]
faceComponents triangulation labelFace =
  faceComponentsFromLabels triangulation labelAt (const True)
 where
  labels =
    V.generate
      (numInnerFaces triangulation)
      (\index -> labelFace (FaceId (fromIntegral (index + 1))))
  labelAt faceIndex = labels V.!? (faceIndex - 1)

-- | Connected components over a selected face section and admitted adjacency.
-- A missing label removes a face from the section; the edge predicate states
-- which overlaps glue. This is the common descent used by ordinary labelled
-- regions and exact-overlay charts whose representation diagonals alone may
-- connect collapsed resident faces.
faceComponentsBy
  :: Eq label
  => Triangulation mode vertex directed undirected face
  -> (FaceId -> Maybe label)
  -> (UndirectedEdgeId -> Bool)
  -> [(label, FaceComponent)]
faceComponentsBy triangulation labelFace =
  faceComponentsFromLabels triangulation labelAt
 where
  labels =
    V.generate
      (numInnerFaces triangulation)
      (\index -> labelFace (FaceId (fromIntegral (index + 1))))
  labelAt faceIndex = labels V.!? (faceIndex - 1) >>= id

faceComponentsFromLabels
  :: Eq label
  => Triangulation mode vertex directed undirected face
  -> (Int -> Maybe label)
  -> (UndirectedEdgeId -> Bool)
  -> [(label, FaceComponent)]
faceComponentsFromLabels triangulation labelAt canCross =
  unfoldr descend initialUnvisited
 where
  initialUnvisited =
    IntSet.fromRange (1, numFaces triangulation - 1)

  descend remaining =
    case IntSet.minView remaining of
      Nothing -> Nothing
      Just (seedIndex, unseeded) ->
        case labelAt seedIndex of
          Nothing -> descend unseeded
          Just componentLabel ->
            let unvisited =
                  collectComponent componentLabel (Seq.singleton seedIndex) unseeded
                componentFaces = IntSet.difference remaining unvisited
             in Just ((componentLabel, FaceComponent componentFaces), unvisited)

  collectComponent componentLabel queued unvisited =
    case Seq.viewl queued of
      Seq.EmptyL -> unvisited
      faceIndex Seq.:< remainingQueue ->
        let face = FaceId (fromIntegral faceIndex)
            (expandedQueue, remainingUnvisited) =
              foldFaceDirectedEdges'
                triangulation
                face
                (admitAdjacent componentLabel)
                (remainingQueue, unvisited)
         in collectComponent
              componentLabel
              expandedQueue
              remainingUnvisited

  admitAdjacent componentLabel (queued, unvisited) edge =
    if
      canCross (asUndirected edge)
        && adjacent /= outerFace
        && IntSet.member adjacentIndex unvisited
        && labelAt adjacentIndex == Just componentLabel
      then
        ( queued Seq.|> adjacentIndex
        , IntSet.delete adjacentIndex unvisited
        )
      else (queued, unvisited)
   where
    adjacent@(FaceId adjacentRaw) =
      incidentFace triangulation (reverseEdge edge)
    adjacentIndex = fromIntegral adjacentRaw
{-# INLINE faceComponentsFromLabels #-}

-- | Descend every equally labelled bounded-face component through the one
-- authoritative boundary tracer. Components are converted independently;
-- callers may group equal labels only after this descent has succeeded.
labelledRegionBoundaries
  :: Eq label
  => Triangulation mode vertex directed undirected face
  -> (FaceId -> label)
  -> Either BoundaryObstruction [(label, RegionBoundary)]
labelledRegionBoundaries triangulation labelFace =
  traverse
    (\(label, component) -> (label,) <$> componentBoundary triangulation component)
    (faceComponents triangulation labelFace)

-- | Extract the non-empty oriented boundary chain of one component. Boundary
-- half-edges retain their incident component face on the left. If several
-- boundary arms meet at one vertex, Euler descent consumes every half-edge
-- once and repeated-vertex splitting publishes finitely many simple loops.
componentBoundaryLoops
  :: Triangulation mode vertex directed undirected face
  -> FaceComponent
  -> Either BoundaryObstruction (NonEmpty BoundaryLoop)
componentBoundaryLoops triangulation component = do
  graph <- componentBoundaryGraph triangulation component
  loops <-
    case graph of
      SimpleBoundaryGraph edges outgoing ->
        traceBoundaryLoops triangulation outgoing edges
      PinchedBoundaryGraph edges _ _ _ ->
        tracePinchedBoundaryLoops triangulation edges
  case NonEmpty.nonEmpty loops of
    Just nonEmptyLoops -> Right nonEmptyLoops
    Nothing -> Left (BoundaryOuterLoopCardinality 0)

-- | Project one component into a strict polygon boundary. A pinched oriented
-- chain remains available through 'componentBoundaryLoops', but it is not one
-- lawful 'RegionBoundary' and is refused here with its first pinch witness.
componentBoundary
  :: Triangulation mode vertex directed undirected face
  -> FaceComponent
  -> Either BoundaryObstruction RegionBoundary
componentBoundary triangulation component = do
  graph <- componentBoundaryGraph triangulation component
  case graph of
    PinchedBoundaryGraph _ vertex firstEdge secondEdge ->
      Left (BoundaryPinch vertex firstEdge secondEdge)
    SimpleBoundaryGraph edges outgoing ->
      traceBoundaryLoops triangulation outgoing edges
        >>= regionBoundaryFromLoops

componentBoundaryGraph
  :: Triangulation mode vertex directed undirected face
  -> FaceComponent
  -> Either BoundaryObstruction BoundaryGraph
componentBoundaryGraph triangulation (FaceComponent componentFaces) =
  case IntSet.lookupGE (numFaces triangulation) componentFaces of
    Just invalid ->
      Left
        ( BoundaryComponentFaceOutOfRange
            (FaceId (fromIntegral invalid))
            (numFaces triangulation)
        )
    Nothing ->
      Right
        ( IntSet.foldl'
            collectFace
            (SimpleBoundaryGraph IntSet.empty IntMap.empty)
            componentFaces
        )
 where
  collectFace graph (face :: Int) =
    foldFaceDirectedEdges'
      triangulation
      (FaceId (fromIntegral face))
      insertBoundaryEdge
      graph

  insertBoundaryEdge graph edge
    | IntSet.member adjacentIndex componentFaces = graph
    | otherwise =
        case graph of
          PinchedBoundaryGraph edges pinchVertex firstEdge secondEdge ->
            PinchedBoundaryGraph
              (insertEdge edges)
              pinchVertex
              firstEdge
              secondEdge
          SimpleBoundaryGraph edges outgoing ->
            case IntMap.lookup vertexIndex outgoing of
              Nothing ->
                SimpleBoundaryGraph
                  (insertEdge edges)
                  (IntMap.insert vertexIndex edge outgoing)
              Just previousEdge ->
                PinchedBoundaryGraph
                  (insertEdge edges)
                  vertex
                  previousEdge
                  edge
   where
    FaceId adjacent = incidentFace triangulation (reverseEdge edge)
    adjacentIndex = fromIntegral adjacent
    vertex@(VertexId rawVertex) = origin triangulation edge
    vertexIndex = fromIntegral rawVertex
    DirectedEdgeId rawEdge = edge
    insertEdge = IntSet.insert (fromIntegral rawEdge)

regionBoundaryFromLoops
  :: [BoundaryLoop]
  -> Either BoundaryObstruction RegionBoundary
regionBoundaryFromLoops loops =
  case outerLoops of
    [outerLoop] ->
      Right
        RegionBoundary
          { regionBoundaryOuterLoop = outerLoop
          , regionBoundaryHoleLoops = holeLoops
          }
    _ -> Left (BoundaryOuterLoopCardinality (length outerLoops))
 where
  (outerLoops, holeLoops) =
    partition
      ((== BoundaryCounterClockwise) . boundaryLoopOrientation)
      loops

tracePinchedBoundaryLoops
  :: Triangulation mode vertex directed undirected face
  -> IntSet.IntSet
  -> Either BoundaryObstruction [BoundaryLoop]
tracePinchedBoundaryLoops triangulation boundaryEdges = do
  cycles <-
    traceOrientedBoundaryCircuits
      (origin triangulation)
      (destination triangulation)
      BoundaryCycleDidNotClose
      outgoingByVertex
      orientedEdges
  traverse
    (simplifyBoundaryLoop triangulation . NonEmpty.toList)
    cycles
 where
  orientedEdges =
    Set.fromDistinctAscList
      ( fmap
          (DirectedEdgeId . fromIntegral)
          (IntSet.toAscList boundaryEdges)
      )
  outgoingByVertex =
    Set.foldr
      (\edge -> Map.insertWith (<>) (origin triangulation edge) [edge])
      Map.empty
      orientedEdges

traceBoundaryLoops
  :: Triangulation mode vertex directed undirected face
  -> IntMap.IntMap DirectedEdgeId
  -> IntSet.IntSet
  -> Either BoundaryObstruction [BoundaryLoop]
traceBoundaryLoops triangulation outgoingByVertex = descend []
 where
  descend loops unvisited =
    case IntSet.minView unvisited of
      Nothing -> Right (reverse loops)
      Just (rawStart, _) -> do
        let start = DirectedEdgeId (fromIntegral rawStart)
        (vertices, remaining) <- traceCycle start start unvisited []
        loop <- simplifyBoundaryLoop triangulation vertices
        descend (loop : loops) remaining

  traceCycle start current unvisited reversedVertices =
    let DirectedEdgeId rawCurrent = current
        remaining = IntSet.delete (fromIntegral rawCurrent) unvisited
        accumulated = origin triangulation current : reversedVertices
        VertexId rawTarget = destination triangulation current
     in case IntMap.lookup (fromIntegral rawTarget) outgoingByVertex of
          Just successor
            | successor == start -> Right (reverse accumulated, remaining)
            | let DirectedEdgeId rawSuccessor = successor
            , IntSet.member (fromIntegral rawSuccessor) remaining ->
                traceCycle start successor remaining accumulated
            | otherwise -> Left (BoundaryCycleDidNotClose start successor)
          Nothing -> Left (BoundaryCycleDidNotClose start current)

simplifyBoundaryLoop
  :: Triangulation mode vertex directed undirected face
  -> [VertexId]
  -> Either BoundaryObstruction BoundaryLoop
simplifyBoundaryLoop triangulation vertices = do
  (windingOrder, simplifiedVertices) <-
    simplifyBoundaryCycle
      BoundaryLoopDegenerate
      redundant
      winding
      key
      vertices
  case windingOrder of
    GT -> Right (BoundaryLoop BoundaryCounterClockwise simplifiedVertices)
    LT -> Right (BoundaryLoop BoundaryClockwise simplifiedVertices)
    EQ -> Left (BoundaryLoopDegenerate (NonEmpty.toList simplifiedVertices))
 where
  point vertex = vertexPoint triangulation vertex
  redundant previousVertex current nextVertex =
    orient2d (point previousVertex) (point current) (point nextVertex) == EQ
      && onClosedSegment (point previousVertex) (point nextVertex) (point current)
  winding previousVertex current nextVertex =
    orient2d (point previousVertex) (point current) (point nextVertex)
  key vertex = (point vertex, vertex)

-- | Membership of a bounded face in the closed alpha shape. Exact dyadic
-- comparison makes equality independent of circumcenter rounding.
alphaShapeContainsFace
  :: RadiusSquared
  -> Triangulation 'Unconstrained vertex directed undirected face
  -> FaceId
  -> Bool
alphaShapeContainsFace (RadiusSquared threshold) triangulation =
  maybe False withinRadius . innerFaceVertices triangulation
 where
  withinRadius (firstVertex, secondVertex, thirdVertex) =
    let Point ax ay = vertexPoint triangulation firstVertex
        Point bx by = vertexPoint triangulation secondVertex
        Point cx cy = vertexPoint triangulation thirdVertex
     in circumradiusSquaredWithinCoordinates threshold ax ay bx by cx cy

-- | Inner faces separated from the outer face by an even minimum number of
-- barriers. A 0–1 BFS floods freely within one depth before crossing a barrier,
-- so a free-ended barrier can be walked around at depth zero while nested
-- closed barriers alternate outside and inside.
facesAtEvenBarrierDepth
  :: Triangulation mode vertex directed undirected face
  -> (UndirectedEdgeId -> Bool)
  -> [FaceId]
facesAtEvenBarrierDepth triangulation isBarrier =
  concat (evenLayers (barrierDepthLayers triangulation isBarrier))
 where
  evenLayers :: [[FaceId]] -> [[FaceId]]
  evenLayers (outsideLayer : _insideLayer : deeper) =
    outsideLayer : evenLayers deeper
  evenLayers shallow = shallow

barrierDepthLayers
  :: Triangulation mode vertex directed undirected face
  -> (UndirectedEdgeId -> Bool)
  -> [[FaceId]]
barrierDepthLayers triangulation isBarrier =
  map (filter (/= outerFace)) (layers IntSet.empty [outerFace])
 where
  known (FaceId value) = fromIntegral value < numFaces triangulation
  key :: FaceId -> Int
  key (FaceId value) = fromIntegral value

  layers visited frontier = case flood visited [] frontier of
    ([], _) -> []
    (layer, visited') -> layer : layers visited' (concatMap (neighbours isBarrier) layer)

  flood visited acc [] = (reverse acc, visited)
  flood visited acc (face : rest)
    | not (known face) || IntSet.member (key face) visited = flood visited acc rest
    | otherwise =
        flood
          (IntSet.insert (key face) visited)
          (face : acc)
          (neighbours (not . isBarrier) face <> rest)

  neighbours admit face =
    [ incidentFace triangulation (reverseEdge edge)
    | edge <- faceDirectedEdges triangulation face
    , admit (asUndirected edge)
    ]

-- | Faces reachable from the outer face without crossing a barrier edge.
outerFaceFloodFill :: Triangulation mode vertex directed undirected face -> (UndirectedEdgeId -> Bool) -> [FaceId]
outerFaceFloodFill triangulation canCross = floodFillFaces triangulation starts canCross
 where
  starts =
    [ face
    | outerEdge <- faceDirectedEdges triangulation outerFace
    , let edge = asUndirected outerEdge
    , canCross edge
    , let face = incidentFace triangulation (reverseEdge outerEdge)
    , face /= outerFace
    ]

-- | The faces a shape's start point lands in.
shapeStartFaces :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [FaceId]
shapeStartFaces triangulation metric =
  case locatePoint triangulation (metricStartPoint metric) of
    InFace face -> [face]
    OnEdge edge -> filter (/= outerFace) [incidentFace triangulation edge, incidentFace triangulation (reverseEdge edge)]
    OnVertex vertex ->
      intSetToFaces
        (List.foldl' (\set edge -> let FaceId value = incidentFace triangulation edge in if value == 0 then set else IntSet.insert (fromIntegral value) set) IntSet.empty (vertexOutgoingEdges triangulation vertex))
    OutsideConvexHull _ ->
      [ incidentFace triangulation (reverseEdge edge)
      | edge <- faceDirectedEdges triangulation outerFace
      , let from = vertexPoint triangulation (origin triangulation edge)
      , let to = vertexPoint triangulation (destination triangulation edge)
      , metricIntersectsEdge metric from to
      , incidentFace triangulation (reverseEdge edge) /= outerFace
      ]
    EmptyTriangulation -> []
 where
  intSetToFaces = map (FaceId . fromIntegral) . IntSet.toAscList

segmentRectangleIntersection :: RectangleMetric -> Point -> Point -> Bool
segmentRectangleIntersection (RectangleMetric lowerQuery upperQuery _) from to
  | lx > ux || ly > uy = False
  | lower == upper = onClosedSegment from to lower
  | otherwise = any (uncurry (segmentsIntersect from to)) boundaries
 where
  lower@(Point lx ly) = queryPointValue lowerQuery
  upper@(Point ux uy) = queryPointValue upperQuery
  boundaries =
    [ (Point lx ly, Point lx uy)
    , (Point lx uy, Point ux uy)
    , (Point ux uy, Point ux ly)
    , (Point ux ly, Point lx ly)
    ]