packages feed

moonlight-triangulation-1.3.0.0: src-dcel/Moonlight/Triangulation/Internal/BoundaryCycle.hs

-- | Shared pure algebra for descending oriented boundary graphs into simple
-- cycles, then simplifying and classifying those cycles.
module Moonlight.Triangulation.Internal.BoundaryCycle
  ( traceOrientedBoundaryCircuits
  , simplifyBoundaryCycle
  , rotateCycleLeast
  , rotateCycleLeastBy
  , consecutivePairs
  , unorderedPairs
  , orderedPair
  , cyclePairs
  , cyclePairsNonEmpty
  , cyclicTriples
  ) where

import Data.List (tails)
import qualified Data.List as List
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map.Strict as Map
import Data.Map.Strict (Map)
import qualified Data.Set as Set
import Data.Set (Set)

-- | Consume every supplied oriented edge exactly once and split point contacts
-- into vertex-simple cycles. The outgoing lists carry the caller's required
-- local angular or identifier order. A malformed graph is translated directly
-- through the caller's obstruction constructor; this shared worker owns no
-- disposable error vocabulary.
traceOrientedBoundaryCircuits
  :: (Ord vertex, Ord edge)
  => (edge -> vertex)
  -> (edge -> vertex)
  -> (edge -> edge -> obstruction)
  -> Map vertex [edge]
  -> Set edge
  -> Either obstruction [NonEmpty vertex]
