moonlight-planar-1.1.0.0: src-hex/Moonlight/Hex/Topology.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Packed neighbourhood morphology and graph topology over native hex regions.
-- Dilation and erosion form an adjunction on each bounded layout; components
-- and distances are opaque views derived from one admitted selection.
module Moonlight.Hex.Topology
( hexRegionDilate
, hexRegionErode
, hexRegionOpening
, hexRegionClosing
, hexRegionInnerFrontier
, hexRegionOuterFrontier
, HexComponentLabels
, hexRegionComponentLabels
, hexComponentCount
, hexComponentIndexAt
, hexComponentRegion
, HexTraversalObstruction (..)
, HexDistanceMap
, hexRegionDistancesWithin
, hexDistanceMapLayout
, hexDistanceAt
, hexDistanceMaximum
, hexDistanceReachableRegion
) where
import Control.DeepSeq (NFData)
import Control.Monad.ST (ST, runST)
import Data.Bits ((.&.), (.|.), complement, shiftL)
import Data.Vector.Unboxed qualified as U
import Data.Vector.Unboxed.Mutable qualified as MU
import Data.Word (Word64)
import GHC.Generics (Generic)
import Moonlight.Hex.Coordinate
( HexCoord (..)
, HexDirection
, allHexDirections
, hexDirectionDelta
)
import Moonlight.Hex.Region (complementHexRegion)
import Moonlight.Hex.Region.Internal
( HexLayout (..)
, HexRegion (..)
, extractBits
, firstSameLayoutDifferenceIndex
, generateHexRegionIndexed
, hexCoordAtIndex
, hexCoordIndex
, hexRegionMemberAtIndex
)
-- | One closed-neighbourhood step, clipped to the owning layout.
hexRegionDilate :: HexRegion -> HexRegion
hexRegionDilate region@(HexRegion layout _) =
HexRegion layout (U.generate (layoutWordCount layout) (dilatedHexWord region))
{-# INLINE hexRegionDilate #-}
-- | Retain exactly the cells whose six neighbours are selected. A neighbour
-- outside the layout imposes no obligation: this is the right adjoint of
-- clipped dilation, so opening and closing remain lawful on bounded layouts.
hexRegionErode :: HexRegion -> HexRegion
hexRegionErode region@(HexRegion layout wordsValue) =
let absent = complementHexRegion region
in HexRegion layout
(U.generate (layoutWordCount layout) (erodedHexWord absent wordsValue))
{-# INLINE hexRegionErode #-}
dilatedHexWord :: HexRegion -> Int -> Word64
dilatedHexWord region@(HexRegion _ wordsValue) wordIndex =
foldl'
(\word direction -> word .|. translatedHexWord region direction wordIndex)
(wordsValue `U.unsafeIndex` wordIndex)
allHexDirections
{-# INLINE dilatedHexWord #-}
erodedHexWord :: HexRegion -> U.Vector Word64 -> Int -> Word64
erodedHexWord absent selectedWords wordIndex =
foldl'
(\word direction ->
word .&. complement (translatedHexWord absent direction wordIndex))
(selectedWords `U.unsafeIndex` wordIndex)
allHexDirections
{-# INLINE erodedHexWord #-}
-- | Erosion followed by dilation in the same bounded layout.
hexRegionOpening :: HexRegion -> HexRegion
hexRegionOpening = hexRegionDilate . hexRegionErode
-- | Dilation followed by erosion in the same bounded layout.
hexRegionClosing :: HexRegion -> HexRegion
hexRegionClosing = hexRegionErode . hexRegionDilate
-- | Selected cells adjacent to absence, including absence beyond the layout.
hexRegionInnerFrontier :: HexRegion -> HexRegion
hexRegionInnerFrontier source@(HexRegion layout sourceWords) =
let absent = complementHexRegion source
in HexRegion layout
( U.generate
(layoutWordCount layout)
(\wordIndex ->
sourceWords `U.unsafeIndex` wordIndex
.&. complement (erodedHexWord absent sourceWords wordIndex))
)
-- | Unselected cells inside the layout adjacent to a selected cell.
hexRegionOuterFrontier :: HexRegion -> HexRegion
hexRegionOuterFrontier source@(HexRegion layout sourceWords) =
HexRegion layout
( U.generate
(layoutWordCount layout)
(\wordIndex ->
dilatedHexWord source wordIndex
.&. complement (sourceWords `U.unsafeIndex` wordIndex))
)
-- | Canonical zero-based component labels in row-major seed order. Unselected
-- cells carry no public label and the view owns no independent membership.
data HexComponentLabels = HexComponentLabels !HexLayout {-# UNPACK #-} !Int !(U.Vector Int)
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
hexRegionComponentLabels :: HexRegion -> HexComponentLabels
hexRegionComponentLabels region@(HexRegion layout _) = runST $ do
labels <- MU.replicate (layoutCellCount layout) (-1)
queue <- MU.new (layoutCellCount layout)
count <- labelHexComponents region labels queue 0 0
HexComponentLabels layout count <$> U.unsafeFreeze labels
hexComponentCount :: HexComponentLabels -> Int
hexComponentCount (HexComponentLabels _ count _) = count
{-# INLINE hexComponentCount #-}
-- | The component of a selected coordinate. Absence means either an
-- unselected coordinate or one outside the labels' layout.
hexComponentIndexAt :: HexCoord -> HexComponentLabels -> Maybe Int
hexComponentIndexAt coordinate (HexComponentLabels layout _ labels) = do
index <- hexCoordIndex layout coordinate
let component = labels `U.unsafeIndex` index
if component < 0 then Nothing else Just component
{-# INLINE hexComponentIndexAt #-}
-- | Materialize one component on demand; invalid component indices are
-- rejected without fabricating an empty component.
hexComponentRegion :: Int -> HexComponentLabels -> Maybe HexRegion
hexComponentRegion requested (HexComponentLabels layout count labels)
| requested < 0 || requested >= count = Nothing
| otherwise = Just (generateHexRegionIndexed layout ((== requested) . (labels `U.unsafeIndex`)))
-- | Typed incompatibilities for traversal inside a selected domain.
data HexTraversalObstruction
= HexTraversalLayoutMismatch !HexLayout !HexLayout
| HexTraversalSourceOutsideDomain !HexCoord
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
-- | Dense shortest-path distances through one hex region. Negative internal
-- entries are unreachable and never escape through the public observation.
data HexDistanceMap = HexDistanceMap !HexLayout !(U.Vector Int)
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
-- | Multi-source unit distances through selected domain cells.
hexRegionDistancesWithin
:: HexRegion
-> HexRegion
-> Either HexTraversalObstruction HexDistanceMap
hexRegionDistancesWithin domain@(HexRegion domainLayout _) sources@(HexRegion sourceLayout _)
| domainLayout /= sourceLayout = Left (HexTraversalLayoutMismatch domainLayout sourceLayout)
| Just sourceIndex <- firstSameLayoutDifferenceIndex sources domain =
Left (HexTraversalSourceOutsideDomain (hexCoordAtIndex domainLayout sourceIndex))
| otherwise = Right (runST buildDistances)
where
buildDistances :: forall s. ST s HexDistanceMap
buildDistances = do
distances <- MU.replicate (layoutCellCount domainLayout) (-1)
queue <- MU.new (layoutCellCount domainLayout)
queued <- seedHexDistances sources distances queue 0 0
descendHexDistances domain distances queue 0 queued
HexDistanceMap domainLayout <$> U.unsafeFreeze distances
hexDistanceMapLayout :: HexDistanceMap -> HexLayout
hexDistanceMapLayout (HexDistanceMap layout _) = layout
{-# INLINE hexDistanceMapLayout #-}
hexDistanceAt :: HexCoord -> HexDistanceMap -> Maybe Int
hexDistanceAt coordinate (HexDistanceMap layout distances) = do
index <- hexCoordIndex layout coordinate
let distance = distances `U.unsafeIndex` index
if distance < 0 then Nothing else Just distance
{-# INLINE hexDistanceAt #-}
hexDistanceMaximum :: HexDistanceMap -> Maybe Int
hexDistanceMaximum (HexDistanceMap _ distances) =
let maximumValue = U.foldl' max (-1) distances
in if maximumValue < 0 then Nothing else Just maximumValue
hexDistanceReachableRegion :: HexDistanceMap -> HexRegion
hexDistanceReachableRegion (HexDistanceMap layout distances) =
generateHexRegionIndexed layout ((>= 0) . (distances `U.unsafeIndex`))
translatedHexWord :: HexRegion -> HexDirection -> Int -> Word64
translatedHexWord (HexRegion layout wordsValue) direction wordIndex =
gather 0 (wordIndex * 64) 0
where
HexCoord deltaQ deltaR = hexDirectionDelta direction
gather !destinationShift !targetIndex !accumulator
| destinationShift >= 64 || targetIndex >= layoutCellCount layout = accumulator
| otherwise =
let (targetRow, targetColumn) = targetIndex `quotRem` layoutWidth layout
rowRun = min (64 - destinationShift) (layoutWidth layout - targetColumn)
sourceRow = targetRow - deltaR
finalTargetColumn = targetColumn + rowRun - 1
copiedTargetColumn = max targetColumn deltaQ
copiedFinalTargetColumn = min finalTargetColumn (layoutWidth layout - 1 + deltaQ)
hasRow = sourceRow >= 0 && sourceRow < layoutHeight layout
hasColumns = copiedTargetColumn <= copiedFinalTargetColumn
copied =
if hasRow && hasColumns
then
let leading = copiedTargetColumn - targetColumn
copiedCount = copiedFinalTargetColumn - copiedTargetColumn + 1
sourceColumn = copiedTargetColumn - deltaQ
sourceBitIndex = sourceRow * layoutWidth layout + sourceColumn
in extractBits wordsValue sourceBitIndex copiedCount
`shiftL` (destinationShift + leading)
else 0
in gather
(destinationShift + rowRun)
(targetIndex + rowRun)
(accumulator .|. copied)
{-# INLINE translatedHexWord #-}
labelHexComponents
:: HexRegion
-> MU.MVector s Int
-> MU.MVector s Int
-> Int
-> Int
-> ST s Int
labelHexComponents region@(HexRegion layout _) labels queue index component
| index >= layoutCellCount layout = pure component
| not (hexRegionMemberAtIndex region index) =
labelHexComponents region labels queue (index + 1) component
| otherwise = do
existing <- MU.unsafeRead labels index
if existing >= 0
then labelHexComponents region labels queue (index + 1) component
else do
MU.unsafeWrite labels index component
MU.unsafeWrite queue 0 index
descendHexComponent region labels queue component 0 1
labelHexComponents region labels queue (index + 1) (component + 1)
descendHexComponent
:: HexRegion
-> MU.MVector s Int
-> MU.MVector s Int
-> Int
-> Int
-> Int
-> ST s ()
descendHexComponent region@(HexRegion layout _) labels queue component readIndex queued
| readIndex >= queued = pure ()
| otherwise = do
cellIndex <- MU.unsafeRead queue readIndex
nextQueued <-
foldHexNeighbourIndicesM
layout
cellIndex
(admitComponentCell region labels queue component)
queued
descendHexComponent region labels queue component (readIndex + 1) nextQueued
admitComponentCell
:: HexRegion
-> MU.MVector s Int
-> MU.MVector s Int
-> Int
-> Int
-> Int
-> ST s Int
admitComponentCell region labels queue component queued candidate
| not (hexRegionMemberAtIndex region candidate) = pure queued
| otherwise = do
existing <- MU.unsafeRead labels candidate
if existing >= 0
then pure queued
else do
MU.unsafeWrite labels candidate component
MU.unsafeWrite queue queued candidate
pure (queued + 1)
seedHexDistances
:: HexRegion
-> MU.MVector s Int
-> MU.MVector s Int
-> Int
-> Int
-> ST s Int
seedHexDistances region@(HexRegion layout _) distances queue index queued
| index >= layoutCellCount layout = pure queued
| hexRegionMemberAtIndex region index = do
MU.unsafeWrite distances index 0
MU.unsafeWrite queue queued index
seedHexDistances region distances queue (index + 1) (queued + 1)
| otherwise =
seedHexDistances region distances queue (index + 1) queued
descendHexDistances
:: HexRegion
-> MU.MVector s Int
-> MU.MVector s Int
-> Int
-> Int
-> ST s ()
descendHexDistances domain@(HexRegion layout _) distances queue readIndex queued
| readIndex >= queued = pure ()
| otherwise = do
cellIndex <- MU.unsafeRead queue readIndex
distance <- MU.unsafeRead distances cellIndex
nextQueued <-
foldHexNeighbourIndicesM
layout
cellIndex
(admitDistanceCell domain distances queue (distance + 1))
queued
descendHexDistances domain distances queue (readIndex + 1) nextQueued
admitDistanceCell
:: HexRegion
-> MU.MVector s Int
-> MU.MVector s Int
-> Int
-> Int
-> Int
-> ST s Int
admitDistanceCell region distances queue distance queued candidate
| not (hexRegionMemberAtIndex region candidate) = pure queued
| otherwise = do
existing <- MU.unsafeRead distances candidate
if existing >= 0
then pure queued
else do
MU.unsafeWrite distances candidate distance
MU.unsafeWrite queue queued candidate
pure (queued + 1)
foldHexNeighbourIndicesM
:: HexLayout
-> Int
-> (accumulator -> Int -> ST s accumulator)
-> accumulator
-> ST s accumulator
foldHexNeighbourIndicesM layout index step initial = do
let (row, column) = index `quotRem` layoutWidth layout
hasEast = column + 1 < layoutWidth layout
hasNorth = row > 0
hasWest = column > 0
hasSouth = row + 1 < layoutHeight layout
applyIf condition candidate accumulator =
if condition then step accumulator candidate else pure accumulator
east <- applyIf hasEast (index + 1) initial
northEast <- applyIf (hasNorth && hasEast) (index - layoutWidth layout + 1) east
northWest <- applyIf hasNorth (index - layoutWidth layout) northEast
west <- applyIf hasWest (index - 1) northWest
southWest <- applyIf (hasSouth && hasWest) (index + layoutWidth layout - 1) west
applyIf hasSouth (index + layoutWidth layout) southWest
{-# INLINE foldHexNeighbourIndicesM #-}