packages feed

moonlight-planar-1.0.0.0: src-public/Moonlight/Hex/Planar.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}

-- | Exact planar interpretation of native hexagonal cells and regions.
module Moonlight.Hex.Planar
  ( HexPlanarObstruction (..)
  , hexCellExactLoop
  , hexRegionPlanarRegion
  ) where

import Control.DeepSeq (NFData)
import Data.Bifunctor (first)
import Data.Either (isRight)
import Data.Foldable (foldlM)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import GHC.Generics (Generic)
import Moonlight.Hex.Coordinate
  ( HexCoord
  , HexDirection
  , allHexDirections
  , hexStepCoord
  )
import Moonlight.Hex.Element
  ( HexVertex
  , hexBoundaryFrom
  , hexBoundaryTo
  , hexCellBoundarySide
  , hexCellVertices
  , hexVertexCoordinates
  )
import Moonlight.Hex.Region
  ( HexRegion
  , foldHexRegionCoords
  , hexRegionMember
  )
import Moonlight.Triangulation.Exact (ExactPoint, exactPoint)
import Moonlight.Triangulation.Region
  ( ExactLoop
  , PlanarRegion
  , RegionValidationError
  , emptyPlanarRegion
  , exactLoop
  , planarRegion
  , polygonComponent
  )

-- | Every typed obstruction in boundary descent and exact publication.
data HexPlanarObstruction
  = HexPlanarRegionInvalid !RegionValidationError
  | HexPlanarBoundaryOpen !HexVertex
  | HexPlanarBoundaryVectorUnexpected !HexVertex !HexVertex
  | HexPlanarBoundaryDegenerate !(NonEmpty HexVertex)
  | HexPlanarHoleUnowned !ExactLoop
  deriving stock (Eq, Show, Generic)
  deriving anyclass (NFData)

hexCellExactLoop :: HexCoord -> Either HexPlanarObstruction ExactLoop
hexCellExactLoop =
  first HexPlanarRegionInvalid
    . exactLoop
    . fmap exactVertexPoint
    . hexCellVertices

-- | Trace the exterior sides of the selected native section, classify the
-- resulting exact loops by winding, and publish one canonical planar region.
-- Interior cell sides never enter the boundary graph.
hexRegionPlanarRegion :: HexRegion -> Either HexPlanarObstruction PlanarRegion
hexRegionPlanarRegion region
  | Set.null boundary = Right emptyPlanarRegion
  | otherwise = do
      vertexLoops <- traceBoundaryLoops boundary
      exactLoops <- traverse admitBoundaryLoop vertexLoops
      publishClassifiedLoops exactLoops
 where
  boundary = regionBoundaryEdges region

  admitBoundaryLoop
    :: NonEmpty HexVertex
    -> Either HexPlanarObstruction (Integer, ExactLoop)
  admitBoundaryLoop vertices = do
    let area = signedDoubleArea vertices
    if area == 0
      then Left (HexPlanarBoundaryDegenerate vertices)
      else
        fmap
          (\loop -> (area, loop))
          (first HexPlanarRegionInvalid (exactLoop (fmap exactVertexPoint vertices)))

-- One oriented edge with selected area on its left.
data BoundaryEdge = BoundaryEdge !HexVertex !HexVertex
  deriving stock (Eq, Ord, Show)

regionBoundaryEdges :: HexRegion -> Set BoundaryEdge
regionBoundaryEdges region =
  foldHexRegionCoords addCell Set.empty region
 where
  addCell :: Set BoundaryEdge -> HexCoord -> Set BoundaryEdge
  addCell edges coordinate =
    foldl' (addDirection coordinate) edges allHexDirections

  addDirection
    :: HexCoord
    -> Set BoundaryEdge
    -> HexDirection
    -> Set BoundaryEdge
  addDirection coordinate edges direction =
    case hexStepCoord coordinate direction of
      Just neighbour | hexRegionMember neighbour region -> edges
      _ ->
        let side = hexCellBoundarySide coordinate direction
         in Set.insert
              (BoundaryEdge (hexBoundaryFrom side) (hexBoundaryTo side))
              edges

traceBoundaryLoops :: Set BoundaryEdge -> Either HexPlanarObstruction [NonEmpty HexVertex]
traceBoundaryLoops edges = descend edges []
 where
  outgoing =
    Set.foldl'
      (\index edge@(BoundaryEdge from _) -> Map.insertWith (<>) from [edge] index)
      Map.empty
      edges

  descend
    :: Set BoundaryEdge
    -> [NonEmpty HexVertex]
    -> Either HexPlanarObstruction [NonEmpty HexVertex]
  descend remaining reversedLoops =
    case Set.lookupMin remaining of
      Nothing -> Right (reverse reversedLoops)
      Just firstEdge@(BoundaryEdge firstVertex _) -> do
        (vertices, unconsumed) <- traceCycle outgoing firstVertex firstEdge (Set.delete firstEdge remaining)
        descend unconsumed (vertices : reversedLoops)

