packages feed

moonlight-planar-1.1.0.0: test/native/Moonlight/Planar/OverlaySelectionSpec.hs

-- | Closed selection, exterior marginals, and lower-dimensional incidence.
module Moonlight.Planar.OverlaySelectionSpec (tests) where

import Data.List.NonEmpty ( NonEmpty(..) )
import Moonlight.Planar.CellSet ( ExactCellSet, exactCellSetEdgeCount, exactCellSetFaceCount,
  exactCellSetVertexCount )
import Moonlight.Planar.Exact ( ExactPoint )
import Moonlight.Planar.Internal.Incidence ( faceBoundaryComponents )
import Moonlight.Planar.Internal.Overlay.Types ( overlayResultIncidence )
import Moonlight.Planar.Overlay ( BoundaryRef(boundaryRefSource), OverlayResult,
  OverlaySelectionError(OverlaySelectionContainsUnboundedCell),
  OverlaySelectionKind(RegularizedDifferenceSelection, ClosedUnionSelection,
  ClosedIntersectionSelection), OverlaySourceId(OverlaySourceId), OverlayVertex(overlayExactPoint,
  overlayVertexOrigin), OverlayVertexOrigin(..), overlayArrangementVertices, overlayCells,
  overlayClosedIntersection, overlayClosedUnion, overlayLayers, overlayRegularizedDifference,
  overlaySelectedRegion )
import Moonlight.Planar.OverlayAssertions ( assertOverlayIntegrity, unboundedLoopCount )
import Moonlight.Planar.OverlayFixtures ( singletonLayer )
import Moonlight.Planar.Region ( exactLoop, polygonComponent, planarLayer, planarRegion,
  planarRegionComponents )
import Moonlight.Planar.Valuation ( cellValuations, eulerCharacteristicValue, exactAreaValue,
  valuationArea, valuationEuler )
import Support ( assertEqual, integerPoint, requireRight, rectangleRegion, annulusRegion )
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set

tests :: IO ()
tests =
  sequence_
    [ testLowerDimensionalIntersections
    , testSelectorRefusalsAndOperandSwap
    , testExteriorMarginalClosures
    , testNestedVertexContactClosure
    ]

testLowerDimensionalIntersections :: IO ()
testLowerDimensionalIntersections = do
  left <- rectangleRegion 0 0 1 1 >>= singletonLayer "left-outside" "left"
  pointTouching <- rectangleRegion 1 1 2 2 >>= singletonLayer "right-outside" "right"
  pointResult <- requireRight "point-touching overlay" (overlayLayers left pointTouching)
  assertOverlayIntegrity "point-touching" pointResult
  assertEqual
    "point-touching outside is one boundary walk with a repeated contact vertex"
    (Just 1)
    (unboundedLoopCount pointResult)
  pointIntersection <-
    requireRight
      "point-only closed intersection"
      (overlayClosedIntersection (== "left") (== "right") pointResult)
  assertEqual
    "point-only closed intersection retains only its zero-cell"
    (1, 0, 0)
    ( exactCellSetVertexCount pointIntersection
    , exactCellSetEdgeCount pointIntersection
    , exactCellSetFaceCount pointIntersection
    )

  edgeTouching <- rectangleRegion 1 0 2 1 >>= singletonLayer "right-outside" "right"
  edgeResult <- requireRight "edge-touching overlay" (overlayLayers left edgeTouching)
  assertOverlayIntegrity "edge-touching" edgeResult
  edgeIntersection <-
    requireRight
      "edge-only closed intersection"
      (overlayClosedIntersection (== "left") (== "right") edgeResult)
  assertEqual
    "edge-only closed intersection retains its closure"
    (2, 1, 0)
    ( exactCellSetVertexCount edgeIntersection
    , exactCellSetEdgeCount edgeIntersection
    , exactCellSetFaceCount edgeIntersection
    )
  publishedEdgeOnly <-
    requireRight
      "edge-only polygon publication"
      (overlaySelectedRegion (== ("left", "right")) edgeResult)
  assertEqual
    "edge-only selection is not fabricated into a polygon"
    0
    (length (planarRegionComponents publishedEdgeOnly))

testSelectorRefusalsAndOperandSwap :: IO ()
testSelectorRefusalsAndOperandSwap = do
  left <- rectangleRegion 0 0 2 2 >>= singletonLayer "left-outside" "left"
  right <- rectangleRegion 1 (-1) 3 1 >>= singletonLayer "right-outside" "right"
  result <- requireRight "selector refusal overlay" (overlayLayers left right)
  assertSelectionRefusal
    "closed union refuses selected outside cell"
    (OverlaySelectionContainsUnboundedCell ClosedUnionSelection)
    (overlayClosedUnion (== "left-outside") (const False) result)
  assertSelectionRefusal
    "closed intersection refuses selected outside cell"
    (OverlaySelectionContainsUnboundedCell ClosedIntersectionSelection)
    ( overlayClosedIntersection
        (== "left-outside")
        (== "right-outside")
        result
    )
  assertSelectionRefusal
    "regularized difference refuses selected outside cell"
    (OverlaySelectionContainsUnboundedCell RegularizedDifferenceSelection)
    ( overlayRegularizedDifference
        (== "left-outside")
        (== "right")
        result
    )

  swapped <- requireRight "operand-swapped overlay" (overlayLayers right left)
  assertOverlayIntegrity "operand-swapped" swapped
  assertEqual
    "operand swap preserves exact arrangement vertices"
    (Set.fromList (map (overlayExactPoint . snd) (overlayArrangementVertices result)))
    (Set.fromList (map (overlayExactPoint . snd) (overlayArrangementVertices swapped)))
  assertEqual
    "operand swap preserves cells and exchanges labels"
    [ (face, (rightLabel, leftLabel))
    | (face, (leftLabel, rightLabel)) <- overlayCells result
    ]
    (overlayCells swapped)
  assertEqual
    "operand swap exchanges typed vertex provenance"
    (vertexOriginCensus True result)
    (vertexOriginCensus False swapped)

