moonlight-triangulation-1.0.0.0: src-dcel/Moonlight/Triangulation/FloodFillIterator.hs
{-# LANGUAGE FlexibleInstances #-}
-- | Shape queries and face flood fills over immutable triangulations.
module Moonlight.Triangulation.FloodFillIterator
( DistanceMetric (..)
, CircleMetric
, CircleMetricError (..)
, RectangleMetric
, RectangleMetricError (..)
, circleMetric
, rectangleMetric
, edgesInShape
, verticesInShape
, edgesInCircle
, verticesInCircle
, edgesInRectangle
, verticesInRectangle
, floodFillFaces
, outerFaceFloodFill
, facesAtEvenBarrierDepth
) where
import qualified Data.IntSet as IntSet
import Moonlight.Triangulation.Dcel
import Moonlight.Triangulation.Handles.HandleDefs
import Moonlight.Triangulation.Handles.Iterators.FixedIterators (undirectedEdges)
import Moonlight.Triangulation.Math
import Moonlight.Triangulation.PointLocation
import Moonlight.Triangulation.Types
-- | 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) !Double
deriving stock (Eq, Ord, Show)
-- | Typed refusal for an invalid circle query.
data CircleMetricError
= InvalidCircleCenter !PointValidationError
| NonFiniteRadiusSquared !NonFiniteValue
| NegativeRadiusSquared !Double
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 = do
queryCenter <- either (Left . InvalidCircleCenter) Right (mkQueryPoint center)
case classifyNonFinite radiusSquared of
Just nonFinite -> Left (NonFiniteRadiusSquared nonFinite)
Nothing
| radiusSquared < 0 -> Left (NegativeRadiusSquared radiusSquared)
| otherwise -> Right (CircleMetric queryCenter 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) point =
squaredDistanceWide (queryPointValue center) point <= radiusSquared
metricIntersectsEdge (CircleMetric center 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 = 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) = 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') =
foldl' expand (stack, visited, accepted, rejected) (faceDirectedEdges triangulation face)
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
-- | 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
(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)
]