traceCycle
  :: Map HexVertex [BoundaryEdge]
  -> HexVertex
  -> BoundaryEdge
  -> Set BoundaryEdge
  -> Either HexPlanarObstruction (NonEmpty HexVertex, Set BoundaryEdge)
traceCycle outgoing firstVertex firstEdge remaining =
  walk firstEdge (firstVertex :| []) remaining
 where
  walk
    :: BoundaryEdge
    -> NonEmpty HexVertex
    -> Set BoundaryEdge
    -> Either HexPlanarObstruction (NonEmpty HexVertex, Set BoundaryEdge)
  walk current reversedVertices unconsumed =
    let BoundaryEdge _ endpoint = current
     in if endpoint == firstVertex
          then Right (NonEmpty.reverse reversedVertices, unconsumed)
          else do
            next <- selectNextBoundaryEdge outgoing unconsumed current endpoint
            walk next (endpoint NonEmpty.<| reversedVertices) (Set.delete next unconsumed)

selectNextBoundaryEdge
  :: Map HexVertex [BoundaryEdge]
  -> Set BoundaryEdge
  -> BoundaryEdge
  -> HexVertex
  -> Either HexPlanarObstruction BoundaryEdge
selectNextBoundaryEdge outgoing remaining current endpoint =
  case NonEmpty.nonEmpty (filter (`Set.member` remaining) (Map.findWithDefault [] endpoint outgoing)) of
    Nothing -> Left (HexPlanarBoundaryOpen endpoint)
    Just candidates -> do
      currentRank <- boundaryEdgeRank current
      ranked <- traverse (rankCandidate currentRank) candidates
      pure (snd (leastRanked ranked))
 where
  rankCandidate
    :: Int
    -> BoundaryEdge
    -> Either HexPlanarObstruction (Int, BoundaryEdge)
  rankCandidate currentRank candidate = do
    candidateRank <- boundaryEdgeRank candidate
    pure ((candidateRank - currentRank) `mod` 6, candidate)

  leastRanked :: NonEmpty (Int, BoundaryEdge) -> (Int, BoundaryEdge)
  leastRanked (firstRanked :| rest) = foldl' choose firstRanked rest

  choose :: (Int, BoundaryEdge) -> (Int, BoundaryEdge) -> (Int, BoundaryEdge)
  choose left right = if fst left <= fst right then left else right

boundaryEdgeRank :: BoundaryEdge -> Either HexPlanarObstruction Int
boundaryEdgeRank (BoundaryEdge from to) =
  let (fromX, fromY) = hexVertexCoordinates from
      (toX, toY) = hexVertexCoordinates to
   in case (toX - fromX, toY - fromY) of
        (2, 0) -> Right 0
        (1, 1) -> Right 1
        (-1, 1) -> Right 2
        (-2, 0) -> Right 3
        (-1, -1) -> Right 4
        (1, -1) -> Right 5
        _ -> Left (HexPlanarBoundaryVectorUnexpected from to)

publishClassifiedLoops :: [(Integer, ExactLoop)] -> Either HexPlanarObstruction PlanarRegion
publishClassifiedLoops classified = do
  let outerLoops = [(area, loop) | (area, loop) <- classified, area > 0]
      holeLoops = [loop | (area, loop) <- classified, area < 0]
  holesByOuter <- foldlM (assignHole outerLoops) Map.empty holeLoops
  components <-
    traverse
      (\(_, outer) ->
          first HexPlanarRegionInvalid
            (polygonComponent outer (Map.findWithDefault [] outer holesByOuter))
      )
      outerLoops
  first HexPlanarRegionInvalid (planarRegion components)

assignHole
  :: [(Integer, ExactLoop)]
  -> Map ExactLoop [ExactLoop]
  -> ExactLoop
  -> Either HexPlanarObstruction (Map ExactLoop [ExactLoop])
assignHole outerLoops assignments hole =
  case filter (\(_, outer) -> isRight (polygonComponent outer [hole])) outerLoops of
    [] -> Left (HexPlanarHoleUnowned hole)
    firstOwner : remainingOwners ->
      let (_, owner) = foldl' smallerOuter firstOwner remainingOwners
       in Right (Map.insertWith (<>) owner [hole] assignments)
 where
  smallerOuter :: (Integer, ExactLoop) -> (Integer, ExactLoop) -> (Integer, ExactLoop)
  smallerOuter left right = if fst left <= fst right then left else right

signedDoubleArea :: NonEmpty HexVertex -> Integer
signedDoubleArea vertices@(firstVertex :| rest) =
  sum
    [ leftX * rightY - leftY * rightX
    | (left, right) <- zip (NonEmpty.toList vertices) (rest <> [firstVertex])
    , let (leftX, leftY) = hexVertexCoordinates left
    , let (rightX, rightY) = hexVertexCoordinates right
    ]

exactVertexPoint :: HexVertex -> ExactPoint
exactVertexPoint vertex =
  let (x, y) = hexVertexCoordinates vertex
   in exactPoint (fromInteger x) (fromInteger y)