moonlight-triangulation-1.4.0.1: src-dcel/Moonlight/Triangulation/Validation.hs
{-# LANGUAGE BangPatterns #-}
-- | Discharge: the invariants the constructors guarantee, checkable on a value
-- built by any route.
module Moonlight.Triangulation.Validation
( validateTopology
, ValidationClosureStats (..)
, validateTopologyClosure
, validateTopologyClosureWithStats
, validateDelaunay
, validateTriangulation
, triangulationIsValid
, faceArea
, faceMinimumAngleDegrees
) where
import Data.List (nub)
import qualified Data.IntSet as IntSet
import Moonlight.Triangulation.Internal.BoxedPaged (boxedPagedLength)
import Moonlight.Triangulation.Internal.BoundaryCycle (orderedPair)
import Moonlight.Triangulation.Internal.Paged (pagedFoldl', pagedLength, pagedUnsafeIndex)
import Moonlight.Triangulation.Dcel
import Moonlight.Triangulation.Handles.HandleDefs
import Moonlight.Triangulation.Handles.Iterators.FixedIterators (allFaces, directedEdges, undirectedEdges, vertices)
import Moonlight.Triangulation.Internal.PackedIndex (noIndex)
import Moonlight.Triangulation.Math
import Moonlight.Triangulation.Internal.Representation
import Moonlight.Triangulation.Internal.Types
-- | Every structural invariant violated, not the first.
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
++ rangeViolations
++ edgeViolations
++ faceViolations
++ vertexViolations
++ eulerViolations
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
]
rangeViolations =
[ EdgeOriginOutOfRange (DirectedEdgeId (fromIntegral index)) (VertexId value) verticesCount
| index <- [0 .. halfCount - 1]
, let value = pagedUnsafeIndex (triHalfTopology triangulation) (4 * index)
, fromIntegral value >= verticesCount
]
++ [ EdgeNextOutOfRange (DirectedEdgeId (fromIntegral index)) (DirectedEdgeId value) halfCount
| index <- [0 .. halfCount - 1]
, let value = pagedUnsafeIndex (triHalfTopology triangulation) (4 * index + 1)
, fromIntegral value >= halfCount
]
++ [ EdgePreviousOutOfRange (DirectedEdgeId (fromIntegral index)) (DirectedEdgeId value) halfCount
| index <- [0 .. halfCount - 1]
, let value = pagedUnsafeIndex (triHalfTopology triangulation) (4 * index + 2)
, fromIntegral value >= halfCount
]
++ [ EdgeFaceOutOfRange (DirectedEdgeId (fromIntegral index)) (FaceId value) facesCount
| index <- [0 .. halfCount - 1]
, let value = pagedUnsafeIndex (triHalfTopology triangulation) (4 * index + 3)
, fromIntegral value >= facesCount
]
++ [ VertexOutgoingOutOfRange (VertexId (fromIntegral index)) (DirectedEdgeId value) halfCount
| index <- [0 .. pagedLength (triVertexOut triangulation) - 1]
, let value = pagedUnsafeIndex (triVertexOut triangulation) index
, value /= noIndex
, fromIntegral value >= halfCount
]
++ [ FaceAdjacentOutOfRange (FaceId (fromIntegral index)) (DirectedEdgeId value) halfCount
| index <- [0 .. facesCount - 1]
, let value = pagedUnsafeIndex (triFaceEdge triangulation) index
, value /= noIndex
, fromIntegral value >= halfCount
]
edgeViolations
| not halfArraysEqual || odd halfCount || not (null rangeViolations) = []
| otherwise = concatMap validateEdge (directedEdges triangulation)
validateEdge edge@(DirectedEdgeId raw) =
let index = fromIntegral raw
nextEdge = next triangulation edge
previousEdge = previous triangulation edge
twinEdge = reverseEdge edge
local =
[ EdgeNextPreviousMismatch edge nextEdge
| validEdge nextEdge && previous triangulation nextEdge /= edge
]
++ [ EdgePreviousNextMismatch edge previousEdge
| validEdge previousEdge && next triangulation previousEdge /= edge
]
++ [EdgeDoubleReversalMismatch edge | reverseEdge twinEdge /= edge]
++ [EdgeSelfLinkedNext edge | nextEdge == edge && halfCount > 2]
++ [EdgeSelfLinkedPrevious edge | previousEdge == edge && halfCount > 2]
innerCycle =
if incidentFace triangulation edge /= outerFace && validEdge nextEdge && validEdge previousEdge
then
[ InnerFaceNotTriangularAtEdge edge
| next triangulation (next triangulation nextEdge) /= edge
]
else []
in if index < halfCount then local ++ innerCycle else []
validEdge (DirectedEdgeId value) = fromIntegral value < halfCount
faceViolations
| not (null rangeViolations) = []
| otherwise = concatMap validateFace (allFaces triangulation)
validateFace face@(FaceId _) =
case adjacentEdge triangulation face of
Nothing
| face == outerFace && halfCount == 0 -> []
| otherwise -> [FaceMissingAdjacentEdge face]
Just edge ->
[ FaceRepresentativeMismatch face edge representedFace
| let representedFace = incidentFace triangulation edge
, representedFace /= face
]
++ [ InnerFaceVertexCardinalityMismatch face (length faceVertexIds) (length (nub faceVertexIds))
| face /= outerFace
, let faceVertexIds = faceVertices triangulation face
, length faceVertexIds /= 3 || length (nub faceVertexIds) /= 3
]
vertexViolations
| vertexOutCount /= verticesCount || not (null rangeViolations) = []
| otherwise = concatMap validateVertex (vertices triangulation)
validateVertex vertex = case vertexOutEdge triangulation vertex of
Nothing
| verticesCount <= 1 -> []
| otherwise -> [ConnectedVertexMissingOutgoing vertex]
Just edge ->
[ VertexOutgoingOriginMismatch vertex edge actualOrigin
| let actualOrigin = origin triangulation edge
, actualOrigin /= vertex
]
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 the same DCEL edge, face, vertex, and orientation laws as
-- 'validateTopology', but only to a certified local closure. 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)
validateTopologyClosureWithStats
:: IntSet.IntSet
-> IntSet.IntSet
-> Triangulation mode vertex directed undirected face
-> (ValidationClosureStats, [InvariantViolation])
validateTopologyClosureWithStats admittedFaces interfacePairs triangulation =
( ValidationClosureStats
{ validationClosureFaces = IntSet.size selectedFaces
, validationClosureDirectedEdges = IntSet.size selectedEdges
, validationClosureVertices = IntSet.size selectedVertices
, validationClosureInterfacePairs = IntSet.size interfacePairs
, validationClosureConstraintPairs = IntSet.size selectedPairs
}
, rangeViolations ++ edgeViolations ++ faceViolations ++ vertexViolations ++ orientationViolations ++ constraintViolations
)
where
verticesCount = numVertices triangulation
halfCount = numDirectedEdges triangulation
facesCount = numFaces 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 faceRaw [incidentFace triangulation forward, incidentFace triangulation backward]
, rawFace > 0
]
selectedFaces = IntSet.union admittedFaces collarFaces
selectedFaceEdges =
IntSet.fromList
[ rawEdge
| rawFace <- IntSet.toAscList selectedFaces
, edge <- faceEdgesBounded (FaceId (fromIntegral rawFace))
, rawEdge <- [edgeRaw edge, edgeRaw (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
, edgeRaw (reverseEdge (DirectedEdgeId (fromIntegral rawEdge))) < halfCount
, rawVertex <-
[ vertexRaw (origin triangulation (DirectedEdgeId (fromIntegral rawEdge)))
, vertexRaw (destination triangulation (DirectedEdgeId (fromIntegral rawEdge)))
]
, rawVertex < verticesCount
]
rangeViolations =
[ EdgeOriginOutOfRange edge vertex verticesCount
| rawEdge <- IntSet.toAscList selectedEdges
, rawEdge >= 0
, let edge = DirectedEdgeId (fromIntegral rawEdge)
, rawEdge < halfCount
, let vertex = origin triangulation edge
, vertexRaw vertex >= verticesCount
]
++ [ EdgeNextOutOfRange edge nextEdge halfCount
| rawEdge <- IntSet.toAscList selectedEdges
, rawEdge >= 0
, rawEdge < halfCount
, let edge = DirectedEdgeId (fromIntegral rawEdge)
, let nextEdge = next triangulation edge
, edgeRaw nextEdge >= halfCount
]
++ [ EdgePreviousOutOfRange edge previousEdge halfCount
| rawEdge <- IntSet.toAscList selectedEdges
, rawEdge >= 0
, rawEdge < halfCount
, let edge = DirectedEdgeId (fromIntegral rawEdge)
, let previousEdge = previous triangulation edge
, edgeRaw previousEdge >= halfCount
]
++ [ EdgeFaceOutOfRange edge face facesCount
| rawEdge <- IntSet.toAscList selectedEdges
, rawEdge >= 0
, rawEdge < halfCount
, let edge = DirectedEdgeId (fromIntegral rawEdge)
, let face = incidentFace triangulation edge
, faceRaw face >= facesCount
]
++ [ FaceAdjacentOutOfRange face edge halfCount
| rawFace <- IntSet.toAscList selectedFaces
, rawFace > 0
, rawFace < facesCount
, let face = FaceId (fromIntegral rawFace)
, Just edge <- [adjacentEdge triangulation face]
, edgeRaw edge >= halfCount
]
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 = edgeRaw nextEdge < halfCount
previousValid = edgeRaw previousEdge < halfCount
nextNextValid = nextValid && edgeRaw (next triangulation nextEdge) < halfCount
innerCycle =
if incidentFace triangulation edge /= outerFace && nextValid && previousValid && nextNextValid
then [InnerFaceNotTriangularAtEdge edge | next triangulation (next triangulation nextEdge) /= edge]
else []
in [ EdgeNextPreviousMismatch edge nextEdge
| nextValid && previous triangulation nextEdge /= edge
]
++ [ EdgePreviousNextMismatch edge previousEdge
| previousValid && next triangulation previousEdge /= edge
]
++ [EdgeDoubleReversalMismatch edge | reverseEdge twinEdge /= edge]
++ [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
| edgeRaw 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 face
vertices' = fmap (vertexRaw . 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 rawVertex =
let vertex = VertexId (fromIntegral rawVertex)
in case vertexOutEdge triangulation vertex of
Nothing
| verticesCount > 1 -> [ConnectedVertexMissingOutgoing vertex]
| otherwise -> []
Just edge
| edgeRaw 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)
]
selectedPairs = IntSet.fromList [rawEdge `quot` 2 | rawEdge <- IntSet.toAscList selectedEdges]
faceEdgesBounded face =
case face of
FaceId raw
| toInteger raw <= 0 || toInteger raw >= toInteger facesCount -> []
_ -> adjacentEdges
where
adjacentEdges = case adjacentEdge triangulation face of
Nothing -> []
Just start
| edgeRaw start >= halfCount -> []
| otherwise ->
let second = next triangulation start
in if edgeRaw second >= halfCount
then [start]
else
let third = next triangulation second
in if edgeRaw third >= halfCount
then [start, second]
else [start, second, third]
edgeRaw (DirectedEdgeId raw) = fromIntegral raw
vertexRaw (VertexId raw) = fromIntegral raw
faceRaw (FaceId raw) = fromIntegral raw
-- | 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