packages feed

moonlight-planar-1.2.0.0: src-dcel/Moonlight/Planar/Curve/Region.hs

{-# LANGUAGE DerivingStrategies #-}

-- | Simple outer/hole assembly of closed curves, certified. A region is
-- lowered only after its curves are proved simple, pairwise disjoint and
-- wound as their roles say, and its polygon is admitted only when the
-- lowered loops nest as the curves do. The certificate reads exact
-- predicates on control points alone, never the lowering's metric.
--
-- The domain is four predicates within a finite subdivision budget. D1:
-- every piece strictly advances along some direction. D2: at every joint,
-- with stationary steps contracted, the incoming piece lies ahead of the
-- joint and the outgoing piece behind it along one direction. D3: every
-- other pair of pieces, in one contour or two, has disjoint control hulls or
-- a certified absence of crossing. Under D1 to D3 the straight-line homotopy
-- of every piece to its chord is an isotopy of the whole configuration that
-- fixes the piece endpoints: each contour is simple, the contours are
-- pairwise disjoint, a contour's winding is its chord polygon's, and a point
-- outside every piece's control hull lies inside a contour exactly when it
-- lies inside that contour's chord polygon. D4: the admitted polygon's loops
-- are pairwise boundary-disjoint and each lies inside another exactly when
-- its curve does.
--
-- Outside the domain it refuses: with the certified contact or crossing
-- when one is found, and otherwise with the unresolved obligation and the
-- budget it spent. Contact is certified only at a source step's own
-- endpoint, where the point is exact and on both curves; any other tangency
-- is never certified either way.
module Moonlight.Planar.Curve.Region
  ( CurveComponent (..)
  , ContourRole (..)
  , ContourRef (..)
  , ContourSpan (..)
  , TopologyObstruction (..)
  , CurveRegionError (..)
  , SubdivisionBudget
  , SubdivisionBudgetError (..)
  , subdivisionBudget
  , BudgetObligation (..)
  , CrossingCertificate
  , CurveTopologyEvidence
  , certifiedPieceCounts
  , certifiedPointLocation
  , certifySimpleRegion
  , lowerSimpleRegion
  ) where

import Control.DeepSeq (NFData (..))
import Control.Monad (join, unless, zipWithM)
import Data.Bifunctor (first)
import Data.Foldable (toList, traverse_)
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 Data.Maybe (isJust, isNothing, listToMaybe)
import Moonlight.Planar.Curve
  ( ClosedTrail, CurveShapeView (..), CurveStep, Located, Subpath (..), curveStepEnd, curveStepShape
  , shapeView )
import Moonlight.Planar.Curve.Lowering
  ( LoweringPolicy, LoweringError, LoweredPath, lowerClosedTrail, loweredPoints )
import Moonlight.Planar.Exact
  ( ExactBounds, ExactPoint, UnitInterval, exactOrient2d, exactPointsBounds, pointInBounds
  , translateExactPoint )
import Moonlight.Planar.Internal.CurveBudget
  ( BudgetObligation (..), SubdivisionBudget, SubdivisionBudgetError (..), budgetDepth
  , budgetLeaves, subdivisionBudget )
import Moonlight.Planar.Internal.CurveCertificate
  ( CrossingCertificate, CrossingVerdict (..), crossingVerdict, hullSeparation, jointWitness
  , monotoneWitness, separatingAxis, stationaryPiece )
import Moonlight.Planar.Internal.CurveSource
  ( SourceSpan, admitSpan, halveSpan, sourceSpanControls, sourceSpanFrom, sourceSpanPiece, sourceSpanStart
  , sourceSpanStep, sourceSpanTo, sourceStepIndex, sourceSteps, wholeSpan )
import Moonlight.Planar.Internal.Region.Bounds (overlappingPairs)
import Moonlight.Planar.Internal.Region.Loop
  ( PreparedLoop, crossLoopRelations, cycleWinding, firstPreparedPoint, pointLocationInCycle, prepareLoop
  , preparedBounds, preparedPointLocation )
import Moonlight.Planar.Region
  ( ExactLoop, PlanarRegion, RegionPointLocation (..), RegionValidationError, exactLoop
  , polygonComponent, planarRegion )

-- | The submitted winding is significant: outer CCW, holes CW. An explicitly
-- located closed trail does not by itself claim simplicity or containment.
data CurveComponent = CurveComponent
  { curveOuter :: !(Located ClosedTrail)
  , curveHoles :: ![Located ClosedTrail]
  }

data ContourRole = OuterContour | HoleContour !Int
  deriving stock (Eq, Ord, Show)

-- | A contour by its component's submitted ordinal and its role there.
data ContourRef = ContourRef !Int !ContourRole
  deriving stock (Eq, Ord, Show)

-- | The part of a contour a refusal is about: a source step's index among
-- the closed trail's steps, closing step last, and a bracket in that step's
-- parameter.
data ContourSpan = ContourSpan !ContourRef !Int !UnitInterval !UnitInterval
  deriving stock (Eq, Show)

-- | Why a region's curves are outside the certified domain.
data TopologyObstruction
  = DegenerateContour !ContourRef
    -- ^ Every step of the contour is stationary.
  | SourceSpanRefused !ContourSpan !BudgetObligation
    -- ^ The source steps themselves exceed the budget.
  | UnresolvedMonotonicity !ContourSpan !BudgetObligation
  | UnseparatedJoin !ContourSpan !ContourSpan !BudgetObligation
    -- ^ The incoming and outgoing spans at a joint.
  | CertifiedContact !ContourSpan !ContourSpan !ExactPoint
    -- ^ Two non-adjacent source steps meet at an exact point on both: an
    -- endpoint of each, or an endpoint of one on the other, a line.
  | UnresolvedContact !ContourSpan !ContourSpan !BudgetObligation
  | CertifiedSelfCrossing !ContourSpan !ContourSpan !CrossingCertificate
  | CertifiedContourCrossing !ContourSpan !ContourSpan !CrossingCertificate
  | ContourWindingRefused !ContourRef !Ordering
    -- ^ A certified simple contour whose winding is not its role's. Under D1
    -- to D3 the isotopy to the chords keeps the contour simple, so its chord
    -- polygon is simple and carries the curve's winding.
  | PolygonTopologyDiffers !ContourRef !ContourRef
    -- ^ The two lowered loops touch, or the first lies inside the second,
    -- where the curves do not, or the reverse.
  deriving stock (Eq, Show)

data CurveRegionError
  = CurveTopologyRefused !TopologyObstruction
  | CurveLoweringRefused !LoweringError
  | CurvePolygonRefused !RegionValidationError
  deriving stock (Eq, Show)

-- | What certification proved about a region's curves, observed only through
-- the questions it answers exactly.
newtype CurveTopologyEvidence = CurveTopologyEvidence [CertifiedContour]

instance NFData CurveTopologyEvidence where
  rnf (CurveTopologyEvidence contours) = rnf contours

-- | A certified contour's chord cycle, anchor first, and its pieces' control
-- hulls.
data CertifiedContour = CertifiedContour !ContourRef !(NonEmpty ExactPoint) ![PieceHull]

instance NFData CertifiedContour where
  rnf (CertifiedContour ref chord hulls) = ref `seq` rnf chord `seq` rnf hulls

data PieceHull = PieceHull !ExactBounds !(NonEmpty ExactPoint)

instance NFData PieceHull where
  rnf (PieceHull bounds hull) = bounds `seq` rnf hull

-- | Each contour's certified piece count, in submitted order.
certifiedPieceCounts :: CurveTopologyEvidence -> [(ContourRef, Int)]
certifiedPieceCounts (CurveTopologyEvidence contours) =
  [(ref, length hulls) | CertifiedContour ref _ hulls <- contours]

-- | Where a point lies in the curves' region, when it lies strictly outside
-- every piece's control hull; the region is each component's outer contour
-- less its holes. Nearer the curves than that, it is not decided.
certifiedPointLocation :: CurveTopologyEvidence -> ExactPoint -> Maybe RegionPointLocation
certifiedPointLocation (CurveTopologyEvidence contours) point
  | any touchesHull [hull | CertifiedContour _ _ hulls <- contours, hull <- hulls] = Nothing
  | any outerWithoutHole inside = Just RegionInterior
  | otherwise = Just RegionExterior
 where
  touchesHull (PieceHull bounds hull) =
    pointInBounds point bounds && isNothing (separatingAxis (point :| []) hull)
  inside = [ref | CertifiedContour ref chord _ <- contours, pointLocationInCycle chord point == RegionInterior]
  outerWithoutHole (ContourRef component role) =
    role == OuterContour && not (any (\(ContourRef other otherRole) -> other == component && otherRole /= OuterContour) inside)

-- | Certify the region's curves: D1 to D3 and each contour's winding. The
-- budget bounds the whole region: depth below any source step, pieces over
-- all contours, and the bits of every piece, located controls included, and
-- of every crossing certificate, each admitted where it is made.
certifySimpleRegion
  :: SubdivisionBudget -> [CurveComponent] -> Either TopologyObstruction CurveTopologyEvidence
certifySimpleRegion budget components = do
  initial <- traverse (uncurry (initialContour budget)) (contourTrails components)
  case drop (budgetLeaves budget) [contourSpan ref sourceSpan | Contour ref pieces <- initial, Piece _ sourceSpan <- toList pieces] of
    extra : _ -> Left (SourceSpanRefused extra LeavesExhausted)
    [] -> Right ()
  traverse_ endpointContact (overlappingPairs placedBounds (concat (zipWith placeContour [0 ..] initial)))
  certified <- refineUntilCertified budget initial
  CurveTopologyEvidence <$> traverse certifiedContour certified

-- | Certify, lower each contour under the policy, admit the polygon region,
-- and check D4 on its loops. Receipts remain in submitted component, outer,
-- hole order, independently of polygon canonicalization. Coordinates remain
-- source-local; the policy's affine map only measures error, and its leaf
-- budget applies per contour.
lowerSimpleRegion
  :: LoweringPolicy
  -> SubdivisionBudget
  -> [CurveComponent]
  -> Either CurveRegionError (PlanarRegion, [LoweredPath], CurveTopologyEvidence)
lowerSimpleRegion policy budget components = do
  evidence@(CurveTopologyEvidence contours) <- first CurveTopologyRefused (certifySimpleRegion budget components)
  admitted <- traverse lowerComponent components
  region <- first CurvePolygonRefused (planarRegion [component | (component, _, _) <- admitted])
  loopsAgree contours (concat [loops | (_, _, loops) <- admitted])
  pure (region, concat [paths | (_, paths, _) <- admitted], evidence)
 where
  lowerComponent (CurveComponent outer holes) = do
    outerPath <- first CurveLoweringRefused (lowerClosedTrail policy outer)
    holePaths <- traverse (first CurveLoweringRefused . lowerClosedTrail policy) holes
    outerLoop <- first CurvePolygonRefused (exactLoop (loweredPoints outerPath))
    holeLoops <- traverse (first CurvePolygonRefused . exactLoop . loweredPoints) holePaths
    component <- first CurvePolygonRefused (polygonComponent outerLoop holeLoops)
    pure (component, outerPath : holePaths, outerLoop : holeLoops)

-- D4. Within a component the admitted polygon already has disjoint
-- boundaries, holes inside the outer loop and no hole inside another, so
-- boundary contact is asked only across components; nesting is compared for
-- every ordered pair. With boundaries disjoint, a loop's first point decides
-- whether it lies inside another.
loopsAgree :: [CertifiedContour] -> [ExactLoop] -> Either CurveRegionError ()
loopsAgree contours loops = do
  prepared <- first CurvePolygonRefused (traverse prepareLoop loops)
  let paired = zip contours prepared
  traverse_ disjointBoundaries
    (filter acrossComponents (overlappingPairs (preparedBounds . snd) paired))
  traverse_ sameNesting [(a, b) | a <- paired, b <- paired, certifiedRef (fst a) /= certifiedRef (fst b)]
 where
  acrossComponents :: ((CertifiedContour, PreparedLoop), (CertifiedContour, PreparedLoop)) -> Bool
  acrossComponents ((a, _), (b, _)) = certifiedComponent a /= certifiedComponent b
  disjointBoundaries ((a, loopA), (b, loopB)) = do
    relations <- first CurvePolygonRefused (crossLoopRelations loopA loopB)
    unless (null relations) (differs a b)
  sameNesting ((a, loopA), (b, loopB)) =
    unless
      (curveInside a b == (preparedPointLocation loopB (firstPreparedPoint loopA) == RegionInterior))
      (differs a b)
  differs :: CertifiedContour -> CertifiedContour -> Either CurveRegionError ()
  differs a b = Left (CurveTopologyRefused (PolygonTopologyDiffers (certifiedRef a) (certifiedRef b)))

-- | Whether the first certified contour lies inside the second. Its anchor is
-- a piece endpoint of a contour disjoint from the second's pieces throughout
-- the isotopy, so the second's chord polygon decides, never on its boundary.
curveInside :: CertifiedContour -> CertifiedContour -> Bool
curveInside (CertifiedContour _ (anchor :| _) _) (CertifiedContour _ chord _) =
  pointLocationInCycle chord anchor == RegionInterior

certifiedRef :: CertifiedContour -> ContourRef
certifiedRef (CertifiedContour ref _ _) = ref

certifiedComponent :: CertifiedContour -> Int
certifiedComponent (CertifiedContour (ContourRef component _) _ _) = component

-- A piece of a contour: its depth below its source step and its span.
data Piece = Piece !Int !SourceSpan

-- A contour's pieces in trail order, stationary steps contracted.
data Contour = Contour !ContourRef !(NonEmpty Piece)

-- A piece placed for one round: contour ordinal, position, the contour's
-- piece count, the contour, and the piece.
data Placed = Placed !Int !Int !Int !ContourRef !Piece

-- The refusal a demanded piece reports if the budget stops its halving.
type Refusal = BudgetObligation -> TopologyObstruction

contourTrails :: [CurveComponent] -> [(ContourRef, Located ClosedTrail)]
contourTrails components = concat (zipWith componentContours [0 ..] components)
 where
  componentContours component (CurveComponent outer holes) =
    (ContourRef component OuterContour, outer)
      : zipWith (\hole trail -> (ContourRef component (HoleContour hole), trail)) [0 ..] holes

initialContour
  :: SubdivisionBudget -> ContourRef -> Located ClosedTrail -> Either TopologyObstruction Contour
initialContour budget ref trail = do
  spans <- traverse admit (filter moving (map wholeSpan (toList (sourceSteps (ClosedSubpath trail)))))
  case spans of
    [] -> Left (DegenerateContour ref)
    span0 : rest -> Right (Contour ref (Piece 0 <$> span0 :| rest))
 where
  moving = not . stationaryPiece . sourceSpanPiece
  admit sourceSpan = sourceSpan <$ admitBits budget (SourceSpanRefused (contourSpan ref sourceSpan)) sourceSpan

refineUntilCertified :: SubdivisionBudget -> [Contour] -> Either TopologyObstruction [Contour]
refineUntilCertified budget contours = do
  demands <- roundDemands budget contours
  case Map.minView demands of
    Nothing -> Right contours
    Just (firstRefusal, _) -> do
      refined <- zipWithM (refineContour budget demands) [0 ..] contours
      unless (sum [length pieces | Contour _ pieces <- refined] <= budgetLeaves budget)
        (Left (firstRefusal LeavesExhausted))
      refineUntilCertified budget refined

-- Every piece this round cannot certify, keyed by contour ordinal and
-- position, with the first refusal it met: monotonicity, then joints, then
-- contacts. A certified crossing refuses at once.
roundDemands :: SubdivisionBudget -> [Contour] -> Either TopologyObstruction (Map (Int, Int) Refusal)
roundDemands budget contours = do
  contacts <- concat <$> traverse (contactDemands budget) (overlappingPairs placedBounds (concat placed))
  pure (Map.fromListWith (\_ earlier -> earlier) (monotonicity <> joints <> contacts))
 where
  placed = zipWith placeContour [0 ..] contours
  monotonicity =
    [ (placedKey piece, UnresolvedMonotonicity (placedSpan piece))
    | piece <- concat placed, isNothing (monotoneWitness (placedStep piece)) ]
  joints =
    [ demand
    | pieces <- placed
    , (incoming, outgoing) <- zip pieces (drop 1 pieces <> take 1 pieces)
    , isNothing (jointWitness (placedStep incoming) (placedStep outgoing))
    , let refusal = UnseparatedJoin (placedSpan incoming) (placedSpan outgoing)
    , demand <- [(placedKey incoming, refusal), (placedKey outgoing, refusal)] ]

placeContour :: Int -> Contour -> [Placed]
placeContour ordinal (Contour ref pieces) =
  zipWith (\position piece -> Placed ordinal position count ref piece) [0 ..] (toList pieces)
 where
  count = length pieces

contactDemands :: SubdivisionBudget -> (Placed, Placed) -> Either TopologyObstruction [((Int, Int), Refusal)]
contactDemands budget (a, b)
  | adjacent a b = Right []
  | isJust (hullSeparation startA stepA startB stepB) = Right []
  | otherwise = case crossingVerdict budget startA stepA startB stepB of
      Left obligation -> Left (refusal obligation)
      Right (Just (SingleCrossing certificate)) -> Left (crossing (placedSpan a) (placedSpan b) certificate)
      Right (Just (NoCrossing _)) -> Right []
      Right Nothing -> Right [(placedKey a, refusal), (placedKey b, refusal)]
 where
  startA = placedStart a
  stepA = placedStep a
  startB = placedStart b
  stepB = placedStep b
  refusal = UnresolvedContact (placedSpan a) (placedSpan b)
  crossing
    | placedContour a == placedContour b = CertifiedSelfCrossing
    | otherwise = CertifiedContourCrossing

-- Contact at the source steps' own endpoints, before any refinement: two
-- non-adjacent steps sharing an endpoint, or an endpoint of one on the other
-- when that is a line segment. Endpoints made by halving lie on the other
-- curve only by coincidence, so they are left to the refinement.
endpointContact :: (Placed, Placed) -> Either TopologyObstruction ()
endpointContact (a, b)
  | adjacent a b = Right ()
  | otherwise = maybe (Right ()) (Left . CertifiedContact (placedSpan a) (placedSpan b)) contact
 where
  contact = listToMaybe (filter (`onStep` b) (bothEnds a) <> filter (`onStep` a) (bothEnds b))
  bothEnds :: Placed -> [ExactPoint]
  bothEnds placed = let (start, end) = placedEnds placed in [start, end]

placedEnds :: Placed -> (ExactPoint, ExactPoint)
placedEnds placed = (start, translateExactPoint start (curveStepEnd (placedStep placed)))
 where
  start = placedStart placed

onStep :: ExactPoint -> Placed -> Bool
onStep point placed =
  point == start || point == end
    || (straight && exactOrient2d start end point == EQ && pointInBounds point (exactPointsBounds (start :| [end])))
 where
  (start, end) = placedEnds placed
  straight = case shapeView (curveStepShape (placedStep placed)) of
    LinearView -> True
    _ -> False

adjacent :: Placed -> Placed -> Bool
adjacent a@(Placed contourA positionA _ _ _) b@(Placed contourB positionB _ _ _) =
  contourA == contourB && (positionB == next a || positionA == next b)
 where
  next (Placed _ position count _ _) = (position + 1) `mod` count

refineContour
  :: SubdivisionBudget -> Map (Int, Int) Refusal -> Int -> Contour -> Either TopologyObstruction Contour
refineContour budget demands ordinal (Contour ref pieces) =
  Contour ref . join <$> traverse refine (NonEmpty.zip (0 :| [1 ..]) pieces)
 where
  refine (position, piece@(Piece depth sourceSpan)) = case Map.lookup (ordinal, position) demands of
    Nothing -> Right (piece :| [])
    Just refusal
      | depth >= budgetDepth budget -> Left (refusal DepthExhausted)
      | otherwise -> do
          let (left, right) = halveSpan sourceSpan
          traverse_ (admitBits budget refusal) [left, right]
          Right (Piece (depth + 1) left :| [Piece (depth + 1) right])

admitBits :: SubdivisionBudget -> Refusal -> SourceSpan -> Either TopologyObstruction ()
admitBits budget refusal sourceSpan = either (Left . refusal) (const (Right ())) (admitSpan budget sourceSpan)

certifiedContour :: Contour -> Either TopologyObstruction CertifiedContour
certifiedContour (Contour ref pieces)
  | winding == expected = Right (CertifiedContour ref chord (map pieceHull (toList pieces)))
  | otherwise = Left (ContourWindingRefused ref winding)
 where
  chord = (\(Piece _ sourceSpan) -> sourceSpanStart sourceSpan) <$> pieces
  winding = cycleWinding chord
  expected = case ref of
    ContourRef _ OuterContour -> GT
    ContourRef _ (HoleContour _) -> LT
  pieceHull (Piece _ sourceSpan) =
    let hull = sourceSpanControls sourceSpan in PieceHull (exactPointsBounds hull) hull

contourSpan :: ContourRef -> SourceSpan -> ContourSpan
contourSpan ref sourceSpan =
  ContourSpan ref (sourceStepIndex (sourceSpanStep sourceSpan)) (sourceSpanFrom sourceSpan) (sourceSpanTo sourceSpan)

placedKey :: Placed -> (Int, Int)
placedKey (Placed contour position _ _ _) = (contour, position)

placedContour :: Placed -> Int
placedContour (Placed contour _ _ _ _) = contour

placedSpan :: Placed -> ContourSpan
placedSpan (Placed _ _ _ ref (Piece _ sourceSpan)) = contourSpan ref sourceSpan

placedStart :: Placed -> ExactPoint
placedStart (Placed _ _ _ _ (Piece _ sourceSpan)) = sourceSpanStart sourceSpan

placedStep :: Placed -> CurveStep
placedStep (Placed _ _ _ _ (Piece _ sourceSpan)) = sourceSpanPiece sourceSpan

placedBounds :: Placed -> ExactBounds
placedBounds (Placed _ _ _ _ (Piece _ sourceSpan)) = exactPointsBounds (sourceSpanControls sourceSpan)