moonlight-planar-1.1.0.0: src-build/Moonlight/Planar/Internal/CircleSweep.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
module Moonlight.Planar.Internal.CircleSweep
( RadiallyOrderedArena
, radiallyOrderArena
, radiallyOrderGeometry
, circleSweepInsert
) where
import Moonlight.Planar.BuildStats (BuildMetric (HullInsertions, EdgeFlips, LegalizationMaxStack, SweepFastPoints, SweepSkippedPoints))
import Control.Monad (forM_, when)
import Control.Monad.ST (ST)
import Data.Bits ((.&.), shiftL, shiftR, xor)
import qualified Data.Vector.Algorithms.Intro as Intro
import qualified Data.Vector.Algorithms.Radix as Radix
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as MUV
import Data.Word (Word32, Word64)
import GHC.Float (castDoubleToWord64)
import Moonlight.Planar.Internal.HandleDefs (DirectedEdgeId (..))
import Moonlight.Planar.Insertion (insertExistingVertex)
import Moonlight.Planar.Internal.DcelOperations.Hull
( ReservedSweepCells
, SweepCellCursor
, SweepInsertion (..)
, closeOuterTurnReserved
, commitReservedSweepConnections
, fixHullConvexity
, initialSweepCellCursor
, insertOutsideHullAtEdge
, reserveSweepCells
)
import Moonlight.Planar.Internal.DcelOperations.CandidateArena
( seedGenericPairInArena
)
import Moonlight.Planar.Internal.DcelOperations.Chain
( connectAscendingLineArena
, prepareAscendingLineArena
)
import Moonlight.Planar.Internal.DcelOperations.Normalize
( LegalizationDrain (..)
, drainDenseUnconstrainedGenericLegalization
)
import Moonlight.Planar.Internal.Mutable
( DenseMutableDcel
, MutableDcel
, connectedCount
, denseMutableDcel
, denseMutableOwner
, denseFaceEdges
, denseReadFaceEdge
, denseReadNext
, denseReadOrigin
, denseReadPointX
, denseReadPointY
, denseReadPrevious
, directedEdgeCount
, faceCount
, halfEdgeCapacity
, pointCapacity
)
import Moonlight.Planar.Internal.OperationState
( Counter (..)
, LegalizationArena
, OperationState
, addCounter
, legalizationArena
, maxCounter
, readScratch
, storeLegalizationArena
, writeScratch
)
import Moonlight.Planar.Internal.PackedIndex (noIndex, unpackIndex)
import Moonlight.Planar.Internal.Probe (Probe (..))
import Moonlight.Planar.Internal.Predicates (orient2dCoordinates)
import Moonlight.Planar.Types (BuildError (..))
-- | The hull is an angular index over the DCEL outer-face cycle. The cycle
-- itself already owns hull adjacency, so left and right walking is
-- 'readPrevious'/'readNext' on the live mesh and this record only caches what
-- topology cannot state: the pseudo-angle of each outer edge's origin and the
-- bucket anchors accelerating the predecessor search. An outer edge's key is
-- its origin's @(angle, x, y)@ with the edge id as the final tie-break; the
-- angle lives in 'hullAngleByEdge' and the coordinates are re-read from the
-- immutable origin only when two cached angles compare exactly equal. Slots
-- of edges that have left the outer cycle are never read again, so the cache
-- needs no invalidation, and there is no second ring beside the authoritative
-- one.
data Hull s = Hull
{ hullCenterX :: {-# UNPACK #-} !Double
, hullCenterY :: {-# UNPACK #-} !Double
, hullBucketCapacity :: {-# UNPACK #-} !Int
, hullAngleByEdge :: !(MUV.MVector s Double)
}
-- | The strict state of the derived angular section. The bucket vector is
-- mutated only to transport the section across one local hull rewrite; its
-- extent and active cardinality are threaded as values, so the sweep does not
-- bounce through singleton mutable cells for facts already known at descent.
data HullIndex s = HullIndex
!(MUV.MVector s Word32)
{-# UNPACK #-} !Int
data DeferredInsertion s
= DeferredInsertionFailure !BuildError
| DeferredInsertionSuccess
{-# UNPACK #-} !Int
{-# UNPACK #-} !Int
{-# UNPACK #-} !SweepCellCursor
!(LegalizationArena s)
{-# UNPACK #-} !(HullIndex s)
data ClosedHullSection s = ClosedHullSection
{-# UNPACK #-} !Int
{-# UNPACK #-} !Int
{-# UNPACK #-} !Int
!(LegalizationArena s)
{-# UNPACK #-} !SweepCellCursor
-- | Vertex ids after their radial keys have descended to one total order. The
-- constructor is private: the circle sweep consumes the proof and therefore
-- never pays to establish the same ordering twice. Coordinates remain owned
-- by the DCEL rather than being duplicated in the sweep input.
newtype RadiallyOrderedArena s = RadiallyOrderedArena
(MUV.MVector s Word32)
radiallyOrderArena
:: MUV.MVector s (Double, Double, Double, Word32)
-> ST s (RadiallyOrderedArena s)
radiallyOrderArena arena = do
Intro.sort arena
let (_, _, _, vertices) = MUV.unzip4 arena
pure (RadiallyOrderedArena vertices)
{-# INLINE radiallyOrderArena #-}
-- | Order the geometry-only ingress by moving only its existing vertex-id
-- plane. Stable radix descent first orders the high 36 bits of each binary64
-- distance. Every equal-prefix fibre is then normalized by the complete
-- historical @(distance, x, y, vertex)@ order, recovering the discarded low
-- 28 bits before the fibres glue into the authoritative radial order. The key
-- coordinates remain in their immutable planes, so the sole linear scratch
-- plane is four bytes per site rather than a second copy of every radial
-- record.
--
-- Coordinate admission bounds every squared distance to a finite,
-- non-negative binary64 value, whose unsigned word order is numeric order.
radiallyOrderGeometry
:: U.Vector Double
-> U.Vector Double
-> U.Vector Double
-> MUV.MVector s Word32
-> ST s (RadiallyOrderedArena s)
radiallyOrderGeometry distances pointXs pointYs vertices = do
Radix.sortBy
radialPassCount
radialBucketCount
(radialDistanceBucket distances)
vertices
when (MUV.length vertices > 1) $ do
first <- MUV.unsafeRead vertices 0
let !firstPrefix = geometryDistancePrefix distances first
(!finalFibreStart, _) <-
MUV.ifoldM'
(\(!fibreStart, !distancePrefix) relativeIndex vertex ->
let !nextPrefix = geometryDistancePrefix distances vertex
!index = relativeIndex + 1
in if nextPrefix == distancePrefix
then pure (fibreStart, distancePrefix)
else do
sortGeometryPrefixFibre
distances
pointXs
pointYs
vertices
fibreStart
index
pure (index, nextPrefix)
)
(0, firstPrefix)
(MUV.unsafeSlice 1 (MUV.length vertices - 1) vertices)
sortGeometryPrefixFibre
distances
pointXs
pointYs
vertices
finalFibreStart
(MUV.length vertices)
pure (RadiallyOrderedArena vertices)
{-# INLINE radiallyOrderGeometry #-}
radialPassCount :: Int
radialPassCount = 3
radialBucketCount :: Int
radialBucketCount = 1 `shiftL` radialPassWidth
radialPassWidth :: Int
radialPassWidth = 12
radialPrefixDiscardedBits :: Int
radialPrefixDiscardedBits = 64 - radialPassCount * radialPassWidth
radialBucketMask :: Word64
radialBucketMask = fromIntegral (radialBucketCount - 1)
radialDistanceBucket :: U.Vector Double -> Int -> Word32 -> Int
radialDistanceBucket distances pass vertex =
fromIntegral
( (castDoubleToWord64 (geometryDistance distances vertex)
`shiftR` (radialPrefixDiscardedBits + radialPassWidth * pass)
)
.&. radialBucketMask
)
{-# INLINE radialDistanceBucket #-}
geometryDistancePrefix :: U.Vector Double -> Word32 -> Word64
geometryDistancePrefix distances vertex =
castDoubleToWord64 (geometryDistance distances vertex)
`shiftR` radialPrefixDiscardedBits
{-# INLINE geometryDistancePrefix #-}
geometryDistance :: U.Vector Double -> Word32 -> Double
geometryDistance distances vertex = U.unsafeIndex distances (fromIntegral vertex)
{-# INLINE geometryDistance #-}
compareGeometryVertex
:: U.Vector Double
-> U.Vector Double
-> U.Vector Double
-> Word32
-> Word32
-> Ordering
compareGeometryVertex distances pointXs pointYs left right =
case compare (geometryDistance distances left) (geometryDistance distances right) of
EQ ->
case compare (coordinateAt pointXs left) (coordinateAt pointXs right) of
EQ ->
case compare (coordinateAt pointYs left) (coordinateAt pointYs right) of
EQ -> compare left right
yOrder -> yOrder
xOrder -> xOrder
distanceOrder -> distanceOrder
{-# INLINE compareGeometryVertex #-}
coordinateAt :: U.Vector Double -> Word32 -> Double
coordinateAt coordinates vertex = U.unsafeIndex coordinates (fromIntegral vertex)
{-# INLINE coordinateAt #-}
sortGeometryPrefixFibre
:: U.Vector Double
-> U.Vector Double
-> U.Vector Double
-> MUV.MVector s Word32
-> Int
-> Int
-> ST s ()
sortGeometryPrefixFibre distances pointXs pointYs vertices start end =
when (end - start > 1) $
Intro.sortBy
(compareGeometryVertex distances pointXs pointYs)
(MUV.unsafeSlice start (end - start) vertices)
{-# INLINE sortGeometryPrefixFibre #-}
-- | Circle sweep over one mutable DCEL, consuming one radial vertex order.
-- Coordinates are read from the authoritative dense DCEL. Insertions
-- initially close only acute hull turns. One terminal Graham pass restores
-- full convexity, so construction does not repeatedly pay for global
-- convexity that no intermediate observer can see.
circleSweepInsert
:: MutableDcel s vertex directed undirected face
-> OperationState s
-> RadiallyOrderedArena s
-> ST s (Either BuildError Int)
circleSweepInsert mutable operation (RadiallyOrderedArena arena) =
case denseMutableDcel mutable of
Nothing -> pure (Left CircleSweepRequiresDenseStorage)
Just dense -> circleSweepInsertDense dense operation arena
{-# INLINE circleSweepInsert #-}
circleSweepInsertDense
:: forall s vertex directed undirected face
. DenseMutableDcel s vertex directed undirected face
-> OperationState s
-> MUV.MVector s Word32
-> ST s (Either BuildError Int)
circleSweepInsertDense dense operation arena
| MUV.length arena == 0 = pure (Right 0)
| otherwise = do
residentCount <- connectedCount mutable
ascendingLine <-
if residentCount == 0
then prepareAscendingLineArena dense arena
else pure Nothing
case ascendingLine of
Just line -> connectAscendingLineArena dense operation line
Nothing -> circleSweepInsertArea dense operation arena
where
!mutable = denseMutableOwner dense
{-# INLINE circleSweepInsertDense #-}
circleSweepInsertArea
:: forall s vertex directed undirected face
. DenseMutableDcel s vertex directed undirected face
-> OperationState s
-> MUV.MVector s Word32
-> ST s (Either BuildError Int)
circleSweepInsertArea dense operation arena = do
let !ordered = MUV.length arena
seed <- insertSeed 0
case seed of
Left failure -> pure (Left failure)
Right seedCount -> do
faces <- faceCount mutable
if faces <= 1 || seedCount >= ordered
then pure (Right seedCount)
else do
(centerX, centerY) <- seedCentre dense
reservedOutcome <- reserveSweepCells dense (ordered - seedCount)
case reservedOutcome of
Left failure -> pure (Left failure)
Right reserved -> do
builtHull <- buildHull dense operation centerX centerY
case builtHull of
Left failure -> pure (Left failure)
Right (!hull, !initialHullIndex) -> do
initialLegalizationArena <- legalizationArena operation
let !initialCursor = initialSweepCellCursor reserved
inserted <-
insertRemaining
reserved
initialCursor
initialLegalizationArena
hull
initialHullIndex
ordered
seedCount
(0 :: Int)
(0 :: Int)
(0 :: Int)
(0 :: Int)
case inserted of
Left failure -> pure (Left failure)
Right (!skippedCount, !fastCount, !flips, !maxDepth, !sweepCursor, !sweepArena) -> do
repaired <- fixHullConvexity reserved sweepCursor operation sweepArena
case repaired of
Left failure -> pure (Left failure)
Right (!_closures, !terminalFlips, !terminalMaxDepth, !finalArena, !finalCursor) -> do
commitReservedSweepConnections reserved finalCursor fastCount
storeLegalizationArena operation finalArena
-- The sweep counts its own hull insertions; its drains
-- hand their flip and depth tallies up once.
addCounter operation (Reported HullInsertions) fastCount
-- The angular candidate leaves a point to the fallback
-- whenever it lands right of, or on, the hull edge its
-- own angle selected — spade's
-- `is_on_right_side_or_on_line` branch, which spade's own
-- source calls "very slow". Both counts ride out in
-- BuildStats so the split is comparable across the two
-- implementations and satisfies
-- seed + fast + skipped = unique.
--
-- (Reported SweepFastPoints) is NOT (Reported HullInsertions)
-- renamed: the latter is also charged by
-- insertOutsideHull, so it counts hull-adjacent
-- insertions from either path and dominates this one
-- whenever a skipped point lands outside the hull. The
-- two coincide only while skipped is zero, and charging
-- them independently is what makes the identity a check
-- rather than a restatement.
addCounter operation (Reported SweepFastPoints) fastCount
addCounter operation (Reported SweepSkippedPoints) skippedCount
addCounter operation (Reported EdgeFlips) flips
maxCounter operation (Reported LegalizationMaxStack) maxDepth
addCounter operation (Reported EdgeFlips) terminalFlips
maxCounter operation (Reported LegalizationMaxStack) terminalMaxDepth
insertedSkipped <- insertSkipped skippedCount 0
pure (seedCount <$ insertedSkipped)
where
!mutable = denseMutableOwner dense
insertSeed !index
| index >= MUV.length arena = pure (Right index)
| otherwise = do
raw <- MUV.unsafeRead arena index
result <- insertExistingVertex @'ProbeOff mutable operation (unpackIndex raw)
case result of
Left failure -> pure (Left failure)
Right () -> do
faces <- faceCount mutable
if faces > 1
then pure (Right (index + 1))
else insertSeed (index + 1)
insertRemaining
:: ReservedSweepCells s vertex directed undirected face
-> SweepCellCursor
-> LegalizationArena s
-> Hull s
-> HullIndex s
-> Int
-> Int
-> Int
-> Int
-> Int
-> Int
-> ST s (Either BuildError (Int, Int, Int, Int, SweepCellCursor, LegalizationArena s))
insertRemaining !reserved !cursor !candidateArena !hull !hullIndex !ordered !index !skippedCount !fastCount !flips !maxDepth
| index >= ordered = pure (Right (skippedCount, fastCount, flips, maxDepth, cursor, candidateArena))
| otherwise = do
raw <- MUV.unsafeRead arena index
let !vertex = unpackIndex raw
queryXWide <- denseReadPointX dense vertex
queryYWide <- denseReadPointY dense vertex
let !queryAngle =
pseudoAngle (hullCenterX hull) (hullCenterY hull) queryXWide queryYWide
edge <- hullCandidate dense hull hullIndex queryAngle queryXWide queryYWide
fromVertex <- denseReadOrigin dense edge
toVertex <- denseReadOrigin dense (edge `xor` 1)
fromX <- denseReadPointX dense fromVertex
fromY <- denseReadPointY dense fromVertex
toX <- denseReadPointX dense toVertex
toY <- denseReadPointY dense toVertex
if orient2dCoordinates fromX fromY toX toY queryXWide queryYWide == GT
then do
deferred <-
insertDeferred
reserved
cursor
dense
hull
hullIndex
candidateArena
edge
fromVertex
toVertex
vertex
queryXWide
queryYWide
queryAngle
case deferred of
DeferredInsertionFailure failure -> pure (Left failure)
DeferredInsertionSuccess !newFlips !newMaxDepth !nextCursor !nextArena !nextHullIndex ->
insertRemaining
reserved
nextCursor
nextArena
hull
nextHullIndex
ordered
(index + 1)
skippedCount
(fastCount + 1)
(flips + newFlips)
(max maxDepth newMaxDepth)
else do
-- The radial order is consumed monotonically, so its processed
-- prefix is dead. Compact deferred vertices into that prefix
-- instead of allocating a second linear arena.
MUV.unsafeWrite arena skippedCount raw
insertRemaining reserved cursor candidateArena hull hullIndex ordered (index + 1) (skippedCount + 1) fastCount flips maxDepth
insertSkipped !count = go
where
go !index
| index >= count = pure (Right ())
| otherwise = do
vertex <- fromIntegral <$> MUV.unsafeRead arena index
inserted <- insertExistingVertex @'ProbeOff mutable operation vertex
case inserted of
Left failure -> pure (Left failure)
Right () -> go (index + 1)
-- | The hull centre: the centroid of the first inner face, in the widened
-- comparison format, computed exactly as @centroid@ states it.
seedCentre
:: DenseMutableDcel s vertex directed undirected face
-> ST s (Double, Double)
seedCentre dense = do
(e0, e1, e2) <- denseFaceEdges dense 1
o0 <- denseReadOrigin dense e0
o1 <- denseReadOrigin dense e1
o2 <- denseReadOrigin dense e2
x0 <- denseReadPointX dense o0
y0 <- denseReadPointY dense o0
x1 <- denseReadPointX dense o1
y1 <- denseReadPointY dense o1
x2 <- denseReadPointX dense o2
y2 <- denseReadPointY dense o2
pure (x0 + (x1 - x0) / 3 + (x2 - x0) / 3, y0 + (y1 - y0) / 3 + (y2 - y0) / 3)
-- | Index the authoritative outer cycle. The seed fan is star-shaped around
-- the first face's centroid — a collinear chain closed by its apex — so the
-- cycle is already the angular order the predecessor search assumes; what
-- remains is caching each edge's angle and anchoring the buckets.
buildHull
:: DenseMutableDcel s vertex directed undirected face
-> OperationState s
-> Double
-> Double
-> ST s (Either BuildError (Hull s, HullIndex s))
buildHull dense operation centerX centerY = do
let !mutable = denseMutableOwner dense
countResult <- collectOuterEdges dense operation
case countResult of
Left failure -> pure (Left failure)
Right count
| count <= 0 -> pure (Left CircleSweepHullEmpty)
| otherwise -> do
let !capacity = max (count + 4) (pointCapacity mutable + 8)
hullAngleByEdge <- MUV.new (halfEdgeCapacity mutable)
let hull =
Hull
{ hullCenterX = centerX
, hullCenterY = centerY
, hullBucketCapacity = capacity
, hullAngleByEdge
}
forM_ [0 .. count - 1] $ \index -> do
edge <- readScratch operation index
origin <- denseReadOrigin dense edge
x <- denseReadPointX dense origin
y <- denseReadPointY dense origin
MUV.unsafeWrite hullAngleByEdge edge (pseudoAngle centerX centerY x y)
hullIndex <- rebuildBuckets dense hull count (initialBucketCount count capacity)
pure (Right (hull, hullIndex))
collectOuterEdges
:: forall s vertex directed undirected face.
DenseMutableDcel s vertex directed undirected face
-> OperationState s
-> ST s (Either BuildError Int)
collectOuterEdges dense operation = do
let !mutable = denseMutableOwner dense
start <- denseReadFaceEdge dense 0
if start < 0
then pure (Right 0)
else do
bound <- directedEdgeCount mutable
go (bound + 1) start start False 0
where
go
:: Int
-> Int
-> Int
-> Bool
-> Int
-> ST s (Either BuildError Int)
go !remaining !start !edge !seen !count
| remaining <= 0 =
pure
( Left
( OuterCycleDidNotTerminate
(DirectedEdgeId (fromIntegral start))
(DirectedEdgeId (fromIntegral edge))
count
)
)
| seen && edge == start = pure (Right count)
| otherwise = do
writeScratch operation count edge
following <- denseReadNext dense edge
go (remaining - 1) start following True (count + 1)
initialBucketCount :: Int -> Int -> Int
initialBucketCount active capacity =
min capacity (nextPowerOfTwo (max 8 ((active + 1) `quot` 2)))
nextPowerOfTwo :: Int -> Int
nextPowerOfTwo requested = go 1
where
target = max 1 requested
go !value
| value >= target = value
| value > maxBound `quot` 2 = maxBound
| otherwise = go (value * 2)
readAngle :: Hull s -> Int -> ST s Double
readAngle hull edge = MUV.unsafeRead (hullAngleByEdge hull) edge
{-# INLINE readAngle #-}
rebuildBuckets
:: forall s vertex directed undirected face.
DenseMutableDcel s vertex directed undirected face
-> Hull s
-> Int
-> Int
-> ST s (HullIndex s)
rebuildBuckets dense hull active requested = do
let !count = max 1 (min (hullBucketCapacity hull) requested)
buckets <- MUV.replicate count noIndex
start <- denseReadFaceEdge dense 0
let go
:: Int
-> Int
-> ST s ()
go !remaining !edge
| remaining <= 0 = pure ()
| otherwise = do
following <- denseReadNext dense edge
angle <- readAngle hull edge
followingAngle <- readAngle hull following
writeBucketSegment buckets angle followingAngle edge
go (remaining - 1) following
when (active > 0 && start >= 0) (go active start)
pure (HullIndex buckets active)
maybeGrowBuckets :: DenseMutableDcel s vertex directed undirected face -> Hull s -> HullIndex s -> ST s (HullIndex s)
maybeGrowBuckets dense hull hullIndex@(HullIndex buckets active) = do
let !current = MUV.length buckets
if active > 2 * current && current < hullBucketCapacity hull
then rebuildBuckets dense hull active (min (hullBucketCapacity hull) (2 * current))
else pure hullIndex
bucketFor :: Int -> Double -> Int
bucketFor count angle =
min (count - 1) (max 0 (floor (angle * fromIntegral count * 0.25)))
{-# INLINE bucketFor #-}
ceilingBucketFor :: Int -> Double -> Int
ceilingBucketFor count angle =
ceiling (angle * fromIntegral count * 0.25) `rem` count
{-# INLINE ceilingBucketFor #-}
-- | Install the authoritative outer edge whose angular segment contains each
-- bucket boundary in the half-open clockwise arc from @fromAngle@ to
-- @toAngle@. Adjacency remains solely in the DCEL; this is the derived section
-- needed to land a lookup near that ring.
writeBucketSegment
:: MUV.MVector s Word32
-> Double
-> Double
-> Int
-> ST s ()
writeBucketSegment buckets fromAngle toAngle edge = do
let !count = MUV.length buckets
!fromBucket = ceilingBucketFor count fromAngle
!toBucket = ceilingBucketFor count toAngle
!packed = fromIntegral edge
case compare fromBucket toBucket of
LT -> MUV.set (MUV.unsafeSlice fromBucket (toBucket - fromBucket) buckets) packed
GT -> do
MUV.set (MUV.unsafeSlice fromBucket (count - fromBucket) buckets) packed
MUV.set (MUV.unsafeSlice 0 toBucket buckets) packed
EQ -> pure ()
{-# INLINE writeBucketSegment #-}
-- | Whether an outer edge's key orders at or before the stated query key,
-- settled field by field without materializing either key.
edgeAtMostAtAngle
:: DenseMutableDcel s vertex directed undirected face
-> Int
-> Double
-> Double
-> Double
-> Double
-> Int
-> ST s Bool
edgeAtMostAtAngle dense edge angle queryAngle queryX queryY tie =
case compare angle queryAngle of
LT -> pure True
GT -> pure False
EQ -> do
origin <- denseReadOrigin dense edge
x <- denseReadPointX dense origin
case compare x queryX of
LT -> pure True
GT -> pure False
EQ -> do
y <- denseReadPointY dense origin
case compare y queryY of
LT -> pure True
GT -> pure False
EQ -> pure (edge <= tie)
{-# INLINE edgeAtMostAtAngle #-}
-- | Reconcile the derived angular index once after the local topology section
-- has glued. Replacing one outer edge by two adds one active edge; every closed
-- turn removes one. The final two edges cover the entire rewritten angular
-- arc, so their segments descend to the bucket view in one gluing step.
finishHullRewrite
:: DenseMutableDcel s vertex directed undirected face
-> Hull s
-> HullIndex s
-> Int
-> Int
-> Int
-> ST s (HullIndex s)
finishHullRewrite dense hull (HullIndex buckets active) leftEdge rightEdge activeDelta = do
leftAngle <- readAngle hull leftEdge
middleAngle <- readAngle hull rightEdge
afterRight <- denseReadNext dense rightEdge
rightAngle <- readAngle hull afterRight
writeBucketSegment buckets leftAngle middleAngle leftEdge
writeBucketSegment buckets middleAngle rightAngle rightEdge
maybeGrowBuckets dense hull (HullIndex buckets (active + activeDelta))
{-# INLINE finishHullRewrite #-}
-- | The outer edge whose key is the greatest key at or below the query: the
-- visible candidate the sweep inserts against. Bucket anchors land the walk
-- near the answer and the live outer cycle carries it the rest of the way.
hullCandidate
:: forall s vertex directed undirected face.
DenseMutableDcel s vertex directed undirected face
-> Hull s
-> HullIndex s
-> Double
-> Double
-> Double
-> ST s Int
hullCandidate dense hull (HullIndex buckets active) queryAngle queryX queryY = do
let !count = MUV.length buckets
!bucket = bucketFor count queryAngle
raw <- MUV.unsafeRead buckets bucket
if raw == noIndex
then denseReadFaceEdge dense 0
else adjustFromBoundary bucket (unpackIndex raw)
where
adjustFromBoundary
:: Int
-> Int
-> ST s Int
adjustFromBoundary !bucket !initial = do
initialAngle <- readAngle hull initial
initialAtMost <- cyclicAtMost bucket initial initialAngle
if initialAtMost
then advance active initial initialAngle
else retreat active initial
where
cyclicAtMost
:: Int
-> Int
-> Double
-> ST s Bool
cyclicAtMost boundaryBucket edge angle =
if boundaryBucket == 0 && angle > queryAngle
then pure True
else edgeAtMostAtAngle dense edge angle queryAngle queryX queryY maxBound
advance
:: Int
-> Int
-> Double
-> ST s Int
advance !remaining !edge !edgeAngle
| remaining <= 0 = pure initial
| otherwise = do
following <- denseReadNext dense edge
followingAngle <- readAngle hull following
-- Only the bucket-zero anchor may precede the query by crossing the
-- angular seam. Once the walk leaves that anchor, ordinary key order
-- is authoritative; treating every high-angle edge as below a
-- bucket-zero query walks straight past the answer and around the
-- entire ring.
let crossesSeam = followingAngle < edgeAngle
seamPermitted = bucket == 0 && edgeAngle > queryAngle
followingAtMost <- edgeAtMostAtAngle dense following followingAngle queryAngle queryX queryY maxBound
if (not crossesSeam || seamPermitted) && followingAtMost
then advance (remaining - 1) following followingAngle
else pure edge
retreat
:: Int
-> Int
-> ST s Int
retreat !remaining !edge
| remaining <= 0 = pure initial
| otherwise = do
previous <- denseReadPrevious dense edge
previousAngle <- readAngle hull previous
previousAtMost <- cyclicAtMost bucket previous previousAngle
if previousAtMost
then advance remaining previous previousAngle
else retreat (remaining - 1) previous
insertDeferred
:: ReservedSweepCells s vertex directed undirected face
-> SweepCellCursor
-> DenseMutableDcel s vertex directed undirected face
-> Hull s
-> HullIndex s
-> LegalizationArena s
-> Int
-> Int
-> Int
-> Int
-> Double
-> Double
-> Double
-> ST s (DeferredInsertion s)
insertDeferred reserved cursor dense hull hullIndex arena replacedEdge fromVertex toVertex vertex insertedX insertedY insertedAngle = do
inserted <-
insertOutsideHullAtEdge
reserved
cursor
arena
replacedEdge
fromVertex
toVertex
vertex
case inserted of
SweepInsertionFailure failure -> pure (DeferredInsertionFailure failure)
SweepInsertion firstOuterEdge lastOuterEdge initialFlips initialMaxDepth arenaAfterInsertion cursorAfterInsertion ->
insertDeferredBetween
firstOuterEdge
lastOuterEdge
initialFlips
initialMaxDepth
arenaAfterInsertion
cursorAfterInsertion
where
insertDeferredBetween firstEdge lastEdge initialFlips initialMaxDepth arenaAfterInsertion cursorAfterInsertion = do
-- The patch proves the new keys algebraically, so no key is rediscovered
-- from the mesh: replacing outer edge a->b with a->v, v->b keeps the
-- replaced edge's origin for the first spoke (its cached angle carries
-- over) and starts the last spoke at the inserted vertex, whose angle is the
-- one the candidate search was already given. The tie-break is each fresh
-- edge id itself.
replacedAngle <- readAngle hull replacedEdge
-- Both spokes join the outer cycle before either is indexed, and a bucket
-- rebuild during activation walks the live cycle: both angle slots must be
-- initialized before the first activation can read them.
MUV.unsafeWrite (hullAngleByEdge hull) firstEdge replacedAngle
MUV.unsafeWrite (hullAngleByEdge hull) lastEdge insertedAngle
-- Every turn this insertion closes legalizes together in one epoch. Closure
-- never deletes an edge and never touches the outer cycle, so which turns
-- close does not depend on when the interior is repaired; the fan above
-- keeps its own drain because its candidates are oriented against the
-- inserted vertex, and that star must still be intact when they are tested.
ClosedHullSection left closuresLeft topLeft arenaAfterLeft cursorAfterLeft <- closeLeft firstEdge 0 0 arenaAfterInsertion cursorAfterInsertion
ClosedHullSection right closuresRight topAll arenaAfterRight cursorAfterRight <- closeRight lastEdge 0 topLeft arenaAfterLeft cursorAfterLeft
let !closures = closuresLeft + closuresRight
nextHullIndex <- finishHullRewrite dense hull hullIndex left right (1 - closures)
(!flips, !maxDepth, !finalArena) <-
if topAll == 0
then pure (0, 0, arenaAfterRight)
else do
LegalizationDrain drainedFlips drainedMaxDepth () drainedArena <-
drainDenseUnconstrainedGenericLegalization dense arenaAfterRight topAll
pure (drainedFlips, drainedMaxDepth, drainedArena)
pure
( DeferredInsertionSuccess
(initialFlips + flips)
(max initialMaxDepth maxDepth)
cursorAfterRight
finalArena
nextHullIndex
)
closeLeft !current !closures !top !sectionArena !sectionCursor = do
left <- denseReadPrevious dense current
close <- shouldCloseLeftTurn dense hull insertedAngle insertedX insertedY left current
if not close
then pure (ClosedHullSection current closures top sectionArena sectionCursor)
else do
leftAngle <- readAngle hull left
(replacement, nextCursor) <- closeOuterTurnReserved reserved sectionCursor left
-- Closing consecutive a->b, b->c into a->c keeps the first edge's
-- origin, so the replacement inherits its angle; the tie is the fresh
-- edge id. Both retired edges answer to the same left neighbour, the
-- edge now preceding the replacement on the outer cycle. Seeding
-- happens here, after the replacement's links exist.
MUV.unsafeWrite (hullAngleByEdge hull) replacement leftAngle
(nextArena, nextTop) <- seedGenericPairInArena sectionArena top left current
closeLeft replacement (closures + 1) nextTop nextArena nextCursor
closeRight !current !closures !top !sectionArena !sectionCursor = do
right <- denseReadNext dense current
close <- shouldCloseRightTurn dense hull insertedAngle insertedX insertedY current right
if not close
then pure (ClosedHullSection current closures top sectionArena sectionCursor)
else do
currentAngle <- readAngle hull current
(replacement, nextCursor) <- closeOuterTurnReserved reserved sectionCursor current
MUV.unsafeWrite (hullAngleByEdge hull) replacement currentAngle
(nextArena, nextTop) <- seedGenericPairInArena sectionArena top current right
closeRight replacement (closures + 1) nextTop nextArena nextCursor
{-# INLINE insertDeferred #-}
-- | Test the left-hand turn where the inserted point is the target of the
-- second edge. Adjacency is not rediscovered: 'closeLeft' obtained @first@
-- from @previous second@ in the same local section.
shouldCloseLeftTurn
:: DenseMutableDcel s vertex directed undirected face
-> Hull s
-> Double
-> Double
-> Double
-> Int
-> Int
-> ST s Bool
shouldCloseLeftTurn dense hull insertedAngle insertedX insertedY first second = do
fromVertex <- denseReadOrigin dense first
middleVertex <- denseReadOrigin dense (first `xor` 1)
fromX <- denseReadPointX dense fromVertex
fromY <- denseReadPointY dense fromVertex
middleX <- denseReadPointX dense middleVertex
middleY <- denseReadPointY dense middleVertex
middleAngle <- readAngle hull second
-- Same-ray/acute compatibility rejects most local sections. Settle that
-- cheap obstruction before paying for the exact orientation predicate; the
-- conjunction is unchanged, only its evaluation order is less profligate.
if
middleAngle /= insertedAngle
&& not (acuteAtMiddle fromX fromY middleX middleY insertedX insertedY)
then pure False
else pure (orient2dCoordinates fromX fromY middleX middleY insertedX insertedY == GT)
{-# INLINE shouldCloseLeftTurn #-}
-- | The symmetric right-hand test, where the inserted point is the first
-- edge's source. 'closeRight' obtained @second@ from @next first@, so this
-- section likewise consumes that adjacency proof instead of reading it again.
shouldCloseRightTurn
:: DenseMutableDcel s vertex directed undirected face
-> Hull s
-> Double
-> Double
-> Double
-> Int
-> Int
-> ST s Bool
shouldCloseRightTurn dense hull insertedAngle insertedX insertedY first second = do
middleVertex <- denseReadOrigin dense (first `xor` 1)
targetVertex <- denseReadOrigin dense (second `xor` 1)
middleX <- denseReadPointX dense middleVertex
middleY <- denseReadPointY dense middleVertex
targetX <- denseReadPointX dense targetVertex
targetY <- denseReadPointY dense targetVertex
middleAngle <- readAngle hull second
-- Symmetric to the left-hand descent above: compatibility first, exact
-- orientation only for sections that can actually glue.
if
middleAngle /= insertedAngle
&& not (acuteAtMiddle insertedX insertedY middleX middleY targetX targetY)
then pure False
else pure (orient2dCoordinates insertedX insertedY middleX middleY targetX targetY == GT)
{-# INLINE shouldCloseRightTurn #-}
-- Spade's deferred-convexity rule is local: close the turn when the angle at
-- the shared hull vertex is strictly below 90 degrees. Requiring the entire
-- triangle to be acute leaves avoidable star-hull work for the terminal pass.
acuteAtMiddle :: Double -> Double -> Double -> Double -> Double -> Double -> Bool
acuteAtMiddle ax ay bx by cx cy =
let !ux = ax - bx
!uy = ay - by
!vx = cx - bx
!vy = cy - by
!dot = ux * vx + uy * vy
!scale = max 1 (ux * ux + uy * uy + vx * vx + vy * vy)
in dot > 64 * encodeFloat 1 (-52) * scale
{-# INLINE acuteAtMiddle #-}
-- Clockwise pseudo-angle in [0,4), matching the orientation of the outer-face
-- cycle.
pseudoAngle :: Double -> Double -> Double -> Double -> Double
pseudoAngle centerX centerY x y
| norm == 0 = 0
| raw >= 4 = 0
| otherwise = raw
where
!dx = x - centerX
!dy = y - centerY
!norm = abs dx + abs dy
!projection = dx / norm
!raw = if dy > 0 then 1 + projection else 3 - projection
{-# INLINE pseudoAngle #-}