moonlight-planar-1.1.0.0: src-dcel/Moonlight/Planar/Internal/Validation.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-expose-all-unfoldings #-}
-- | Discharge: the invariants the constructors guarantee, checkable on a value
-- built by any route.
module Moonlight.Planar.Internal.Validation
( canonicalAdmission
, coordinatesAscend
, validateTopology
, ClosureStats (..)
, topologyClosureStats
, topologyClosureSelectionStats
, validateTopologyClosure
, validateTopologyClosureWithStats
, validateDelaunay
, validateTriangulation
, triangulationIsValid
, faceArea
, faceMinimumAngleDegrees
) where
import Control.Monad (foldM)
import Control.Monad.ST (ST, runST)
import Data.Bits (countLeadingZeros, finiteBitSize, shiftL, shiftR, (.&.))
import Data.List (nub)
import qualified Data.IntSet as IntSet
import qualified Data.Vector.Unboxed.Mutable as MUV
import Moonlight.Planar.Internal.BoxedPaged (boxedPagedLength)
import Moonlight.Planar.Internal.BoundaryCycle (orderedPair)
import Moonlight.Planar.Internal.Paged (Paged, pagedFoldl', pagedLength, pagedUnsafeIndex)
import Moonlight.Planar.Dcel
import Moonlight.Planar.Internal.HandleDefs
import Moonlight.Planar.Internal.Incidence (nativePlanarIncidence, validatePlanarIncidence, validateIncidenceEdgeLinks)
import Moonlight.Planar.Handles.Iterators.FixedIterators (allFaces, undirectedEdges)
import Moonlight.Planar.Internal.PackedIndex (noIndex, packIndex, unpackIndex)
import Moonlight.Planar.Internal.Predicates
import Moonlight.Planar.Math
( squaredDistance
, triangleArea
)
import Moonlight.Planar.Internal.Representation
import Moonlight.Planar.Internal.Types
-- | Native cardinality/triangle/geometry laws around the single finite
-- incidence admission boundary. Unsafe geometry follows successful incidence.
validateTopology :: Triangulation mode vertex directed undirected face -> [InvariantViolation]
validateTopology triangulation =
structuralViolations ++ orientationViolations
where
verticesCount = numVertices triangulation
halfCount = numDirectedEdges triangulation
edgeCount = numUndirectedEdges triangulation
facesCount = numFaces triangulation
-- Geometry descends only after the finite DCEL has glued structurally.
-- Reading triangle coordinates through malformed links would turn a typed
-- validation failure into an indexing crash.
structuralViolations =
cardinalityViolations
++ incidenceViolations
++ faceViolations
++ eulerViolations
incidenceViolations
| not (null cardinalityViolations) = []
| otherwise = either (pure . IncidenceViolation) (const [])
(validatePlanarIncidence (nativePlanarIncidence triangulation))
orientationViolations
| not (null structuralViolations) = []
| otherwise =
[ InnerFaceNotCounterClockwise face
| face <- allFaces triangulation
, face /= outerFace
, Just (first, second, third) <- [innerFaceVertices triangulation face]
, orient2d
(vertexPoint triangulation first)
(vertexPoint triangulation second)
(vertexPoint triangulation third)
/= GT
]
cardinalityViolations =
[ CoordinatePlaneLengthMismatch pointXCount pointYCount
| pointXCount /= pointYCount
]
++ [VertexOutgoingLengthMismatch vertexOutCount verticesCount | vertexOutCount /= verticesCount]
++ [VertexPayloadLengthMismatch vertexPayloadCount verticesCount | vertexPayloadCount /= verticesCount]
++ [TopologyArenaLengthMismatch topologyLength (4 * halfCount) | not halfArraysEqual]
++ [DirectedPayloadLengthMismatch directedPayloadCount halfCount | directedPayloadCount /= halfCount]
++ [UndirectedPayloadLengthMismatch undirectedPayloadCount edgeCount | undirectedPayloadCount /= edgeCount]
++ [DirectedEdgeCountOdd halfCount | odd halfCount]
++ [ConstraintLengthMismatch constraintLength edgeCount | constraintLength /= edgeCount]
++ [ NonCanonicalConstraintFlag (UndirectedEdgeId (fromIntegral index)) flag
| index <- [0 .. pagedLength (triConstraint triangulation) - 1]
, let flag = pagedUnsafeIndex (triConstraint triangulation) index
, flag /= 0 && flag /= 1
]
++ [CachedConstraintCountMismatch (triConstraintCount triangulation) actualConstraintCount | triConstraintCount triangulation /= actualConstraintCount]
++ [ CachedConstraintIndexMismatch
| constraintLength == edgeCount
, triConstraintEdges triangulation /= indexedConstraintEdges
]
++ [MissingOuterFace | facesCount == 0]
++ [FacePayloadLengthMismatch facePayloadCount facesCount | facePayloadCount /= facesCount]
pointXCount = pagedLength (triPointX triangulation)
pointYCount = pagedLength (triPointY triangulation)
vertexOutCount = pagedLength (triVertexOut triangulation)
vertexPayloadCount = boxedPagedLength (triVertexData triangulation)
topologyLength = pagedLength (triHalfTopology triangulation)
directedPayloadCount = boxedPagedLength (triDirectedData triangulation)
undirectedPayloadCount = boxedPagedLength (triUndirectedData triangulation)
constraintLength = pagedLength (triConstraint triangulation)
facePayloadCount = boxedPagedLength (triFaceData triangulation)
actualConstraintCount = pagedFoldl' (\count flag -> if flag == 1 then count + 1 else count) 0 (triConstraint triangulation)
halfArraysEqual = topologyLength == 4 * halfCount
indexedConstraintEdges =
IntSet.fromAscList
[ index
| index <- [0 .. edgeCount - 1]
, pagedUnsafeIndex (triConstraint triangulation) index == 1
]
faceViolations
| not (null cardinalityViolations) || not (null incidenceViolations) = []
| otherwise =
[ InnerFaceVertexCardinalityMismatch face (length faceVertexIds) (length (nub faceVertexIds))
| face <- allFaces triangulation
, face /= outerFace
, let faceVertexIds = faceVertices triangulation face
, length faceVertexIds /= 3 || length (nub faceVertexIds) /= 3
]
eulerViolations
| not (null cardinalityViolations) || verticesCount < 2 = []
| numInnerFaces triangulation == 0 =
[ CollinearEdgeCountMismatch (verticesCount - 1) edgeCount
| edgeCount /= verticesCount - 1
]
| otherwise =
[ EulerCharacteristicMismatch eulerCharacteristic
| eulerCharacteristic /= 2
]
where
eulerCharacteristic = verticesCount - edgeCount + facesCount
-- | Apply ranges, shared incidence link laws, triangle/representative laws and
-- orientation to a certified local closure. Whole-value incidence coverage is
-- an obligation of the admitted source, not a scan repeated for each edit. The caller
-- supplies the admitted inner faces and the interface pairs; the collar faces
-- on the other side of those pairs are included here so a protected source
-- cannot be changed behind the local transaction. Cardinality and Euler
-- observations remain global scalar invariants and are deliberately not
-- rebuilt from the resident mesh.
validateTopologyClosure
:: IntSet.IntSet
-> IntSet.IntSet
-> Triangulation mode vertex directed undirected face
-> [InvariantViolation]
validateTopologyClosure admittedFaces interfacePairs triangulation =
snd (validateTopologyClosureWithStats admittedFaces interfacePairs triangulation)
-- | The support a local domain spans: the admitted faces with the collar
-- faces across the interface, the directed edges those faces and interface
-- pairs carry, the vertices those edges join, and the undirected pairs
-- beneath them. A closure validation walks this selection; its statistics
-- are the selection's sizes and need no walk.
data ClosureSelection = ClosureSelection
{ selectionFaces :: !IntSet.IntSet
, selectionEdges :: !IntSet.IntSet
, selectionVertices :: !IntSet.IntSet
, selectionPairs :: !IntSet.IntSet
}
topologyClosureSelection
:: IntSet.IntSet
-> IntSet.IntSet
-> Triangulation mode vertex directed undirected face
-> ClosureSelection
topologyClosureSelection admittedFaces interfacePairs triangulation =
ClosureSelection
{ selectionFaces = selectedFaces
, selectionEdges = selectedEdges
, selectionVertices = selectedVertices
, selectionPairs = selectedPairs
}
where
verticesCount = numVertices triangulation
halfCount = numDirectedEdges triangulation
collarFaces =
IntSet.fromList
[ rawFace
| rawPair <- IntSet.toAscList interfacePairs
, rawPair >= 0
, rawPair < numUndirectedEdges triangulation
, let edge = UndirectedEdgeId (fromIntegral rawPair)
, let (forward, backward) = directedPair edge
, rawFace <- fmap faceIdIndex [incidentFace triangulation forward, incidentFace triangulation backward]
, rawFace > 0
]
selectedFaces = IntSet.union admittedFaces collarFaces
selectedFaceEdges =
IntSet.fromList
[ rawEdge
| rawFace <- IntSet.toAscList selectedFaces
, edge <- faceEdgesBounded triangulation (FaceId (fromIntegral rawFace))
, rawEdge <- [directedEdgeIdIndex edge, directedEdgeIdIndex (reverseEdge edge)]
]
selectedInterfaceEdges =
IntSet.fromList
[ rawEdge
| rawPair <- IntSet.toAscList interfacePairs
, rawPair >= 0
, rawPair <= (maxBound - 1) `quot` 2
, rawEdge <- [2 * rawPair, 2 * rawPair + 1]
]
selectedEdges = IntSet.union selectedFaceEdges selectedInterfaceEdges
selectedVertices =
IntSet.fromList
[ rawVertex
| rawEdge <- IntSet.toAscList selectedEdges
, rawEdge >= 0
, rawEdge < halfCount
, directedEdgeIdIndex (reverseEdge (DirectedEdgeId (fromIntegral rawEdge))) < halfCount
, rawVertex <-
[ vertexIdIndex (origin triangulation (DirectedEdgeId (fromIntegral rawEdge)))
, vertexIdIndex (destination triangulation (DirectedEdgeId (fromIntegral rawEdge)))
]
, rawVertex < verticesCount
]
selectedPairs = IntSet.fromList [rawEdge `quot` 2 | rawEdge <- IntSet.toAscList selectedEdges]
closureSelectionStats :: IntSet.IntSet -> ClosureSelection -> ClosureStats
closureSelectionStats interfacePairs selection =
ClosureStats
{ closureFaces = IntSet.size (selectionFaces selection)
, closureDirectedEdges = IntSet.size (selectionEdges selection)
, closureVertices = IntSet.size (selectionVertices selection)
, closureInterfacePairs = IntSet.size interfacePairs
, closureConstraintPairs = IntSet.size (selectionPairs selection)
}
-- | The sizes of the closure a local domain spans, read off the materialized
-- selection: what 'validateTopologyClosureWithStats' reports beside its
-- violations. The oracle 'topologyClosureStats' is held to.
topologyClosureSelectionStats
:: IntSet.IntSet
-> IntSet.IntSet
-> Triangulation mode vertex directed undirected face
-> ClosureStats
topologyClosureSelectionStats admittedFaces interfacePairs triangulation =
closureSelectionStats interfacePairs (topologyClosureSelection admittedFaces interfacePairs triangulation)
-- | The same sizes counted from the domain's own faces and interface pairs,
-- for a caller whose value is already admitted: the initial and final
-- permitted faces are taken separately so their union is never built, every
-- directed edge is attributed to the one selected face whose cycle carries it
-- (its reverse to that face only when the face across is unselected), the
-- pairs are half the edges because the selection is closed under reversal,
-- and the vertices are the distinct origins of those edges, deduplicated in
-- a table sized by the closure rather than by the mesh. Equal to
-- 'topologyClosureSelectionStats' on the union wherever each cycle edge's
-- incident face is the face whose cycle it lies on, which every admitted
-- triangulation satisfies.
topologyClosureStats
:: IntSet.IntSet
-> IntSet.IntSet
-> IntSet.IntSet
-> Triangulation mode vertex directed undirected face
-> ClosureStats
topologyClosureStats initialFaces finalFaces interfacePairs triangulation = runST $ do
seen <- MUV.replicate capacity (-1)
(!cycleEdges, !cycleVertices) <-
foldM (visitFace seen) (0, 0) selectedFaceList
(!edges, !verticesSeen) <-
foldM (visitInterfacePair seen) (cycleEdges, cycleVertices) (IntSet.toAscList interfacePairs)
pure
ClosureStats
{ closureFaces = selectedFaceCount
, closureDirectedEdges = edges
, closureVertices = verticesSeen
, closureInterfacePairs = IntSet.size interfacePairs
, closureConstraintPairs = edges `quot` 2
}
where
verticesCount = numVertices triangulation
halfCount = numDirectedEdges triangulation
undirectedCount = numUndirectedEdges triangulation
collarFaces =
IntSet.fromList
[ rawFace
| rawPair <- IntSet.toAscList interfacePairs
, rawPair >= 0
, rawPair < undirectedCount
, let (forward, backward) = directedPair (UndirectedEdgeId (fromIntegral rawPair))
, rawFace <- fmap faceIdIndex [incidentFace triangulation forward, incidentFace triangulation backward]
, rawFace > 0
]
admitted rawFace = IntSet.member rawFace initialFaces || IntSet.member rawFace finalFaces
selected rawFace = admitted rawFace || IntSet.member rawFace collarFaces
novelFinal = filter (\rawFace -> not (IntSet.member rawFace initialFaces)) (IntSet.toAscList finalFaces)
novelCollar = filter (not . admitted) (IntSet.toAscList collarFaces)
selectedFaceList = IntSet.toAscList initialFaces ++ novelFinal ++ novelCollar
selectedFaceCount = IntSet.size initialFaces + length novelFinal + length novelCollar
-- Every vertex of the closure is the origin of a selected edge and there
-- are at most three per selected face plus two per interface pair, so a
-- half-full table of that size always has a vacant slot.
vertexBound = 3 * (IntSet.size initialFaces + IntSet.size finalFaces + IntSet.size collarFaces) + 2 * IntSet.size interfacePairs
capacityBits = finiteBitSize (0 :: Int) - countLeadingZeros (max 8 (2 * vertexBound))
capacity = 1 `shiftL` capacityBits
slotOf :: Int -> Int
slotOf rawVertex =
fromIntegral ((fromIntegral rawVertex * (0x9E3779B97F4A7C15 :: Word)) `shiftR` (finiteBitSize (0 :: Word) - capacityBits))
.&. (capacity - 1)
visitFace :: MUV.MVector s Int -> (Int, Int) -> Int -> ST s (Int, Int)
visitFace seen (!edges, !seenVertices) rawFace =
case adjacentEdge triangulation (FaceId (fromIntegral rawFace)) of
Nothing -> pure (edges, seenVertices)
Just start
| directedEdgeIdIndex start >= halfCount -> pure (edges, seenVertices)
| otherwise -> do
let second = next triangulation start
first <- visitCycleEdge seen (edges, seenVertices) start
if directedEdgeIdIndex second >= halfCount
then pure first
else do
let third = next triangulation second
secondVisited <- visitCycleEdge seen first second
if directedEdgeIdIndex third >= halfCount
then pure secondVisited
else visitCycleEdge seen secondVisited third
-- A cycle edge counts for its face; its reverse counts here only when the
-- face across is outside the selection, otherwise that face's cycle counts it.
visitCycleEdge :: MUV.MVector s Int -> (Int, Int) -> DirectedEdgeId -> ST s (Int, Int)
visitCycleEdge seen (!edges, !seenVertices) edge = do
let reversed = reverseEdge edge
seenVertices' <- visitOrigin seen seenVertices edge
if selected (faceIdIndex (incidentFace triangulation reversed))
then pure (edges + 1, seenVertices')
else do
seenVertices'' <- visitOrigin seen seenVertices' reversed
pure (edges + 2, seenVertices'')
-- An interface edge is already counted through a selected face on either
-- side; only a pair with neither side selected adds its two edges here.
visitInterfacePair :: MUV.MVector s Int -> (Int, Int) -> Int -> ST s (Int, Int)
visitInterfacePair seen (!edges, !seenVertices) rawPair
| rawPair < 0 || rawPair > (maxBound - 1) `quot` 2 = pure (edges, seenVertices)
| otherwise =
foldM
(\(!edgesSoFar, !seenSoFar) rawEdge ->
if rawEdge < halfCount
&& ( selected (faceIdIndex (incidentFace triangulation (DirectedEdgeId (fromIntegral rawEdge))))
|| selected (faceIdIndex (incidentFace triangulation (DirectedEdgeId (fromIntegral (rawEdge + 1 - 2 * (rawEdge `rem` 2))))))
)
then pure (edgesSoFar, seenSoFar)
else
if rawEdge < halfCount
then do
seenSoFar' <- visitOrigin seen seenSoFar (DirectedEdgeId (fromIntegral rawEdge))
pure (edgesSoFar + 1, seenSoFar')
else pure (edgesSoFar + 1, seenSoFar))
(edges, seenVertices)
[2 * rawPair, 2 * rawPair + 1]
visitOrigin :: forall s. MUV.MVector s Int -> Int -> DirectedEdgeId -> ST s Int
visitOrigin seen seenVertices edge
| rawVertex >= verticesCount = pure seenVertices
| otherwise = insertVertex (slotOf rawVertex)
where
!rawVertex = vertexIdIndex (origin triangulation edge)
insertVertex :: Int -> ST s Int
insertVertex !slot = do
occupant <- MUV.unsafeRead seen slot
if occupant < 0
then seenVertices + 1 <$ MUV.unsafeWrite seen slot rawVertex
else
if occupant == rawVertex
then pure seenVertices
else insertVertex ((slot + 1) .&. (capacity - 1))
-- | A bounded face's cycle edges, at most three, stopping at the first
-- handle beyond the arena.
faceEdgesBounded
:: Triangulation mode vertex directed undirected face
-> FaceId
-> [DirectedEdgeId]
faceEdgesBounded triangulation face =
case face of
FaceId raw
| toInteger raw <= 0 || toInteger raw >= toInteger (numFaces triangulation) -> []
_ -> adjacentEdges
where
halfCount = numDirectedEdges triangulation
adjacentEdges = case adjacentEdge triangulation face of
Nothing -> []
Just start
| directedEdgeIdIndex start >= halfCount -> []
| otherwise ->
let second = next triangulation start
in if directedEdgeIdIndex second >= halfCount
then [start]
else
let third = next triangulation second
in if directedEdgeIdIndex third >= halfCount
then [start, second]
else [start, second, third]
validateTopologyClosureWithStats
:: IntSet.IntSet
-> IntSet.IntSet
-> Triangulation mode vertex directed undirected face
-> (ClosureStats, [InvariantViolation])
validateTopologyClosureWithStats admittedFaces interfacePairs triangulation =
( closureSelectionStats interfacePairs selection
, rangeViolations ++ edgeViolations ++ faceViolations ++ vertexViolations ++ orientationViolations ++ constraintViolations
)
where
verticesCount = numVertices triangulation
halfCount = numDirectedEdges triangulation
facesCount = numFaces triangulation
selection = topologyClosureSelection admittedFaces interfacePairs triangulation
selectedFaces = selectionFaces selection
selectedEdges = selectionEdges selection
selectedVertices = selectionVertices selection
selectedPairs = selectionPairs selection
rangeViolations =
originViolations
++ nextViolations
++ previousViolations
++ faceRangeViolations
++ [ FaceAdjacentOutOfRange face edge halfCount
| rawFace <- IntSet.toAscList selectedFaces
, rawFace > 0
, rawFace < facesCount
, let face = FaceId (fromIntegral rawFace)
, Just edge <- [adjacentEdge triangulation face]
, directedEdgeIdIndex edge >= halfCount
]
-- The selected edges descend once, from the largest handle, so each family
-- is consed into ascending order as four separate walks would report it.
(originViolations, nextViolations, previousViolations, faceRangeViolations) =
selectedEdgeRange (IntSet.toDescList selectedEdges) [] [] [] []
where
selectedEdgeRange remaining origins nexts previouses faces =
case remaining of
[] -> (origins, nexts, previouses, faces)
rawEdge : rest
| rawEdge < 0 || rawEdge >= halfCount ->
selectedEdgeRange rest origins nexts previouses faces
| otherwise ->
let !edge = DirectedEdgeId (fromIntegral rawEdge)
!vertex = origin triangulation edge
!nextEdge = next triangulation edge
!previousEdge = previous triangulation edge
!face = incidentFace triangulation edge
in selectedEdgeRange
rest
( if vertexIdIndex vertex >= verticesCount
then EdgeOriginOutOfRange edge vertex verticesCount : origins
else origins
)
( if directedEdgeIdIndex nextEdge >= halfCount
then EdgeNextOutOfRange edge nextEdge halfCount : nexts
else nexts
)
( if directedEdgeIdIndex previousEdge >= halfCount
then EdgePreviousOutOfRange edge previousEdge halfCount : previouses
else previouses
)
( if faceIdIndex face >= facesCount
then EdgeFaceOutOfRange edge face facesCount : faces
else faces
)
edgeViolations =
concatMap validateEdge (IntSet.toAscList selectedEdges)
validateEdge rawEdge
| rawEdge < 0 || rawEdge >= halfCount = []
| otherwise =
let edge = DirectedEdgeId (fromIntegral rawEdge)
nextEdge = next triangulation edge
previousEdge = previous triangulation edge
twinEdge = reverseEdge edge
nextValid = directedEdgeIdIndex nextEdge < halfCount
previousValid = directedEdgeIdIndex previousEdge < halfCount
nextNextValid = nextValid && directedEdgeIdIndex (next triangulation nextEdge) < halfCount
innerCycle =
if incidentFace triangulation edge /= outerFace && nextValid && previousValid && nextNextValid
then [InnerFaceNotTriangularAtEdge edge | next triangulation (next triangulation nextEdge) /= edge]
else []
linkViolations
| nextValid && previousValid && directedEdgeIdIndex twinEdge < halfCount =
either (pure . IncidenceViolation) (const [])
(validateIncidenceEdgeLinks (nativePlanarIncidence triangulation) edge)
| otherwise = []
in linkViolations
++ [EdgeSelfLinkedNext edge | nextEdge == edge && halfCount > 2]
++ [EdgeSelfLinkedPrevious edge | previousEdge == edge && halfCount > 2]
++ innerCycle
faceViolations = concatMap validateFace (IntSet.toAscList selectedFaces)
validateFace rawFace
| rawFace <= 0 || rawFace >= facesCount = []
| otherwise =
let face = FaceId (fromIntegral rawFace)
in case adjacentEdge triangulation face of
Nothing -> [FaceMissingAdjacentEdge face]
Just edge
| directedEdgeIdIndex edge >= halfCount -> []
| otherwise ->
let representedFace = incidentFace triangulation edge
(faceEdges, faceVertices') = triangleEdgesAndVertices face
in [FaceRepresentativeMismatch face edge representedFace | representedFace /= face]
++ [ InnerFaceVertexCardinalityMismatch face (length faceVertices') (length (nub faceVertices'))
| rawFace > 0
, length faceVertices' /= 3 || length (nub faceVertices') /= 3
]
++ [InnerFaceNotTriangularAtEdge edge
| rawFace > 0
, not (triangleClosed faceEdges)
]
triangleEdgesAndVertices face =
let edges = faceEdgesBounded triangulation face
vertices' = fmap (vertexIdIndex . origin triangulation) edges
in (edges, vertices')
triangleClosed edges =
case edges of
[first, _, third] -> next triangulation third == first
_ -> False
vertexViolations = concatMap validateVertex (IntSet.toAscList selectedVertices)
validateVertex
:: Int
-> [InvariantViolation]
validateVertex rawVertex =
let vertex = VertexId (fromIntegral rawVertex)
in case vertexOutEdge triangulation vertex of
Nothing
| verticesCount > 1 -> [ConnectedVertexMissingOutgoing vertex]
| otherwise -> []
Just edge
| directedEdgeIdIndex edge >= halfCount ->
[VertexOutgoingOutOfRange vertex edge halfCount]
| otherwise ->
[ VertexOutgoingOriginMismatch vertex edge actualOrigin
| let actualOrigin = origin triangulation edge
, actualOrigin /= vertex
]
orientationViolations =
[ InnerFaceNotCounterClockwise face
| rawFace <- IntSet.toAscList selectedFaces
, rawFace > 0
, let face = FaceId (fromIntegral rawFace)
, let (_, vertices') = triangleEdgesAndVertices face
, all (< verticesCount) vertices'
, [first, second, third] <- [fmap (VertexId . fromIntegral) vertices']
, orient2d (vertexPoint triangulation first) (vertexPoint triangulation second) (vertexPoint triangulation third) /= GT
]
constraintViolations =
[ NonCanonicalConstraintFlag edge flag
| rawPair <- IntSet.toAscList selectedPairs
, rawPair >= 0
, let edge = UndirectedEdgeId (fromIntegral rawPair)
, rawPair < numUndirectedEdges triangulation
, let flag = pagedUnsafeIndex (triConstraint triangulation) rawPair
, flag /= 0 && flag /= 1
]
++ [ CachedConstraintIndexMismatch
| rawPair <- IntSet.toAscList selectedPairs
, rawPair >= 0
, rawPair < numUndirectedEdges triangulation
, let flag = pagedUnsafeIndex (triConstraint triangulation) rawPair
, (flag == 1) /= IntSet.member rawPair (triConstraintEdges triangulation)
]
-- | Every edge whose circumcircle is not empty.
validateDelaunay :: Triangulation mode vertex directed undirected face -> [InvariantViolation]
validateDelaunay triangulation = concatMap validateEdge (undirectedEdges triangulation)
where
validateEdge edge
| isConstraintEdge triangulation edge = []
| isBoundaryEdge triangulation edge = []
| otherwise =
let directed = normalizedDirected edge
twin = reverseEdge directed
in case (innerFaceDirectedEdges triangulation (incidentFace triangulation directed), innerFaceDirectedEdges triangulation (incidentFace triangulation twin)) of
(Just _, Just _) ->
let a = vertexPoint triangulation (origin triangulation directed)
b = vertexPoint triangulation (destination triangulation directed)
c = vertexPoint triangulation (origin triangulation (previous triangulation directed))
d = vertexPoint triangulation (origin triangulation (previous triangulation twin))
convex = orient2d c d b == GT && orient2d d c a == GT
circle = inCircle a b c d
illegal = convex && (circle == GT || (circle == EQ && orderedPair c d < orderedPair a b))
in [LocallyIllegalDelaunayEdge edge | illegal]
_ -> [DelaunayIncidentFaceNotTriangular edge]
-- | Topology first; the Delaunay property only if the topology holds.
validateTriangulation
:: Triangulation mode vertex directed undirected face
-> [InvariantViolation]
validateTriangulation triangulation =
let topology = validateTopology triangulation
in if null topology
then validateDelaunay triangulation
else topology
-- | Whether 'validateTriangulation' is empty.
triangulationIsValid
:: Triangulation mode vertex directed undirected face
-> Bool
triangulationIsValid = null . validateTriangulation
-- | Signed area, or 'Nothing' where the face is not a triangle.
faceArea :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe Double
faceArea triangulation face = do
(v0, v1, v2) <- innerFaceVertices triangulation face
pure (triangleArea (vertexPoint triangulation v0) (vertexPoint triangulation v1) (vertexPoint triangulation v2))
-- | Smallest interior angle, in degrees.
faceMinimumAngleDegrees :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe Double
faceMinimumAngleDegrees triangulation face = do
(v0, v1, v2) <- innerFaceVertices triangulation face
let p0 = vertexPoint triangulation v0
p1 = vertexPoint triangulation v1
p2 = vertexPoint triangulation v2
a = sqrt (squaredDistance p1 p2)
b = sqrt (squaredDistance p2 p0)
c = sqrt (squaredDistance p0 p1)
if min a (min b c) <= 0
then Nothing
else Just (minimum [angle b c a, angle c a b, angle a b c])
where
angle left right opposite = acos (clamp ((left * left + right * right - opposite * opposite) / (2 * left * right))) * 180 / pi
clamp = max (-1) . min 1
-- | Decide, in one linear pass and without allocating, whether a
-- triangulation already has the resident numbering published by the build
-- component's canonicalizer:
-- sites in strict lexicographic order; undirected edges in strict
-- lexicographic order of their endpoint pair with the even half leaving the
-- lower endpoint; every vertex and face anchored at its least half-edge;
-- inner faces in order of those anchors, edgeless faces trailing. The arenas
-- are the only evidence read; no carried claim is consulted. Where
-- canonical rebuilding would be the identity this mints the same witness for the cost
-- of the scan rather than the copy.
canonicalAdmission :: Triangulation mode vertex directed undirected face -> CanonicalAdmission
canonicalAdmission source
| coordinatesAscend vertexTotal coordinateX coordinateY
&& edgesRanked 1
&& halvesLeaveLower 0
&& anchorsOwned 0
&& anchorsLeast 0
&& innerFacesRanked 2 =
CanonicalKnown
| otherwise = CanonicalUnknown
where
!vertexTotal = numVertices source
!edgeTotal = numUndirectedEdges source
!faceTotal = numFaces source
!directedTotal = 2 * edgeTotal
!coordinateX = triPointX source
!coordinateY = triPointY source
!topologyArena = triHalfTopology source
!vertexOutArena = triVertexOut source
!faceEdgeArena = triFaceEdge source
topology slot = fromIntegral (topologyArena `pagedUnsafeIndex` slot) :: Int
originOf directed = topology (4 * directed)
faceOf directed = topology (4 * directed + 3)
vertexOut vertex = vertexOutArena `pagedUnsafeIndex` vertex
faceEdge face = faceEdgeArena `pagedUnsafeIndex` face
edgeLow edge = min (originOf (2 * edge)) (originOf (2 * edge + 1))
edgeHigh edge = max (originOf (2 * edge)) (originOf (2 * edge + 1))
edgesRanked !edge
| edge >= edgeTotal = True
| otherwise =
let !previousLow = edgeLow (edge - 1)
!currentLow = edgeLow edge
in ( previousLow < currentLow
|| (previousLow == currentLow && edgeHigh (edge - 1) < edgeHigh edge)
)
&& edgesRanked (edge + 1)
halvesLeaveLower !edge
| edge >= edgeTotal = True
| otherwise = originOf (2 * edge) <= originOf (2 * edge + 1) && halvesLeaveLower (edge + 1)
-- Each anchor names a half-edge that really leaves its vertex or bounds its
-- face; together with 'anchorsLeast' that makes it the least such one.
anchorsOwned !vertex
| vertex < vertexTotal =
let !anchor = vertexOut vertex
in (anchor == noIndex || (unpackIndex anchor < directedTotal && originOf (unpackIndex anchor) == vertex))
&& anchorsOwned (vertex + 1)
| vertex - vertexTotal < faceTotal =
let !face = vertex - vertexTotal
!anchor = faceEdge face
in (anchor == noIndex || (unpackIndex anchor < directedTotal && faceOf (unpackIndex anchor) == face))
&& anchorsOwned (vertex + 1)
| otherwise = True
anchorsLeast !directed
| directed >= directedTotal = True
| otherwise =
let !packed = packIndex directed
in vertexOut (originOf directed) <= packed
&& faceEdge (faceOf directed) <= packed
&& anchorsLeast (directed + 1)
innerFacesRanked !face
| face >= faceTotal = True
| otherwise =
let !previousAnchor = faceEdge (face - 1)
!currentAnchor = faceEdge face
in (previousAnchor < currentAnchor || (previousAnchor == noIndex && currentAnchor == noIndex))
&& innerFacesRanked (face + 1)
-- | Whether the stored sites are already in strict lexicographic order, in
-- which case ranking them is the identity and both permutations are free.
--
-- Strict rather than non-strict: a triangulation stores each site once, so
-- equal adjacent coordinates would mean a mesh this function has no ordering
-- for, and it is the sort's business to say so rather than this predicate's.
coordinatesAscend :: Int -> Paged Double -> Paged Double -> Bool
coordinatesAscend total x y = go 1
where
go !index
| index >= total = True
| otherwise =
let !previousX = x `pagedUnsafeIndex` (index - 1)
!currentX = x `pagedUnsafeIndex` index
in case compare previousX currentX of
LT -> go (index + 1)
GT -> False
EQ ->
y `pagedUnsafeIndex` (index - 1) < y `pagedUnsafeIndex` index
&& go (index + 1)