-- A selected exterior marginal need not select the unbounded joint face.
-- Its bounded intersection can include an entire lower-dimensional component
-- that is not in the downward closure of any selected bounded face.
testExteriorMarginalClosures :: IO ()
testExteriorMarginalClosures = do
  square <- rectangleRegion 0 0 4 4 >>= singletonLayer "outside" "inside"
  sameSquare <- requireRight "same-support exterior intersection" (overlayLayers square square)
  boundary <- requireRight "outside closure intersected with inside closure"
    (overlayClosedIntersection (== "outside") (== "inside") sameSquare)
  assertEqual "exterior/interior marginal intersection retains the closed boundary"
    (4, 4, 0)
    (exactCellSetVertexCount boundary, exactCellSetEdgeCount boundary, exactCellSetFaceCount boundary)
  boundaryValues <- requireRight "exterior/interior boundary valuation" (cellValuations boundary)
  assertEqual "closed square boundary has Euler zero and no area"
    (0, 0)
    (eulerCharacteristicValue (valuationEuler boundaryValues), exactAreaValue (valuationArea boundaryValues))

  region <- annulusRegion (0, 0, 4, 4) (1, 1, 3, 3)
  annulus <- requireRight "exterior annulus layer" (planarLayer "outside" (Map.singleton "annulus" region))
  result <- requireRight "annulus exterior against enclosing square" (overlayLayers annulus square)
  selected <- requireRight "bounded exterior disk plus detached outer boundary"
    (overlayClosedIntersection (== "outside") (== "inside") result)
  assertEqual "exterior intersection keeps a face and an independent edge cycle"
    (8, 8, 1)
    (exactCellSetVertexCount selected, exactCellSetEdgeCount selected, exactCellSetFaceCount selected)
  values <- requireRight "exterior disk and boundary valuation" (cellValuations selected)
  assertEqual "disjoint disk plus edge cycle has Euler one and exact cavity area"
    (1, 4)
    (eulerCharacteristicValue (valuationEuler values), exactAreaValue (valuationArea values))

-- Tangency joins the outer and inner boundary walks at a single vertex.
-- The selected open face is cellular, but its closure is not a disk: the
-- repeated contact vertex must survive incidence rather than split the walk.
testNestedVertexContactClosure :: IO ()
testNestedVertexContactClosure = do
  square <- rectangleRegion 0 0 4 4 >>= singletonLayer "outside" "inside"
  diamondLoop <- requireRight "internally tangent diamond loop"
    (exactLoop (integerPoint 0 2 :| [integerPoint 1 1, integerPoint 2 2, integerPoint 1 3]))
  diamondComponent <- requireRight "internally tangent diamond component" (polygonComponent diamondLoop [])
  diamondRegion <- requireRight "internally tangent diamond region" (planarRegion [diamondComponent])
  diamond <- requireRight "internally tangent diamond layer"
    (planarLayer "outside" (Map.singleton "inside" diamondRegion))
  result <- requireRight "nested single-vertex contact overlay" (overlayLayers square diamond)
  assertOverlayIntegrity "nested single-vertex contact" result
  assertEqual "tangent complement retains one raw boundary orbit"
    [1]
    [length (faceBoundaryComponents (overlayResultIncidence result) face)
    | (face, labels) <- overlayCells result, labels == ("inside", "outside")]
  complement <- requireRight "tangent complement exact closure"
    (overlayRegularizedDifference (== "inside") (== "inside") result)
  values <- requireRight "tangent complement exact valuation" (cellValuations complement)
  assertEqual "contact does not fill the diamond hole in the closure"
    (0, 14)
    (eulerCharacteristicValue (valuationEuler values), exactAreaValue (valuationArea values))

assertSelectionRefusal
  :: String
  -> OverlaySelectionError
  -> Either OverlaySelectionError ExactCellSet
  -> IO ()
assertSelectionRefusal label expected actual =
  case actual of
    Left obstruction -> assertEqual label expected obstruction
    Right _ -> fail (label <> ": unbounded selection was truncated")

vertexOriginCensus
  :: Bool
  -> OverlayResult (String, String)
  -> Map.Map ExactPoint (Int, Int, Int, Int)
vertexOriginCensus preserveSides result =
  Map.fromList
    [ ( overlayExactPoint vertex
      , if preserveSides
          then census origin
          else swapCensus (census origin)
      )
    | (_, vertex) <- overlayArrangementVertices result
    , let origin = overlayVertexOrigin vertex
    ]
 where
  census :: OverlayVertexOrigin -> (Int, Int, Int, Int)
  census origin =
    ( length (filter ((== OverlaySourceId 0) . boundaryRefSource) (overlayOriginVertices origin))
    , length (filter ((== OverlaySourceId 1) . boundaryRefSource) (overlayOriginVertices origin))
    , length (filter ((== OverlaySourceId 0) . boundaryRefSource) (overlayOriginEdges origin))
    , length (filter ((== OverlaySourceId 1) . boundaryRefSource) (overlayOriginEdges origin))
    )
  swapCensus :: (Int, Int, Int, Int) -> (Int, Int, Int, Int)
  swapCensus (leftVertices, rightVertices, leftEdges, rightEdges) =
    (rightVertices, leftVertices, rightEdges, leftEdges)