traceOrientedBoundaryCircuits edgeOrigin edgeDestination obstruction outgoing edges =
  descend edges []
 where
  descend remaining cycles =
    case Set.lookupMin remaining of
      Nothing -> Right (reverse cycles)
      Just seed ->
        let (untraced, circuitEdges) =
              eulerCircuit remaining (edgeOrigin seed)
         in case NonEmpty.nonEmpty circuitEdges of
              Nothing -> Left (obstruction seed seed)
              Just circuit ->
                case splitCircuit circuit of
                  Left failedEdge -> Left (obstruction seed failedEdge)
                  Right splitCycles ->
                    descend
                      untraced
                      (reverse (NonEmpty.toList splitCycles) <> cycles)

  -- Hierholzer descent: delete each chosen edge, then prepend it while
  -- backtracking. No graph-state stream or branch search is materialized.
  eulerCircuit remaining start = walk remaining start [] []
   where
    walk untraced current incomingEdges circuit =
      case
          List.find
            (`Set.member` untraced)
            (Map.findWithDefault [] current outgoing) of
        Just edge ->
          walk
            (Set.delete edge untraced)
            (edgeDestination edge)
            (edge : incomingEdges)
            circuit
        Nothing ->
          case incomingEdges of
            edge : previousEdges ->
              walk
                untraced
                (edgeOrigin edge)
                previousEdges
                (edge : circuit)
            [] -> (untraced, circuit)

  -- The resident path stays simple. Closing at a resident vertex emits and
  -- removes exactly that suffix, partitioning the Euler circuit into cycles.
  splitCircuit circuit =
    walk
      start
      0
      (Map.singleton start 0)
      []
      []
      (NonEmpty.toList circuit)
   where
    start = edgeOrigin (NonEmpty.head circuit)

    walk current depth depths reversedEdges cycles remaining =
      case remaining of
        [] ->
          case
              ( depth
              , reversedEdges
              , NonEmpty.nonEmpty (reverse cycles)
              ) of
            (0, [], Just simpleCycles) -> Right simpleCycles
            _ -> Left (NonEmpty.last circuit)
        edge : rest
          | edgeOrigin edge /= current -> Left edge
          | otherwise ->
              let target = edgeDestination edge
               in case Map.lookup target depths of
                    Nothing ->
                      walk
                        target
                        (depth + 1)
                        (Map.insert target (depth + 1) depths)
                        (edge : reversedEdges)
                        cycles
                        rest
                    Just repeatedDepth ->
                      let suffixLength = depth - repeatedDepth
                          suffixEdges = take suffixLength reversedEdges
                          simpleCycle =
                            fmap edgeOrigin
                              ( NonEmpty.reverse
                                  (edge :| suffixEdges)
                              )
                       in walk
                            target
                            repeatedDepth
                            ( foldr
                                (Map.delete . edgeDestination)
                                depths
                                suffixEdges
                            )
                            (drop suffixLength reversedEdges)
                            (simpleCycle : cycles)
                            rest
{-# INLINABLE traceOrientedBoundaryCircuits #-}

-- | Remove precisely the vertices admitted by @isRedundant@ until a fixed
-- point is reached, then classify the winding at the least keyed retained
-- vertex. The returned cycle preserves the tracer's start; publication layers
-- may rotate their value-level observation independently. The caller supplies
-- its obstruction constructor so the shared worker does not allocate a
-- disposable intermediate error vocabulary at either specialization.
simplifyBoundaryCycle
  :: (Eq value, Ord key)
  => ([value] -> obstruction)
  -> (value -> value -> value -> Bool)
  -> (value -> value -> value -> Ordering)
  -> (value -> key)
  -> [value]
  -> Either obstruction (Ordering, NonEmpty value)
simplifyBoundaryCycle obstruction isRedundant orientation key = descend
 where
  descend values@(_ : _ : _ : _) =
    let triples = cyclicTriples values
        retained =
          [ current
          | (previousValue, current, nextValue) <- triples
          , not (isRedundant previousValue current nextValue)
          ]
     in if retained == values
          then classify values triples
          else descend retained
  descend values = Left (obstruction values)

  classify values triples =
    case triples of
      [] -> Left (obstruction values)
      firstTriple : remainingTriples ->
        let (previousValue, current, nextValue) =
              List.foldl' chooseLeast firstTriple remainingTriples
            winding = orientation previousValue current nextValue
         in case (winding, values) of
              (EQ, _) -> Left (obstruction values)
              (_, initialValue : rest) -> Right (winding, initialValue :| rest)
              _ -> Left (obstruction values)

  chooseLeast selected@(_, selectedValue, _) candidate@(_, candidateValue, _)
    | key candidateValue < key selectedValue = candidate
    | otherwise = selected
{-# INLINE simplifyBoundaryCycle #-}

-- | Choose the least value as a cycle's observational origin without changing
-- its orientation. Boundary publication and generated convex geometry share
-- this one canonical rotation owner.
rotateCycleLeast :: Ord value => NonEmpty value -> NonEmpty value
rotateCycleLeast = rotateCycleLeastBy id
{-# INLINE rotateCycleLeast #-}

-- | Choose the least keyed value as a cycle's observational origin.
rotateCycleLeastBy
  :: Ord key
  => (value -> key)
  -> NonEmpty value
  -> NonEmpty value
rotateCycleLeastBy key values =
  case break ((== minimumKey) . key) asList of
    (before, selected : after) -> selected :| (after <> before)
    _ -> values
 where
  asList = NonEmpty.toList values
  minimumKey =
    List.foldl'
      (\selected candidate -> min selected (key candidate))
      (key (NonEmpty.head values))
      (NonEmpty.tail values)
{-# INLINE rotateCycleLeastBy #-}

-- | Every adjacent pair in a linear sequence.
consecutivePairs :: [value] -> [(value, value)]
consecutivePairs values = zip values (drop 1 values)
{-# INLINE consecutivePairs #-}

-- | Every unordered pair exactly once.
unorderedPairs :: [value] -> [(value, value)]
unorderedPairs values =
  [(left, right) | left : remaining <- tails values, right <- remaining]
{-# INLINE unorderedPairs #-}

-- | Canonically orient an unordered pair.
orderedPair :: Ord value => value -> value -> (value, value)
orderedPair left right
  | left <= right = (left, right)
  | otherwise = (right, left)
{-# INLINE orderedPair #-}

-- | Every directed edge of a non-empty cycle in cycle order.
cyclePairs :: NonEmpty value -> [(value, value)]
cyclePairs = NonEmpty.toList . cyclePairsNonEmpty
{-# INLINE cyclePairs #-}

-- | The non-empty form of 'cyclePairs'. A singleton cycle has its sole value
-- as both ends of its sole cyclic edge.
cyclePairsNonEmpty :: NonEmpty value -> NonEmpty (value, value)
cyclePairsNonEmpty values@(firstValue :| remaining) =
  NonEmpty.zip values successors
 where
  successors =
    case remaining of
      [] -> firstValue :| []
      nextValue : rest -> nextValue :| (rest <> [firstValue])
{-# INLINE cyclePairsNonEmpty #-}

-- | Consecutive cyclic triples, one centered at every value.
cyclicTriples :: [value] -> [(value, value, value)]
cyclicTriples values =
  case values of
    initial : second : remaining ->
      let final = List.foldl' (\_ current -> current) initial (second : remaining)
       in zip3
            (final : values)
            values
            (second : remaining <> [initial])
    _ -> []