packages feed

moonlight-triangulation 1.3.0.3 → 1.4.0.1

raw patch · 43 files changed

+12743/−4865 lines, 43 filesdep +textdep ~basedep ~containersdep ~moonlight-triangulationnew-component:exe:moonlight-triangulation-persistence-rosebinary-addedPVP ok

version bump matches the API change (PVP)

Dependencies added: text

Dependency ranges changed: base, containers, moonlight-triangulation, transformers

API changes (from Hackage documentation)

- Moonlight.Triangulation: ConstrainedSeamLeftSource :: ConstrainedSeamSource
- Moonlight.Triangulation: ConstrainedSeamRightSource :: ConstrainedSeamSource
- Moonlight.Triangulation: ConstraintUnionSourceConstraintNotPreserved :: ConstrainedSeamSource -> CanonicalSegment -> ConstrainedUnionError
- Moonlight.Triangulation: ConstraintUnionSourceFaceNotPreserved :: ConstrainedSeamSource -> FaceId -> ConstrainedUnionError
- Moonlight.Triangulation: ConstraintUnionSourceFaceNotTriangular :: ConstrainedSeamSource -> FaceId -> Int -> ConstrainedUnionError
- Moonlight.Triangulation: ConstraintUnionTargetFaceAmbiguous :: FaceId -> FaceId -> ConstrainedUnionError
- Moonlight.Triangulation: ConstraintUnionTargetFaceNotTriangular :: FaceId -> Int -> ConstrainedUnionError
- Moonlight.Triangulation: constrainedSeamLeftConstraintEvidence :: ConstrainedSeamResult vertex -> Vector ConstrainedSeamConstraintEvidence
- Moonlight.Triangulation: constrainedSeamLeftFaceEvidence :: ConstrainedSeamResult vertex -> Vector ConstrainedSeamFaceEvidence
- Moonlight.Triangulation: constrainedSeamNewFaces :: ConstrainedSeamResult vertex -> Vector FaceId
- Moonlight.Triangulation: data ConstrainedSeamSource
+ Moonlight.Triangulation: BoundarySplitRequiresBoundaryEdge :: UndirectedEdgeId -> BuildError
+ Moonlight.Triangulation: ConstraintUnionRequiresGeometryOnlyPublication :: ConstrainedUnionError
+ Moonlight.Triangulation: PublicationStats :: Int -> Int -> Int -> Int -> Int -> Int -> Int -> Int -> Int -> Int -> Int -> Int -> PublicationStats
+ Moonlight.Triangulation: RefinementDomainRequiresFiniteVertexBudget :: BuildError
+ Moonlight.Triangulation: RefinementMaximumEdgeLengthNotFinite :: NonFiniteValue -> BuildError
+ Moonlight.Triangulation: RefinementMaximumEdgeLengthNotPositive :: Double -> BuildError
+ Moonlight.Triangulation: RefinementOversizedEdge :: FaceId -> UndirectedEdgeId -> Double -> Double -> BuildError
+ Moonlight.Triangulation: RefinementSeamBridgeBudgetExceeded :: Int -> Int -> BuildError
+ Moonlight.Triangulation: RefinementSeamBridgeMidpointCollapsed :: UndirectedEdgeId -> BuildError
+ Moonlight.Triangulation: SeamFrontierUnavailable :: BuildError
+ Moonlight.Triangulation: SeamIncoming :: ConstrainedSeamSide
+ Moonlight.Triangulation: SeamProtectedSourceFaceChanged :: FaceId -> BuildError
+ Moonlight.Triangulation: SeamResident :: ConstrainedSeamSide
+ Moonlight.Triangulation: SeamSourceEdgeRequiresFlip :: UndirectedEdgeId -> BuildError
+ Moonlight.Triangulation: ValidationClosureStats :: Int -> Int -> Int -> Int -> Int -> ValidationClosureStats
+ Moonlight.Triangulation: [publicationBoxedBasePageEnumerations] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationBoxedBasePageFreezes] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationBoxedBasePageOpens] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationBoxedCopiedCells] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationBoxedDirtyAppendedPages] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationBoxedDirtyBasePages] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationUnboxedBasePageEnumerations] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationUnboxedBasePageFreezes] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationUnboxedBasePageOpens] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationUnboxedCopiedCells] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationUnboxedDirtyAppendedPages] :: PublicationStats -> Int
+ Moonlight.Triangulation: [publicationUnboxedDirtyBasePages] :: PublicationStats -> Int
+ Moonlight.Triangulation: [refineMaxEdgeLength] :: RefinementParameters -> Maybe Double
+ Moonlight.Triangulation: [validationClosureConstraintPairs] :: ValidationClosureStats -> Int
+ Moonlight.Triangulation: [validationClosureDirectedEdges] :: ValidationClosureStats -> Int
+ Moonlight.Triangulation: [validationClosureFaces] :: ValidationClosureStats -> Int
+ Moonlight.Triangulation: [validationClosureInterfacePairs] :: ValidationClosureStats -> Int
+ Moonlight.Triangulation: [validationClosureVertices] :: ValidationClosureStats -> Int
+ Moonlight.Triangulation: boundaryLoopResidentVertices :: BoundaryLoop -> NonEmpty VertexId
+ Moonlight.Triangulation: boundedRegionFaces :: Triangulation 'Constrained vertex directed undirected face -> [FaceId]
+ Moonlight.Triangulation: componentBoundaryForFaces :: forall (mode :: ConstraintMode) vertex directed undirected face. Triangulation mode vertex directed undirected face -> Set FaceId -> Either BoundaryObstruction RegionBoundary
+ Moonlight.Triangulation: constrainedSeamCachedFrontierPointReads :: ConstrainedSeamResult vertex -> Int
+ Moonlight.Triangulation: constrainedSeamJoinFaces :: ConstrainedSeamResult vertex -> Vector FaceId
+ Moonlight.Triangulation: constrainedSeamLeftConstraintCount :: ConstrainedSeamResult vertex -> Int
+ Moonlight.Triangulation: constrainedSeamLeftFaceCount :: ConstrainedSeamResult vertex -> Int
+ Moonlight.Triangulation: constrainedSeamPublicationStats :: ConstrainedSeamResult vertex -> PublicationStats
+ Moonlight.Triangulation: data ConstrainedSeamSide
+ Moonlight.Triangulation: data PublicationStats
+ Moonlight.Triangulation: data ValidationClosureStats
+ Moonlight.Triangulation: floodFillFaces :: forall (mode :: ConstraintMode) vertex directed undirected face. Triangulation mode vertex directed undirected face -> [FaceId] -> (UndirectedEdgeId -> Bool) -> [FaceId]
+ Moonlight.Triangulation: geometryOnlyPublication :: forall (mode :: ConstraintMode) vertex directed undirected face. Triangulation mode vertex directed undirected face -> Triangulation mode () () () ()
+ Moonlight.Triangulation: refinementFinalPermittedFaces :: RefinementReceipt -> Vector FaceId
+ Moonlight.Triangulation: refinementPublicationStats :: RefinementReceipt -> PublicationStats
+ Moonlight.Triangulation: refinementValidationClosureStats :: RefinementReceipt -> ValidationClosureStats
- Moonlight.Triangulation: RefinementParameters :: Maybe Int -> Maybe Double -> Maybe Double -> Maybe Double -> Bool -> Bool -> Bool -> RefinementParameters
+ Moonlight.Triangulation: RefinementParameters :: Maybe Int -> Maybe Double -> Maybe Double -> Maybe Double -> Maybe Double -> Bool -> Bool -> Bool -> RefinementParameters
- Moonlight.Triangulation: joinSeparatedConstrained :: Triangulation 'Constrained annotation () () () -> Triangulation 'Constrained annotation () () () -> Either ConstrainedUnionError (ConstrainedSeamResult annotation)
+ Moonlight.Triangulation: joinSeparatedConstrained :: (ConstrainedSeamSide -> FaceId -> Bool) -> RefinementParameters -> Triangulation 'Constrained annotation () () () -> Triangulation 'Constrained annotation () () () -> Either ConstrainedUnionError (ConstrainedSeamResult annotation)
- Moonlight.Triangulation: refineWithinDomain :: forall vertex (mode :: ConstraintMode) directed undirected face. (Point -> vertex) -> RefinementParameters -> Set FaceId -> Set UndirectedEdgeId -> Triangulation mode vertex directed undirected face -> Either BuildError (RefinementDomainResult mode vertex directed undirected face)
+ Moonlight.Triangulation: refineWithinDomain :: forall (mode :: ConstraintMode) vertex directed undirected face. (Point -> vertex) -> RefinementParameters -> Set FaceId -> Set UndirectedEdgeId -> Triangulation mode vertex directed undirected face -> Either BuildError (RefinementDomainResult mode vertex directed undirected face)

Files

CHANGELOG.md view
@@ -6,6 +6,29 @@ The serialization format carries its own version tag, independent of the package version; any change to it is recorded here explicitly. +## 1.4.0.1 - 2026-08-24++* Break `joinSeparatedConstrained` onto its stronger local-extension contract:+  callers now supply a source-face preservation section and+  `RefinementParameters`, and receive seam provenance and publication+  receipts from the one constrained seam owner.+* Extend `RefinementParameters` with an optional maximum edge length and add+  typed refusals for non-finite, non-positive, interface-crossing, protected+  face, bridge-budget, and final oversized-edge obstructions. Local refinement+  now publishes its final permitted-face witness and validation/publication+  receipts.+* Add source-preserving paged publication receipts, cached seam-frontier+  observations, bounded-region face selection, and exact validation-closure+  statistics without introducing a second mesh carrier.+* Preserve each boundary loop's exact resident DCEL vertex chain through+  `boundaryLoopResidentVertices`; `boundaryLoopVertices` remains the derived+  collinearity-reduced polygon observation.+* Ship the persistence-rose source observatory and replace the decorative alpha+  eclipse with the canonical four-site square interval computed by+  `alphaFiltration` and Moonlight Homology: its one `H1` class is born at exact+  squared radius 1 and dies at 2. The retained SVGs and GIF now have a checked+  regeneration/provenance command, and CI rejects stale exhibit artifacts.+ ## 1.3.0.3 - 2026-08-22  * Restore the package-overview Delaunay board on Hackage with a release-tagged
README.md view
@@ -16,6 +16,57 @@ refinement, walk point location, convex hull, exact Shewchuk predicates, incremental insertion and removal, and versioned binary serialization. +## Persistence rose++![Animated Moonlight persistence rose: exact Delaunay alpha filtration, persistent homology, Voronoi dual, and natural-neighbour field](./docs/social/moonlight-triangulation-persistence-rose.gif)++[Open the animation directly.](./docs/social/moonlight-triangulation-persistence-rose.gif)+· [Open the static vector poster.](./docs/moonlight-triangulation-persistence-rose.svg)+· [Browse the exhibit files.](./docs/README.md)++This is computed, not ornament. A deterministic 169-site cover becomes one+Delaunay mesh, one exact 626-value alpha filtration, and one Homology reduction.+The poster state has `β₁ = 7`; seven bounded components recovered+independently by face descent and boundary gluing agree with that persistent+Betti number. The colour field is 447 fallback-free Sibson natural-neighbour+queries, the fine cyan geometry is the finite Voronoi dual, and the flower+silhouette is seven exact Minkowski-offset petals glued by labelled planar+overlay.++On GHC 9.14, the package-owned generator reproduces the poster, the exact alpha+eclipse, or the canonical 65-frame seamless SVG sweep:++```console+cabal run moonlight-triangulation-persistence-rose -- poster persistence-rose.svg+cabal run moonlight-triangulation-persistence-rose -- eclipse alpha-eclipse.svg+cabal run moonlight-triangulation-persistence-rose -- frames 65 persistence-rose-frames+```++The retained GIF is the 960×540, 8 fps projection of those 65 source frames.+Its exact encoding boundary is deliberately outside geometry and Homology:+rasterize the SVG section, encode it, then let the exhibit owner stamp the GIF+with the deterministic source-frame fingerprint it computed itself.++```console+mkdir -p persistence-rose-png+sips -s format png persistence-rose-frames/frame-*.svg --out persistence-rose-png+ffmpeg -framerate 8 -start_number 0 -i persistence-rose-png/frame-%04d.png \+  -vf "scale=960:-2:flags=lanczos,palettegen=max_colors=96:stats_mode=diff" \+  -y persistence-rose-palette.png+ffmpeg -framerate 8 -start_number 0 -i persistence-rose-png/frame-%04d.png \+  -i persistence-rose-palette.png \+  -lavfi "[0:v]scale=960:-2:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" \+  -loop 0 -y persistence-rose-raw.gif+cabal run moonlight-triangulation-persistence-rose -- \+  stamp-gif persistence-rose-raw.gif docs/social/moonlight-triangulation-persistence-rose.gif+cabal run moonlight-triangulation-persistence-rose -- verify-artifacts .+```++`verify-artifacts` compares both retained SVGs byte-for-byte with their pure+renderers, checks the GIF dimensions, timing, and image-frame inventory, and requires+the GIF's embedded source fingerprint to equal the freshly rendered canonical+frame section. A plausible binary can no longer wander in wearing a nice hat.+ ## Operations  | Operation | Role | What it returns |@@ -61,7 +112,7 @@ | Insert a vector of sites | `BulkLoad.insertMany` | Do not fold singleton insertion; one batch thaws and publishes once. | | Compose insertions and removals | one `Session.withSession` | Do not publish every intermediate mesh. Use coordinate-keyed removal after the first compaction. | | Refine the whole mesh | `refine` | Do not manufacture a domain witness merely to reach the local API. |-| Refine a proved face section without changing protected faces | `refineWithinDomain` | Supply its exact permitted faces and interface edges; a guessed boundary is a typed refusal, not a hint. |+| Refine a proved face section without changing protected faces | `refineWithinDomain` | Supply its exact permitted faces; the immutable interface is derived once by the domain owner and returned as receipt evidence. | | Partition bounded faces by any caller-owned label | `faceComponents mesh labelFace` | Do not rebuild adjacency outside the DCEL or collapse disconnected regions sharing a label. | | Recover one component's polygonal boundary | `componentBoundary mesh component` | Do not emit one polygon per triangle; outer and hole loops have canonical winding and exact collinear simplification. | | Combine two labelled polygon layers | `overlayLayers left right` | Do not run separate clipping engines for union, intersection, and difference; select cells from the one common refinement. |@@ -92,7 +143,7 @@ ```cabal -- the consumer package build-depends:-  moonlight-triangulation:cell-complex >= 1.3 && < 1.4,+  moonlight-triangulation:cell-complex >= 1.4 && < 1.5,   moonlight-homology:cell-category >= 0.1 && < 0.2 ``` @@ -110,6 +161,18 @@  ### Exact alpha filtration and persistent topology +![Alpha eclipse: a fixed Delaunay site set closes one hole and later fills it across exact critical radii](./docs/moonlight-triangulation-alpha-eclipse.svg)++[Open the full-size alpha eclipse.](./docs/moonlight-triangulation-alpha-eclipse.svg)++The eclipse uses the package's canonical four-site square witness and isolates+its one finite `H₁` interval. The four sites, five resident Delaunay edges, and+two resident faces remain fixed: the four hull edges close one hole at exact+`α² = 1`; the resident diagonal and both resident faces enter at exact+`α² = 2`, making that cycle a boundary. Only the alpha sublevel changes. Every+visible cell is projected from the resident DCEL, and the `β₁ = 0 → 1 → 0`+profile comes from Moonlight Homology rather than a hand-authored diagram.+ The full alpha path is a different observation of the same resident Delaunay DCEL. `alphaFiltration` computes exact squared-radius births once: vertices at zero, Gabriel edges at one quarter of their squared length, other edges at their@@ -133,9 +196,9 @@  ```cabal build-depends:-  moonlight-triangulation:build >= 1.3 && < 1.4,-  moonlight-triangulation:dcel >= 1.3 && < 1.4,-  moonlight-triangulation:cell-complex >= 1.3 && < 1.4,+  moonlight-triangulation:build >= 1.4 && < 1.5,+  moonlight-triangulation:dcel >= 1.4 && < 1.5,+  moonlight-triangulation:cell-complex >= 1.4 && < 1.5,   moonlight-homology >= 0.1 && < 0.2 ``` @@ -249,7 +312,7 @@ all 24 fixture summaries across all five implementations before timing, and derives both pictures from the retained 120-case CSV. -![Moonlight Delaunay construction over the small Spade fixtures](https://raw.githubusercontent.com/PaleRoses/moonlight/moonlight-triangulation-1.3.0.3/moonlight-triangulation/bench/delaunay-compare/results/moonlight-delaunay-compare-small.svg)+![Moonlight Delaunay construction over the small Spade fixtures](https://raw.githubusercontent.com/PaleRoses/moonlight/moonlight-triangulation-1.4.0.1/moonlight-triangulation/bench/delaunay-compare/results/moonlight-delaunay-compare-small.svg)  The [benchmark README](bench/delaunay-compare/README.md) owns the single retained board, its result, receipt, big-point picture, and reproduction
+ app/persistence-rose/Main.hs view
@@ -0,0 +1,211 @@+module Main (main) where++import Control.Exception (IOException, try)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Except+  ( ExceptT (..),+    except,+    runExceptT,+  )+import Data.Bifunctor (first)+import Data.ByteString (ByteString)+import Data.ByteString qualified as ByteString+import Data.Foldable (traverse_)+import Moonlight.Triangulation.Exhibit.PersistenceRose+  ( PersistenceRoseError,+    PersistenceRoseScene,+    alphaEclipseReceipt,+    buildAlphaEclipseScene,+    buildPersistenceRoseScene,+    persistenceRoseFrameSpecs,+    persistenceRoseReceipt,+    renderAlphaEclipse,+    renderAlphaEclipseReceipt,+    renderPersistenceRoseAnimationReceipt,+    renderPersistenceRoseFrame,+    renderPersistenceRosePoster,+    renderPersistenceRoseReceipt,+    stampPersistenceRoseGif,+    verifyPersistenceRoseGif,+  )+import System.Directory (createDirectoryIfMissing)+import System.Environment (getArgs)+import System.Exit (die)+import System.FilePath ((</>), takeDirectory)+import Text.Printf (printf)+import Text.Read (readMaybe)++data PersistenceRoseCommand+  = WritePoster !FilePath+  | WriteEclipse !FilePath+  | WriteFrames !Int !FilePath+  | StampGif !FilePath !FilePath+  | VerifyArtifacts !FilePath+  | PrintReceipt++data PersistenceRoseCommandError+  = PersistenceRoseUsage ![String]+  | PersistenceRoseInvalidFrameCount !String+  | PersistenceRoseConstructionFailed !PersistenceRoseError+  | PersistenceRoseDirectoryFailed !FilePath !IOException+  | PersistenceRoseReadFailed !FilePath !IOException+  | PersistenceRoseWriteFailed !FilePath !IOException+  | PersistenceRoseArtifactMismatch !FilePath++instance Show PersistenceRoseCommandError where+  show commandError =+    case commandError of+      PersistenceRoseUsage arguments ->+        "invalid persistence-rose command "+          <> show arguments+          <> "; expected: poster OUTPUT.svg | eclipse OUTPUT.svg | frames FRAME_COUNT OUTPUT_DIRECTORY | stamp-gif INPUT.gif OUTPUT.gif | verify-artifacts PACKAGE_ROOT | receipt"+      PersistenceRoseInvalidFrameCount submitted ->+        "frame count must be an integer greater than one, received " <> show submitted+      PersistenceRoseConstructionFailed sceneError -> show sceneError+      PersistenceRoseDirectoryFailed path caughtError ->+        "could not create output directory " <> show path <> ": " <> show caughtError+      PersistenceRoseReadFailed path caughtError ->+        "could not read " <> show path <> ": " <> show caughtError+      PersistenceRoseWriteFailed path caughtError ->+        "could not write " <> show path <> ": " <> show caughtError+      PersistenceRoseArtifactMismatch path ->+        "tracked exhibit differs from its canonical renderer: " <> show path++main :: IO ()+main = do+  arguments <- getArgs+  result <- runExceptT (runPersistenceRoseCommand arguments)+  either (die . show) pure result++runPersistenceRoseCommand :: [String] -> ExceptT PersistenceRoseCommandError IO ()+runPersistenceRoseCommand arguments = do+  command <- except (parseCommand arguments)+  case command of+    WriteEclipse outputPath -> do+      scene <- except (first PersistenceRoseConstructionFailed buildAlphaEclipseScene)+      createOutputDirectory (takeDirectory outputPath)+      writeOutput outputPath (renderAlphaEclipse scene)+      lift+        ( putStrLn+            (renderAlphaEclipseReceipt (alphaEclipseReceipt scene) <> "\nwrote alpha eclipse " <> outputPath)+        )+    StampGif inputPath outputPath -> do+      scene <- requirePersistenceRoseScene+      inputBytes <- readBytes inputPath+      (receipt, stamped) <- except (first PersistenceRoseConstructionFailed (stampPersistenceRoseGif scene inputBytes))+      createOutputDirectory (takeDirectory outputPath)+      writeBytes outputPath stamped+      lift+        ( putStrLn+            (renderPersistenceRoseAnimationReceipt receipt <> "\nwrote stamped GIF " <> outputPath)+        )+    VerifyArtifacts packageRoot -> verifyTrackedArtifacts packageRoot+    PrintReceipt -> do+      scene <- requirePersistenceRoseScene+      lift (putStrLn (renderPersistenceRoseReceipt (persistenceRoseReceipt scene)))+    WritePoster outputPath -> do+      scene <- requirePersistenceRoseScene+      let receiptText = renderPersistenceRoseReceipt (persistenceRoseReceipt scene)+      createOutputDirectory (takeDirectory outputPath)+      writeOutput outputPath (renderPersistenceRosePoster scene)+      lift (putStrLn (receiptText <> "\nwrote poster " <> outputPath))+    WriteFrames frameCount outputDirectory -> do+      scene <- requirePersistenceRoseScene+      let receiptText = renderPersistenceRoseReceipt (persistenceRoseReceipt scene)+      frames <- except (first PersistenceRoseConstructionFailed (persistenceRoseFrameSpecs frameCount))+      createOutputDirectory outputDirectory+      traverse_+        (\(frameIndex, frame) -> writeOutput (framePath outputDirectory frameIndex) (renderPersistenceRoseFrame scene frame))+        (zip [0 :: Int ..] frames)+      lift+        ( putStrLn+            ( receiptText+                <> "\nwrote "+                <> show (length frames)+                <> " frames to "+                <> outputDirectory+            )+        )++requirePersistenceRoseScene :: ExceptT PersistenceRoseCommandError IO PersistenceRoseScene+requirePersistenceRoseScene =+  except (first PersistenceRoseConstructionFailed buildPersistenceRoseScene)++parseCommand :: [String] -> Either PersistenceRoseCommandError PersistenceRoseCommand+parseCommand arguments =+  case arguments of+    ["poster", outputPath] -> Right (WritePoster outputPath)+    ["eclipse", outputPath] -> Right (WriteEclipse outputPath)+    ["stamp-gif", inputPath, outputPath] -> Right (StampGif inputPath outputPath)+    ["verify-artifacts", packageRoot] -> Right (VerifyArtifacts packageRoot)+    ["frames", submittedCount, outputDirectory] ->+      case readMaybe submittedCount of+        Just frameCount+          | frameCount > 1 -> Right (WriteFrames frameCount outputDirectory)+        _ -> Left (PersistenceRoseInvalidFrameCount submittedCount)+    ["receipt"] -> Right PrintReceipt+    _ -> Left (PersistenceRoseUsage arguments)++createOutputDirectory :: FilePath -> ExceptT PersistenceRoseCommandError IO ()+createOutputDirectory outputDirectory =+  liftIOException+    (PersistenceRoseDirectoryFailed outputDirectory)+    (createDirectoryIfMissing True outputDirectory)++writeOutput :: FilePath -> String -> ExceptT PersistenceRoseCommandError IO ()+writeOutput outputPath contents =+  liftIOException (PersistenceRoseWriteFailed outputPath) (writeFile outputPath contents)++readOutput :: FilePath -> ExceptT PersistenceRoseCommandError IO String+readOutput inputPath =+  liftIOException (PersistenceRoseReadFailed inputPath) (readFile inputPath)++readBytes :: FilePath -> ExceptT PersistenceRoseCommandError IO ByteString+readBytes inputPath =+  liftIOException (PersistenceRoseReadFailed inputPath) (ByteString.readFile inputPath)++writeBytes :: FilePath -> ByteString -> ExceptT PersistenceRoseCommandError IO ()+writeBytes outputPath contents =+  liftIOException (PersistenceRoseWriteFailed outputPath) (ByteString.writeFile outputPath contents)++verifyTrackedArtifacts :: FilePath -> ExceptT PersistenceRoseCommandError IO ()+verifyTrackedArtifacts packageRoot = do+  roseScene <- requirePersistenceRoseScene+  eclipseScene <- except (first PersistenceRoseConstructionFailed buildAlphaEclipseScene)+  let posterPath = packageRoot </> "docs" </> "moonlight-triangulation-persistence-rose.svg"+      eclipsePath = packageRoot </> "docs" </> "moonlight-triangulation-alpha-eclipse.svg"+      gifPath = packageRoot </> "docs" </> "social" </> "moonlight-triangulation-persistence-rose.gif"+  verifyTextArtifact posterPath (renderPersistenceRosePoster roseScene)+  verifyTextArtifact eclipsePath (renderAlphaEclipse eclipseScene)+  gifBytes <- readBytes gifPath+  animationReceipt <-+    except+      (first PersistenceRoseConstructionFailed (verifyPersistenceRoseGif roseScene gifBytes))+  lift+    ( putStrLn+        ( "verified poster, alpha eclipse, and stamped GIF\n"+            <> renderPersistenceRoseAnimationReceipt animationReceipt+        )+    )++verifyTextArtifact+  :: FilePath+  -> String+  -> ExceptT PersistenceRoseCommandError IO ()+verifyTextArtifact artifactPath expectedContents = do+  actualContents <- readOutput artifactPath+  except+    ( if actualContents == expectedContents+        then Right ()+        else Left (PersistenceRoseArtifactMismatch artifactPath)+    )++liftIOException+  :: (IOException -> PersistenceRoseCommandError)+  -> IO value+  -> ExceptT PersistenceRoseCommandError IO value+liftIOException wrapFailure = ExceptT . fmap (first wrapFailure) . try++framePath :: FilePath -> Int -> FilePath+framePath outputDirectory frameIndex =+  outputDirectory </> printf "frame-%04d.svg" frameIndex
+ app/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs view
@@ -0,0 +1,1625 @@+{-# LANGUAGE NamedFieldPuns #-}++-- | A deterministic, pure observatory assembled from Moonlight's existing+-- geometry, exact filtration, interpolation, and persistence owners.+module Moonlight.Triangulation.Exhibit.PersistenceRose+  ( PersistenceRoseError (..),+    PersistenceRoseReceipt (..),+    PersistenceRoseScene,+    PersistenceRoseFrame,+    PersistenceRoseAnimationReceipt (..),+    AlphaEclipseReceipt (..),+    AlphaEclipseScene,+    buildPersistenceRoseScene,+    buildAlphaEclipseScene,+    persistenceRoseReceipt,+    alphaEclipseReceipt,+    renderPersistenceRoseReceipt,+    renderAlphaEclipseReceipt,+    renderPersistenceRosePoster,+    renderAlphaEclipse,+    persistenceRoseFrameSpecs,+    renderPersistenceRoseFrame,+    persistenceRoseAnimationReceipt,+    renderPersistenceRoseAnimationReceipt,+    stampPersistenceRoseGif,+    verifyPersistenceRoseGif,+  )+where++import Control.Monad (foldM)+import Control.Monad.ST (runST)+import Data.Bifunctor (first)+import Data.Bits ((.&.), xor)+import Data.ByteString (ByteString)+import Data.ByteString qualified as ByteString+import Data.ByteString.Char8 qualified as ByteString.Char8+import Data.List (find, intercalate, sortOn)+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.Maybe (mapMaybe)+import Data.Ord (Down (..))+import Data.Set qualified as Set+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text.Encoding+import Data.Vector qualified as Vector+import Data.Word (Word64, Word8)+import Moonlight.Homology.Chain+  ( HomologicalDegree (..),+    HomologyFailure,+    PersistencePair (..),+  )+import Moonlight.Homology.Persistence+  ( criticalBettiVectors,+    mod2PersistentPairsWithCriticalBettiTable,+  )+import Moonlight.Triangulation+  ( AlphaBirth,+    AlphaFiltration,+    AlphaFiltrationError,+    BuildError,+    DelaunayTriangulation,+    ExactLoop,+    MinkowskiError,+    OverlayError,+    PlanarRegion,+    Point (..),+    PointValidationError,+    PolygonComponent,+    QueryPoint,+    RegionPublicationError,+    RegionValidationError,+    alphaBirthDenominator,+    alphaBirthNumerator,+    alphaBirthToDouble,+    alphaEdgeBirth,+    alphaFaceBirth,+    alphaFiltration,+    alphaFiltrationCriticalBirths,+    convexPolygon,+    delaunayGeometry,+    exactLoop,+    exactLoopPoints,+    exactPointFromPoint,+    exactPointToEmbeddingCandidate,+    innerFaces,+    innerFaceVertexTriples,+    mkQueryPoint,+    overlayLayers,+    overlaySelectedRegion,+    planarLayer,+    planarRegion,+    planarRegionComponents,+    polygonComponent,+    polygonHoleLoops,+    polygonOffset,+    polygonOuterLoop,+    queryPointValue,+    structuringElement,+    undirectedEdges,+    undirectedEndpoints,+    vertexPoint,+    vertices,+  )+import Moonlight.Triangulation.CellComplex+  ( DCELError,+    filteredAlphaComplex,+  )+import Moonlight.Triangulation.FloodFillIterator+  ( BoundaryObstruction,+    FaceComponent,+    boundaryLoopVertices,+    componentBoundaryLoops,+    faceComponentFaces,+    faceComponentsBy,+    outerFaceFloodFill,+  )+import Moonlight.Triangulation.Handles.HandleDefs+  ( FaceId (..),+    UndirectedEdgeId,+    VertexId,+    normalizedDirected,+  )+import Moonlight.Triangulation.Interpolation+  ( InterpolationStats (..),+    NaturalNeighborResult (..),+    naturalNeighborWeights,+    newNaturalNeighborWorkspace,+  )+import Moonlight.Triangulation.Voronoi+  ( VoronoiEdgeGeometry (..),+    asDirectedVoronoiEdge,+    voronoiEdgeGeometry,+  )+import Numeric (showFFloat, showHex)++-- | Every boundary at which the exhibit may be refused. The renderer never+-- invents a fallback geometry or topology state.+data PersistenceRoseError+  = RoseDelaunayBuildFailed !BuildError+  | RoseAlphaFiltrationFailed !AlphaFiltrationError+  | RoseCellComplexLoweringFailed !DCELError+  | RosePersistenceFailed !HomologyFailure+  | RoseCriticalProfileCountMismatch !Int !Int+  | RoseCriticalProfileDegreeCountMismatch !Int !Int+  | RoseBirthRankMissing !AlphaBirth+  | RoseEdgeBirthMissing !UndirectedEdgeId+  | RoseFaceBirthMissing !FaceId+  | RoseBoundaryDescentFailed !Int !BoundaryObstruction+  | RoseNoCriticalStates+  | RoseNoMaskRegions+  | RoseNoPersistentHole+  | RoseNoCoherentHero !Int ![(Int, Int)]+  | RoseVoronoiGeometryMissing !UndirectedEdgeId+  | RoseNaturalNeighborFallbacks !Int+  | RosePointInvalid !PointValidationError+  | RoseRegionInvalid !RegionValidationError+  | RoseOverlayFailed !(OverlayError Bool Bool)+  | RoseOverlayPublicationFailed !RegionPublicationError+  | RoseMinkowskiFailed !MinkowskiError+  | RoseFrameCountTooSmall !Int+  | RoseEclipseFiniteHoleCountMismatch !Int+  | RoseEclipseFiniteHoleMissingDeath+  | RoseEclipseBettiProfileMismatch !Int !Int !Int+  | RoseGifInvalidSignature+  | RoseGifTruncated !Int+  | RoseGifUnexpectedBlock !Int !Word8+  | RoseGifDimensionMismatch !Int !Int !Int !Int+  | RoseGifFrameCountMismatch !Int !Int+  | RoseGifFrameDelayCountMismatch !Int !Int+  | RoseGifDurationMismatch !Int !Int+  | RoseGifProvenanceMismatch+  | RoseGifMarkerTooLong !Int+  deriving stock (Show)++-- | A compact, test-readable projection of everything the scene actually+-- computed. It is intentionally not another geometry representation.+data PersistenceRoseReceipt = PersistenceRoseReceipt+  { roseSiteCount :: !Int,+    roseEdgeCount :: !Int,+    roseFaceCount :: !Int,+    roseCriticalBirthCount :: !Int,+    roseFinitePersistentHoleCount :: !Int,+    roseEssentialPersistentHoleCount :: !Int,+    roseHeroCriticalRank :: !Int,+    roseHeroBettiOne :: !Int,+    roseHeroGeometricHoleCount :: !Int,+    roseVoronoiSegmentCount :: !Int,+    roseNaturalNeighborSampleCount :: !Int,+    roseNaturalNeighborFallbackCount :: !Int,+    roseMaskComponentCount :: !Int+  }+  deriving stock (Eq, Show)++data PersistenceRoseScene = PersistenceRoseScene+  { sceneSites :: ![Point],+    sceneEdges :: ![RankedSegment],+    sceneFaces :: ![RankedTriangle],+    sceneCriticalStates :: !(NonEmpty CriticalState),+    sceneHeroState :: !CriticalState,+    sceneVoronoiSegments :: ![Segment],+    sceneFieldSamples :: ![FieldSample],+    sceneMaskComponents :: ![MaskComponent],+    scenePersistenceArcs :: ![PersistenceArc],+    sceneReceipt :: !PersistenceRoseReceipt+  }++-- | The canonical source projection carried inside the retained GIF. The GIF+-- codec remains an effect-boundary view; this receipt binds that binary view+-- to the exact deterministic SVG frame section used to produce it.+data PersistenceRoseAnimationReceipt = PersistenceRoseAnimationReceipt+  { roseAnimationFrameCount :: !Int,+    roseAnimationFramesPerSecond :: !Int,+    roseAnimationWidth :: !Int,+    roseAnimationHeight :: !Int,+    roseAnimationSourceFingerprint :: !Word64+  }+  deriving stock (Eq, Show)++-- | The exact, test-readable facts shown by the alpha eclipse. The numerator+-- and denominator fields are projections of opaque 'AlphaBirth' values; the+-- renderer never chooses thresholds independently of the filtration.+data AlphaEclipseReceipt = AlphaEclipseReceipt+  { eclipseSiteCount :: !Int,+    eclipseEdgeCount :: !Int,+    eclipseFaceCount :: !Int,+    eclipseHoleBirthNumerator :: !Integer,+    eclipseHoleBirthDenominator :: !Integer,+    eclipseHoleDeathNumerator :: !Integer,+    eclipseHoleDeathDenominator :: !Integer,+    eclipseBettiBeforeBirth :: !Int,+    eclipseBettiAtBirth :: !Int,+    eclipseBettiAtDeath :: !Int+  }+  deriving stock (Eq, Show)++-- | A three-section observation of one fixed resident Delaunay DCEL. Every+-- displayed edge and face is projected from that DCEL with its exact birth+-- rank; only the selected sublevel differs between sections.+data AlphaEclipseScene = AlphaEclipseScene+  { eclipseSites :: ![Point],+    eclipseEdges :: ![RankedSegment],+    eclipseFaces :: ![RankedTriangle],+    eclipseBeforeState :: !CriticalState,+    eclipseBirthState :: !CriticalState,+    eclipseDeathState :: !CriticalState,+    eclipseReceipt :: !AlphaEclipseReceipt+  }++-- | The shared local descent used by both exhibits. This is not a second+-- filtration carrier: it retains the canonical mesh and filtration beside+-- their checked Homology projection and only the ranked rendering views.+data AlphaProjection = AlphaProjection+  { projectionTriangulation :: !(DelaunayTriangulation ()),+    projectionPersistencePairs :: ![PersistencePair AlphaBirth],+    projectionCriticalStates :: !(NonEmpty CriticalState),+    projectionBirthRanks :: !(Map AlphaBirth Int),+    projectionEdges :: ![RankedSegment],+    projectionFaces :: ![RankedTriangle]+  }++newtype PersistenceRoseFrame = PersistenceRoseFrame+  { frameProgress :: Double+  }+  deriving stock (Eq, Show)++data CriticalState = CriticalState+  { criticalRank :: !Int,+    criticalBirth :: !AlphaBirth,+    criticalBettiOne :: !Int,+    criticalHoles :: ![HoleComponent]+  }++data RankedSegment = RankedSegment+  { rankedSegmentGeometry :: !Segment,+    rankedSegmentBirthRank :: !Int+  }++data RankedTriangle = RankedTriangle+  { rankedTrianglePoints :: !(Point, Point, Point),+    rankedTriangleBirthRank :: !Int+  }++data Segment = Segment !Point !Point++newtype HoleComponent = HoleComponent [[Point]]++data FieldSample = FieldSample+  { fieldSamplePoint :: !Point,+    fieldSampleValue :: !Double,+    fieldSampleNeighborCount :: !Int+  }++data MaskComponent = MaskComponent+  { maskOuterLoop :: ![Point],+    maskHoleLoops :: ![[Point]]+  }++data PersistenceArc = PersistenceArc+  { persistenceArcBirthRank :: !Int,+    persistenceArcDeathRank :: !Int+  }++buildPersistenceRoseScene :: Either PersistenceRoseError PersistenceRoseScene+buildPersistenceRoseScene = do+  projection <- buildAlphaProjection roseSites+  let triangulation = projectionTriangulation projection+      criticalStates = projectionCriticalStates projection+      persistencePairs = projectionPersistencePairs projection+      criticalBirthCount = NonEmpty.length criticalStates+  heroState <- chooseHeroState criticalStates+  finiteVoronoiSegments <- projectFiniteVoronoiSegments triangulation+  (fieldSamples, fallbackCount) <- naturalNeighborField triangulation+  if fallbackCount == 0+    then pure ()+    else Left (RoseNaturalNeighborFallbacks fallbackCount)+  maskComponents <- buildRoseMask+  persistenceArcs <-+    projectPersistenceArcs+      (projectionBirthRanks projection)+      criticalBirthCount+      persistencePairs+  let finiteHoles = filter isFinitePositiveHole persistencePairs+      essentialHoles = filter isEssentialHole persistencePairs+      heroHoleCount = length (criticalHoles heroState)+      receipt =+        PersistenceRoseReceipt+          { roseSiteCount = length (vertices triangulation),+            roseEdgeCount = length (projectionEdges projection),+            roseFaceCount = length (projectionFaces projection),+            roseCriticalBirthCount = criticalBirthCount,+            roseFinitePersistentHoleCount = length finiteHoles,+            roseEssentialPersistentHoleCount = length essentialHoles,+            roseHeroCriticalRank = criticalRank heroState,+            roseHeroBettiOne = criticalBettiOne heroState,+            roseHeroGeometricHoleCount = heroHoleCount,+            roseVoronoiSegmentCount = length finiteVoronoiSegments,+            roseNaturalNeighborSampleCount = length fieldSamples,+            roseNaturalNeighborFallbackCount = fallbackCount,+            roseMaskComponentCount = length maskComponents+          }+  pure+    PersistenceRoseScene+      { sceneSites = fmap (vertexPoint triangulation) (vertices triangulation),+        sceneEdges = projectionEdges projection,+        sceneFaces = projectionFaces projection,+        sceneCriticalStates = criticalStates,+        sceneHeroState = heroState,+        sceneVoronoiSegments = finiteVoronoiSegments,+        sceneFieldSamples = fieldSamples,+        sceneMaskComponents = maskComponents,+        scenePersistenceArcs = persistenceArcs,+        sceneReceipt = receipt+      }++buildAlphaProjection :: [Point] -> Either PersistenceRoseError AlphaProjection+buildAlphaProjection sites = do+  triangulation <- first RoseDelaunayBuildFailed (delaunayGeometry (Vector.fromList sites))+  filtration <- first RoseAlphaFiltrationFailed (alphaFiltration triangulation)+  filteredComplex <- first RoseCellComplexLoweringFailed (filteredAlphaComplex filtration)+  (persistencePairs, criticalBettiTable) <-+    first RosePersistenceFailed+      (mod2PersistentPairsWithCriticalBettiTable filteredComplex)+  let criticalBirths = alphaFiltrationCriticalBirths filtration+      criticalProfiles = criticalBettiVectors criticalBettiTable+      birthRanks = Map.fromDistinctAscList (zip criticalBirths [0 ..])+  if length criticalBirths == length criticalProfiles+    then pure ()+    else Left (RoseCriticalProfileCountMismatch (length criticalBirths) (length criticalProfiles))+  criticalStates <-+    traverse+      (buildCriticalState triangulation filtration)+      (zip3 [0 ..] criticalBirths criticalProfiles)+      >>= maybe (Left RoseNoCriticalStates) Right . NonEmpty.nonEmpty+  rankedEdges <-+    traverse+      (projectRankedEdge triangulation filtration birthRanks)+      (undirectedEdges triangulation)+  rankedFaces <-+    traverse+      (projectRankedFace triangulation filtration birthRanks)+      (zip [1 ..] (Vector.toList (innerFaceVertexTriples triangulation)))+  pure+    AlphaProjection+      { projectionTriangulation = triangulation,+        projectionPersistencePairs = persistencePairs,+        projectionCriticalStates = criticalStates,+        projectionBirthRanks = birthRanks,+        projectionEdges = rankedEdges,+        projectionFaces = rankedFaces+      }++buildAlphaEclipseScene :: Either PersistenceRoseError AlphaEclipseScene+buildAlphaEclipseScene = do+  projection <- buildAlphaProjection alphaEclipseSites+  finiteHoles <-+    case filter isFinitePositiveHole (projectionPersistencePairs projection) of+      [finiteHole] -> Right finiteHole+      holes -> Left (RoseEclipseFiniteHoleCountMismatch (length holes))+  deathBirth <-+    maybe+      (Left RoseEclipseFiniteHoleMissingDeath)+      Right+      (persistenceDeath finiteHoles)+  let birth = persistenceBirth finiteHoles+      birthRanks = projectionBirthRanks projection+      states = projectionCriticalStates projection+  birthRank <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)+  deathRank <- maybe (Left (RoseBirthRankMissing deathBirth)) Right (Map.lookup deathBirth birthRanks)+  let beforeState = criticalStateAt (max 0 (birthRank - 1)) states+      birthState = criticalStateAt birthRank states+      deathState = criticalStateAt deathRank states+      bettiProfile =+        ( criticalBettiOne beforeState,+          criticalBettiOne birthState,+          criticalBettiOne deathState+        )+  case bettiProfile of+    (0, 1, 0) -> pure ()+    (beforeBetti, birthBetti, deathBetti) ->+      Left (RoseEclipseBettiProfileMismatch beforeBetti birthBetti deathBetti)+  let triangulation = projectionTriangulation projection+      receipt =+        AlphaEclipseReceipt+          { eclipseSiteCount = length (vertices triangulation),+            eclipseEdgeCount = length (projectionEdges projection),+            eclipseFaceCount = length (projectionFaces projection),+            eclipseHoleBirthNumerator = alphaBirthNumerator birth,+            eclipseHoleBirthDenominator = alphaBirthDenominator birth,+            eclipseHoleDeathNumerator = alphaBirthNumerator deathBirth,+            eclipseHoleDeathDenominator = alphaBirthDenominator deathBirth,+            eclipseBettiBeforeBirth = criticalBettiOne beforeState,+            eclipseBettiAtBirth = criticalBettiOne birthState,+            eclipseBettiAtDeath = criticalBettiOne deathState+          }+  pure+    AlphaEclipseScene+      { eclipseSites = fmap (vertexPoint triangulation) (vertices triangulation),+        eclipseEdges = projectionEdges projection,+        eclipseFaces = projectionFaces projection,+        eclipseBeforeState = beforeState,+        eclipseBirthState = birthState,+        eclipseDeathState = deathState,+        eclipseReceipt = receipt+      }++persistenceRoseReceipt :: PersistenceRoseScene -> PersistenceRoseReceipt+persistenceRoseReceipt = sceneReceipt++alphaEclipseReceipt :: AlphaEclipseScene -> AlphaEclipseReceipt+alphaEclipseReceipt = eclipseReceipt++renderPersistenceRoseReceipt :: PersistenceRoseReceipt -> String+renderPersistenceRoseReceipt receipt =+  intercalate+    " · "+    [ "sites " <> show (roseSiteCount receipt),+      "edges " <> show (roseEdgeCount receipt),+      "faces " <> show (roseFaceCount receipt),+      "critical births " <> show (roseCriticalBirthCount receipt),+      "finite H1 intervals " <> show (roseFinitePersistentHoleCount receipt),+      "essential H1 intervals " <> show (roseEssentialPersistentHoleCount receipt),+      "hero rank " <> show (roseHeroCriticalRank receipt),+      "beta1 " <> show (roseHeroBettiOne receipt),+      "geometric holes " <> show (roseHeroGeometricHoleCount receipt),+      "Voronoi segments " <> show (roseVoronoiSegmentCount receipt),+      "Sibson samples " <> show (roseNaturalNeighborSampleCount receipt),+      "fallbacks " <> show (roseNaturalNeighborFallbackCount receipt),+      "mask components " <> show (roseMaskComponentCount receipt)+    ]++renderAlphaEclipseReceipt :: AlphaEclipseReceipt -> String+renderAlphaEclipseReceipt receipt =+  intercalate+    " · "+    [ "sites " <> show (eclipseSiteCount receipt),+      "edges " <> show (eclipseEdgeCount receipt),+      "faces " <> show (eclipseFaceCount receipt),+      "H1 ["+        <> renderExactRatio (eclipseHoleBirthNumerator receipt) (eclipseHoleBirthDenominator receipt)+        <> ", "+        <> renderExactRatio (eclipseHoleDeathNumerator receipt) (eclipseHoleDeathDenominator receipt)+        <> ")",+      "beta1 "+        <> show+          ( eclipseBettiBeforeBirth receipt,+            eclipseBettiAtBirth receipt,+            eclipseBettiAtDeath receipt+          )+    ]++buildCriticalState+  :: DelaunayTriangulation ()+  -> AlphaFiltration+  -> (Int, AlphaBirth, [Int])+  -> Either PersistenceRoseError CriticalState+buildCriticalState triangulation filtration (rankValue, birthValue, profile) = do+  bettiOne <-+    case profile of+      _ : secondDegree : _ : _ -> Right secondDegree+      _ -> Left (RoseCriticalProfileDegreeCountMismatch rankValue (length profile))+  holes <- holeComponentsAt rankValue triangulation filtration birthValue+  pure+    CriticalState+      { criticalRank = rankValue,+        criticalBirth = birthValue,+        criticalBettiOne = bettiOne,+        criticalHoles = holes+      }++chooseHeroState :: NonEmpty CriticalState -> Either PersistenceRoseError CriticalState+chooseHeroState states =+  let stateList = NonEmpty.toList states+      maximumBettiOne = foldl' (\best state -> max best (criticalBettiOne state)) 0 stateList+      maximalStates = filter ((== maximumBettiOne) . criticalBettiOne) stateList+      agreements = fmap (\state -> (criticalBettiOne state, length (criticalHoles state))) maximalStates+      coherentStates =+        filter+          (\state -> criticalBettiOne state > 0 && criticalBettiOne state == length (criticalHoles state))+          stateList+      sevenPetalStates = filter ((== rosePetalCount) . criticalBettiOne) coherentStates+   in if maximumBettiOne <= 0+        then Left RoseNoPersistentHole+        else+          case NonEmpty.nonEmpty sevenPetalStates of+            Just preferredStates -> Right (latestCriticalState preferredStates)+            Nothing ->+              maybe+                (Left (RoseNoCoherentHero maximumBettiOne agreements))+                Right+                (find (\state -> criticalBettiOne state == length (criticalHoles state)) maximalStates)++latestCriticalState :: NonEmpty CriticalState -> CriticalState+latestCriticalState (firstState :| remainingStates) =+  foldl'+    (\latest candidate -> if criticalRank candidate > criticalRank latest then candidate else latest)+    firstState+    remainingStates++holeComponentsAt+  :: Int+  -> DelaunayTriangulation ()+  -> AlphaFiltration+  -> AlphaBirth+  -> Either PersistenceRoseError [HoleComponent]+holeComponentsAt rankValue triangulation filtration threshold = do+  inactiveEdges <-+    inactiveHandlesAt+      RoseEdgeBirthMissing+      (alphaEdgeBirth filtration)+      (undirectedEdges triangulation)+      threshold+  inactiveFaces <-+    inactiveHandlesAt+      RoseFaceBirthMissing+      (alphaFaceBirth filtration)+      (innerFaces triangulation)+      threshold+  let edgeInactive edge = Set.member edge inactiveEdges+      faceInactive face = Set.member face inactiveFaces+      exteriorFaces = Set.fromList (outerFaceFloodFill triangulation edgeInactive)+      inactiveComponents =+        fmap snd+          ( faceComponentsBy+              triangulation+              (\face -> if faceInactive face then Just () else Nothing)+              edgeInactive+          )+      boundedComponents =+        filter+          (all (`Set.notMember` exteriorFaces) . faceComponentFaces)+          inactiveComponents+  traverse+    (projectHoleComponent rankValue triangulation)+    boundedComponents++inactiveHandlesAt+  :: Ord handle+  => (handle -> PersistenceRoseError)+  -> (handle -> Maybe AlphaBirth)+  -> [handle]+  -> AlphaBirth+  -> Either PersistenceRoseError (Set.Set handle)+inactiveHandlesAt missingBirth birthAt handles threshold =+  Set.fromList . mapMaybe id+    <$> traverse+      ( \handle ->+          maybe+            (Left (missingBirth handle))+            (\birth -> Right (if birth > threshold then Just handle else Nothing))+            (birthAt handle)+      )+      handles++projectHoleComponent+  :: Int+  -> DelaunayTriangulation ()+  -> FaceComponent+  -> Either PersistenceRoseError HoleComponent+projectHoleComponent rankValue triangulation component = do+  loops <-+    first (RoseBoundaryDescentFailed rankValue)+      (componentBoundaryLoops triangulation component)+  pure+    ( HoleComponent+        ( fmap+            (fmap (vertexPoint triangulation) . NonEmpty.toList . boundaryLoopVertices)+            (NonEmpty.toList loops)+        )+    )++projectRankedEdge+  :: DelaunayTriangulation ()+  -> AlphaFiltration+  -> Map AlphaBirth Int+  -> UndirectedEdgeId+  -> Either PersistenceRoseError RankedSegment+projectRankedEdge triangulation filtration birthRanks edge = do+  birth <- maybe (Left (RoseEdgeBirthMissing edge)) Right (alphaEdgeBirth filtration edge)+  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)+  let (fromVertex, toVertex) = undirectedEndpoints triangulation edge+  pure+    RankedSegment+      { rankedSegmentGeometry = Segment (vertexPoint triangulation fromVertex) (vertexPoint triangulation toVertex),+        rankedSegmentBirthRank = rankValue+      }++projectRankedFace+  :: DelaunayTriangulation ()+  -> AlphaFiltration+  -> Map AlphaBirth Int+  -> (Int, (VertexId, VertexId, VertexId))+  -> Either PersistenceRoseError RankedTriangle+projectRankedFace triangulation filtration birthRanks (rawFace, (firstVertex, secondVertex, thirdVertex)) = do+  let face = toFaceId rawFace+  birth <- maybe (Left (RoseFaceBirthMissing face)) Right (alphaFaceBirth filtration face)+  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)+  pure+    RankedTriangle+      { rankedTrianglePoints =+          ( vertexPoint triangulation firstVertex,+            vertexPoint triangulation secondVertex,+            vertexPoint triangulation thirdVertex+          ),+        rankedTriangleBirthRank = rankValue+      }++toFaceId :: Int -> FaceId+toFaceId = FaceId . fromIntegral++projectFiniteVoronoiSegments+  :: DelaunayTriangulation ()+  -> Either PersistenceRoseError [Segment]+projectFiniteVoronoiSegments triangulation =+  fmap (mapMaybe id)+    . traverse projectSegment+    $ undirectedEdges triangulation+ where+  projectSegment :: UndirectedEdgeId -> Either PersistenceRoseError (Maybe Segment)+  projectSegment edge =+    case voronoiEdgeGeometry triangulation (asDirectedVoronoiEdge (normalizedDirected edge)) of+      Nothing -> Left (RoseVoronoiGeometryMissing edge)+      Just (VoronoiSegment fromPoint toPoint) -> Right (Just (Segment fromPoint toPoint))+      Just (VoronoiRay _ _) -> Right Nothing+      Just (VoronoiLine _ _) -> Right Nothing++naturalNeighborField+  :: DelaunayTriangulation ()+  -> Either PersistenceRoseError ([FieldSample], Int)+naturalNeighborField triangulation = do+  admittedQueries <- traverse (first RosePointInvalid . mkQueryPoint) fieldQueryPoints+  let results = runST $ do+        workspace <- newNaturalNeighborWorkspace triangulation+        traverse (naturalNeighborWeights workspace Nothing) admittedQueries+      pairedResults = zip admittedQueries results+      fallbackCount = length (filter (interpolationUsedFallback . naturalNeighborStats . snd) pairedResults)+      samples = mapMaybe (fieldSampleFromResult triangulation) pairedResults+  pure (samples, fallbackCount)++fieldSampleFromResult+  :: DelaunayTriangulation ()+  -> (QueryPoint, NaturalNeighborResult)+  -> Maybe FieldSample+fieldSampleFromResult triangulation (query, result)+  | Vector.null weights = Nothing+  | interpolationUsedFallback (naturalNeighborStats result) = Nothing+  | otherwise =+      let weightedValue =+            Vector.foldl'+              (\total (vertex, weight) -> total + weight * siteSignal (vertexPoint triangulation vertex))+              0+              weights+       in Just+            FieldSample+              { fieldSamplePoint = queryPointValue query,+                fieldSampleValue = clamp 0 1 weightedValue,+                fieldSampleNeighborCount = Vector.length weights+              }+ where+  weights = naturalNeighborValues result++siteSignal :: Point -> Double+siteSignal (Point x y) =+  clamp 0 1 (0.5 + 0.24 * sin (1.31 * x - 0.73 * y) + 0.18 * cos (0.61 * x + 1.17 * y))++fieldQueryPoints :: [Point]+fieldQueryPoints =+  [ Point (fromIntegral column * 0.34 - 5.1) (fromIntegral row * 0.34 - 4.42)+  | row <- [0 .. 26 :: Int],+    column <- [0 .. 30 :: Int]+  ]++projectPersistenceArcs+  :: Map AlphaBirth Int+  -> Int+  -> [PersistencePair AlphaBirth]+  -> Either PersistenceRoseError [PersistenceArc]+projectPersistenceArcs birthRanks criticalCount pairs = do+  arcs <- traverse pairArc (filter isPositiveHole pairs)+  pure (take 36 (sortOn (Down . persistenceArcLifetime) arcs))+ where+  finalRank = max 0 (criticalCount - 1)+  pairArc pair = do+    birthRank <- rankOf (persistenceBirth pair)+    deathRank <- maybe (Right finalRank) rankOf (persistenceDeath pair)+    pure+      PersistenceArc+        { persistenceArcBirthRank = birthRank,+          persistenceArcDeathRank = deathRank+        }+  rankOf birth = maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)++persistenceArcLifetime :: PersistenceArc -> Int+persistenceArcLifetime PersistenceArc{persistenceArcBirthRank, persistenceArcDeathRank} =+  max 0 (persistenceArcDeathRank - persistenceArcBirthRank)++isPositiveHole :: PersistencePair AlphaBirth -> Bool+isPositiveHole pair =+  persistenceDegree pair == HomologicalDegree 1+    && maybe True (> persistenceBirth pair) (persistenceDeath pair)++isFinitePositiveHole :: PersistencePair AlphaBirth -> Bool+isFinitePositiveHole pair = isPositiveHole pair && maybe False (> persistenceBirth pair) (persistenceDeath pair)++isEssentialHole :: PersistencePair AlphaBirth -> Bool+isEssentialHole pair = persistenceDegree pair == HomologicalDegree 1 && persistenceDeath pair == Nothing++buildRoseMask :: Either PersistenceRoseError [MaskComponent]+buildRoseMask = do+  kernelPoints <- traverse (first RosePointInvalid . exactPointFromPoint) minkowskiKernel+  kernel <- first RoseMinkowskiFailed (convexPolygon kernelPoints >>= structuringElement)+  authoredRegions <- traverse authoredRegion roseMaskLoops+  offsetRegions <-+    traverse+      (fmap fst . first RoseMinkowskiFailed . polygonOffset kernel)+      authoredRegions+  unioned <-+    case NonEmpty.nonEmpty offsetRegions of+      Nothing -> Left RoseNoMaskRegions+      Just (firstRegion :| remainingRegions) -> foldM exactRegionUnion firstRegion remainingRegions+  traverse projectMaskComponent (planarRegionComponents unioned)++authoredRegion :: NonEmpty Point -> Either PersistenceRoseError PlanarRegion+authoredRegion points = do+  exactPoints <- traverse (first RosePointInvalid . exactPointFromPoint) points+  loop <- first RoseRegionInvalid (exactLoop exactPoints)+  component <- first RoseRegionInvalid (polygonComponent loop [])+  first RoseRegionInvalid (planarRegion [component])++exactRegionUnion :: PlanarRegion -> PlanarRegion -> Either PersistenceRoseError PlanarRegion+exactRegionUnion leftRegion rightRegion = do+  leftLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True leftRegion))+  rightLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True rightRegion))+  overlaid <- first RoseOverlayFailed (overlayLayers leftLayer rightLayer)+  first RoseOverlayPublicationFailed (overlaySelectedRegion (uncurry (||)) overlaid)++projectMaskComponent :: PolygonComponent -> Either PersistenceRoseError MaskComponent+projectMaskComponent component =+  MaskComponent+    <$> projectExactLoop (polygonOuterLoop component)+    <*> traverse projectExactLoop (polygonHoleLoops component)++projectExactLoop :: ExactLoop -> Either PersistenceRoseError [Point]+projectExactLoop =+  traverse+    (first RosePointInvalid . exactPointToEmbeddingCandidate)+    . NonEmpty.toList+    . exactLoopPoints++roseSites :: [Point]+roseSites =+  concatMap petalSites [0 .. rosePetalCount - 1]+    <> centerSites++petalSites :: Int -> [Point]+petalSites petalIndex =+  fmap+    (petalPoint petalIndex 2.52 1.82 0.69)+    (sampleAngles 22 (0.021 * fromIntegral petalIndex))++centerSites :: [Point]+centerSites =+  Point 0 0+    : fmap+      (\angle -> Point (0.56 * cos angle) (0.56 * sin angle))+      (sampleAngles 14 0.037)++roseMaskLoops :: [NonEmpty Point]+roseMaskLoops =+  fmap+    (\petalIndex -> fmap (petalPoint petalIndex 2.42 2.12 0.9) (sampleAnglesNonEmpty 34 0))+    [0 .. rosePetalCount - 1]+    <> [fmap (\angle -> Point (1.08 * cos angle) (1.08 * sin angle)) (sampleAnglesNonEmpty 30 0)]++petalPoint :: Int -> Double -> Double -> Double -> Double -> Point+petalPoint petalIndex centerRadius radialRadius tangentialRadius localAngle =+  let orientation = twoPi * fromIntegral petalIndex / fromIntegral rosePetalCount - pi / 2+      radial = centerRadius + radialRadius * cos localAngle+      tangential = tangentialRadius * sin localAngle * (0.91 + 0.09 * cos (2 * localAngle))+      radialX = cos orientation+      radialY = sin orientation+      tangentX = -radialY+      tangentY = radialX+   in Point+        (radial * radialX + tangential * tangentX)+        (radial * radialY + tangential * tangentY)++sampleAngles :: Int -> Double -> [Double]+sampleAngles sampleCount phase =+  fmap+    (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)+    [0 .. sampleCount - 1]++sampleAnglesNonEmpty :: Int -> Double -> NonEmpty Double+sampleAnglesNonEmpty sampleCount phase =+  phase+    :| fmap+      (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)+      [1 .. max 1 sampleCount - 1]++minkowskiKernel :: NonEmpty Point+minkowskiKernel =+  Point 0.085 0+    :| [ Point 0.043 0.074,+         Point (-0.043) 0.074,+         Point (-0.085) 0,+         Point (-0.043) (-0.074),+         Point 0.043 (-0.074)+       ]++rosePetalCount :: Int+rosePetalCount = 7++-- | The package's canonical exact persistence witness. Its four hull edges+-- enter at squared radius one; the resident diagonal and two resident faces+-- enter at squared radius two. Homology therefore observes exactly one finite+-- degree-one interval @[1,2)@ over this fixed DCEL.+alphaEclipseSites :: [Point]+alphaEclipseSites =+  [ Point (-1) (-1),+    Point 1 (-1),+    Point 1 1,+    Point (-1) 1+  ]++renderPersistenceRosePoster :: PersistenceRoseScene -> String+renderPersistenceRosePoster scene =+  renderScene scene (criticalRank (sceneHeroState scene)) 1 True++renderAlphaEclipse :: AlphaEclipseScene -> String+renderAlphaEclipse scene =+  let receipt = eclipseReceipt scene+      birthLabel =+        renderExactRatio+          (eclipseHoleBirthNumerator receipt)+          (eclipseHoleBirthDenominator receipt)+      deathLabel =+        renderExactRatio+          (eclipseHoleDeathNumerator receipt)+          (eclipseHoleDeathDenominator receipt)+   in concat+        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1440 620\" width=\"1440\" height=\"620\" role=\"img\" aria-labelledby=\"title description\">\n",+          "<title id=\"title\">Moonlight Triangulation alpha eclipse</title>\n",+          "<desc id=\"description\">One fixed four-site Delaunay DCEL at three exact alpha sublevels. Its resident boundary creates one hole at squared radius ",+          birthLabel,+          ", and its resident diagonal and faces kill that hole at squared radius ",+          deathLabel,+          ".</desc>\n",+          renderAlphaEclipseDefinitions,+          "<rect width=\"1440\" height=\"620\" fill=\"url(#eclipse-background)\"/>\n",+          "<circle cx=\"720\" cy=\"300\" r=\"246\" fill=\"#ff68bb\" opacity=\"0.035\" filter=\"url(#eclipse-soft-glow)\"/>\n",+          "<text x=\"58\" y=\"58\" fill=\"#84f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"17\" font-weight=\"600\" letter-spacing=\"6\">MOONLIGHT TRIANGULATION</text>\n",+          "<text x=\"58\" y=\"101\" fill=\"#f7f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"38\" font-weight=\"420\">ALPHA ECLIPSE</text>\n",+          "<text x=\"1382\" y=\"61\" text-anchor=\"end\" fill=\"#9baedb\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.5\">ONE FIXED DELAUNAY DCEL · EXACT CRITICAL BIRTHS</text>\n",+          renderAlphaEclipseStage+            scene+            245+            "BEFORE BIRTH"+            "four isolated sites"+            (eclipseBeforeState scene),+          renderAlphaEclipseStage+            scene+            720+            "LOOP BORN"+            "resident boundary closes"+            (eclipseBirthState scene),+          renderAlphaEclipseStage+            scene+            1195+            "CYCLE KILLED"+            "resident faces fill it"+            (eclipseDeathState scene),+          "<g font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\">\n",+          "<text x=\"58\" y=\"516\" fill=\"#9badd8\" font-size=\"13\" letter-spacing=\"2.5\">ONE EXACT H₁ INTERVAL</text>\n",+          "<line x1=\"58\" y1=\"559\" x2=\"1382\" y2=\"559\" stroke=\"#344070\" stroke-width=\"3\"/>\n",+          "<line x1=\"535\" y1=\"559\" x2=\"1042\" y2=\"559\" stroke=\"#ff72c6\" stroke-width=\"9\" stroke-linecap=\"round\"/>\n",+          "<circle cx=\"535\" cy=\"559\" r=\"8\" fill=\"#fff6d6\" stroke=\"#ff72c6\" stroke-width=\"3\"/>\n",+          "<rect x=\"1034\" y=\"551\" width=\"16\" height=\"16\" fill=\"#fff6d6\" stroke=\"#66efff\" stroke-width=\"3\"/>\n",+          "<text x=\"58\" y=\"589\" fill=\"#7588ba\" font-size=\"12\">α² = 0</text>\n",+          "<text x=\"535\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">birth α² = ",+          birthLabel,+          "</text>\n",+          "<text x=\"788\" y=\"548\" text-anchor=\"middle\" fill=\"#fff6d6\" font-size=\"14\">the eclipse hole persists</text>\n",+          "<text x=\"1042\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">death α² = ",+          deathLabel,+          "</text>\n",+          "<text x=\"1382\" y=\"589\" text-anchor=\"end\" fill=\"#7588ba\" font-size=\"12\">exact α² increases →</text>\n",+          "</g>\n",+          "</svg>\n"+        ]++renderAlphaEclipseDefinitions :: String+renderAlphaEclipseDefinitions =+  concat+    [ "<defs>\n",+      "<radialGradient id=\"eclipse-background\" cx=\"50%\" cy=\"38%\" r=\"78%\"><stop offset=\"0\" stop-color=\"#111744\"/><stop offset=\"0.55\" stop-color=\"#070b25\"/><stop offset=\"1\" stop-color=\"#02040f\"/></radialGradient>\n",+      "<linearGradient id=\"eclipse-corona\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#66efff\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#ffd86b\"/></linearGradient>\n",+      "<linearGradient id=\"eclipse-face\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#7c4dff\"/><stop offset=\"0.5\" stop-color=\"#ff4fa3\"/><stop offset=\"1\" stop-color=\"#ffbf5b\"/></linearGradient>\n",+      "<filter id=\"eclipse-soft-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",+      "<filter id=\"eclipse-dot-glow\" x=\"-100%\" y=\"-100%\" width=\"300%\" height=\"300%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",+      "</defs>\n"+    ]++renderAlphaEclipseStage+  :: AlphaEclipseScene+  -> Double+  -> String+  -> String+  -> CriticalState+  -> String+renderAlphaEclipseStage scene centerX stageLabel explanation state =+  let rankValue = criticalRank state+      activeFaces = filter ((<= rankValue) . rankedTriangleBirthRank) (eclipseFaces scene)+      activeEdges = filter ((<= rankValue) . rankedSegmentBirthRank) (eclipseEdges scene)+      highlighted = criticalBettiOne state > 0+      frameStroke = if highlighted then "#ff6fbd" else "#344070"+      frameOpacity = if highlighted then "0.72" else "0.55"+   in concat+        [ "<g transform=\"translate(",+          formatDouble centerX,+          " 292)\">\n",+          "<rect x=\"-194\" y=\"-150\" width=\"388\" height=\"292\" rx=\"28\" fill=\"#090d26\" fill-opacity=\"0.72\" stroke=\"",+          frameStroke,+          "\" stroke-opacity=\"",+          frameOpacity,+          "\"/>\n",+          "<text x=\"-162\" y=\"-114\" fill=\"",+          if highlighted then "#ff89cb" else "#8fa4d6",+          "\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"13\" letter-spacing=\"2.4\">",+          stageLabel,+          "</text>\n",+          "<circle r=\"104\" fill=\"#01020a\" stroke=\"#7fefff\" stroke-opacity=\"0.18\" stroke-width=\"2\"/>\n",+          "<g fill=\"url(#eclipse-face)\" fill-opacity=\"0.58\" stroke=\"#ffd56d\" stroke-opacity=\"0.48\" stroke-width=\"1.2\">",+          concatMap renderAlphaEclipseTriangle activeFaces,+          "</g>\n",+          "<g fill=\"none\" stroke=\"url(#eclipse-corona)\" stroke-width=\"5\" stroke-linejoin=\"round\" stroke-linecap=\"round\" filter=\"url(#eclipse-soft-glow)\">",+          concatMap renderAlphaEclipseSegment activeEdges,+          "</g>\n",+          "<g fill=\"none\" stroke=\"#fff7da\" stroke-width=\"3.2\" stroke-linejoin=\"round\" opacity=\"0.86\">",+          concatMap renderAlphaEclipseHole (criticalHoles state),+          "</g>\n",+          "<g fill=\"#fff8df\" stroke=\"#ff75c5\" stroke-width=\"2\" filter=\"url(#eclipse-dot-glow)\">",+          concatMap renderAlphaEclipseSite (eclipseSites scene),+          "</g>\n",+          "<rect x=\"-126\" y=\"-18\" width=\"252\" height=\"36\" rx=\"18\" fill=\"#05071a\" fill-opacity=\"0.82\"/>\n",+          "<text y=\"5\" text-anchor=\"middle\" fill=\"#fff7da\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.2\">",+          explanation,+          "</text>\n",+          "<text x=\"-162\" y=\"121\" fill=\"#fff4cc\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"25\">β₁ = ",+          show (criticalBettiOne state),+          "</text>\n",+          "<text x=\"162\" y=\"121\" text-anchor=\"end\" fill=\"#8495c4\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"12\">α² = ",+          renderAlphaBirth (criticalBirth state),+          "</text>\n",+          "</g>\n"+        ]++renderAlphaEclipseTriangle :: RankedTriangle -> String+renderAlphaEclipseTriangle RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint)} =+  "<polygon points=\""+    <> renderScreenPoints (fmap toAlphaEclipseLocal [firstPoint, secondPoint, thirdPoint])+    <> "\"/>\n"++renderAlphaEclipseSegment :: RankedSegment -> String+renderAlphaEclipseSegment RankedSegment{rankedSegmentGeometry = Segment firstPoint secondPoint} =+  let Point firstX firstY = toAlphaEclipseLocal firstPoint+      Point secondX secondY = toAlphaEclipseLocal secondPoint+   in "<line x1=\""+        <> formatDouble firstX+        <> "\" y1=\""+        <> formatDouble firstY+        <> "\" x2=\""+        <> formatDouble secondX+        <> "\" y2=\""+        <> formatDouble secondY+        <> "\"/>\n"++renderAlphaEclipseHole :: HoleComponent -> String+renderAlphaEclipseHole (HoleComponent loops) =+  concatMap+    (\points -> "<path d=\"" <> renderClosedPath (fmap toAlphaEclipseLocal points) <> "\"/>\n")+    loops++renderAlphaEclipseSite :: Point -> String+renderAlphaEclipseSite point =+  let Point x y = toAlphaEclipseLocal point+   in "<circle cx=\""+        <> formatDouble x+        <> "\" cy=\""+        <> formatDouble y+        <> "\" r=\"6\"/>\n"++toAlphaEclipseLocal :: Point -> Point+toAlphaEclipseLocal (Point x y) =+  let scale = 72+      inverseSquareRootTwo = sqrt 0.5+   in Point+        (scale * inverseSquareRootTwo * (x - y))+        (scale * inverseSquareRootTwo * (x + y))++renderAlphaBirth :: AlphaBirth -> String+renderAlphaBirth birth =+  renderExactRatio (alphaBirthNumerator birth) (alphaBirthDenominator birth)++renderExactRatio :: Integer -> Integer -> String+renderExactRatio numerator denominator+  | denominator == 1 = show numerator+  | otherwise = show numerator <> "/" <> show denominator++persistenceRoseFrameSpecs :: Int -> Either PersistenceRoseError [PersistenceRoseFrame]+persistenceRoseFrameSpecs frameCount+  | frameCount < 2 = Left (RoseFrameCountTooSmall frameCount)+  | otherwise =+      Right+        ( fmap+            (\frameIndex ->+               PersistenceRoseFrame+                 { frameProgress = frameProgressAt frameCount frameIndex+                 })+            [0 .. frameCount - 1]+        )++renderPersistenceRoseFrame :: PersistenceRoseScene -> PersistenceRoseFrame -> String+renderPersistenceRoseFrame scene frame =+  let finalRank = criticalRank (NonEmpty.last (sceneCriticalStates scene))+      selectedRank = clampInt 0 finalRank (floor (frameProgress frame * fromIntegral finalRank))+   in renderScene scene selectedRank (frameProgress frame) False++persistenceRoseAnimationReceipt+  :: PersistenceRoseScene+  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt+persistenceRoseAnimationReceipt scene = do+  frames <- persistenceRoseFrameSpecs canonicalAnimationFrameCount+  let renderedFrames = fmap (renderPersistenceRoseFrame scene) frames+  pure+    PersistenceRoseAnimationReceipt+      { roseAnimationFrameCount = canonicalAnimationFrameCount,+        roseAnimationFramesPerSecond = canonicalAnimationFramesPerSecond,+        roseAnimationWidth = canonicalAnimationWidth,+        roseAnimationHeight = canonicalAnimationHeight,+        roseAnimationSourceFingerprint = fingerprintSections renderedFrames+      }++renderPersistenceRoseAnimationReceipt :: PersistenceRoseAnimationReceipt -> String+renderPersistenceRoseAnimationReceipt receipt =+  intercalate+    " "+    [ "MOONLIGHT_PERSISTENCE_ROSE",+      "frame-count=" <> show (roseAnimationFrameCount receipt),+      "fps=" <> show (roseAnimationFramesPerSecond receipt),+      "size=" <> show (roseAnimationWidth receipt) <> "x" <> show (roseAnimationHeight receipt),+      "source-fnv1a64=" <> renderWord64Hex (roseAnimationSourceFingerprint receipt)+    ]++stampPersistenceRoseGif+  :: PersistenceRoseScene+  -> ByteString+  -> Either PersistenceRoseError (PersistenceRoseAnimationReceipt, ByteString)+stampPersistenceRoseGif scene gifBytes = do+  receipt <- persistenceRoseAnimationReceipt scene+  summary <- inspectGif gifBytes+  validateGifSummary receipt summary+  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)+      markerLength = ByteString.length marker+  if markerLength > 255+    then Left (RoseGifMarkerTooLong markerLength)+    else+      if marker `ByteString.isInfixOf` gifBytes+        then Right (receipt, gifBytes)+        else+          if gifMarkerPrefix `ByteString.isInfixOf` gifBytes+            then Left RoseGifProvenanceMismatch+            else do+              insertionOffset <- gifDataStart gifBytes+              let (prefix, suffix) = ByteString.splitAt insertionOffset gifBytes+                  commentExtension =+                    ByteString.pack [0x21, 0xfe, fromIntegral markerLength]+                      <> marker+                      <> ByteString.singleton 0+              pure (receipt, prefix <> commentExtension <> suffix)++verifyPersistenceRoseGif+  :: PersistenceRoseScene+  -> ByteString+  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt+verifyPersistenceRoseGif scene gifBytes = do+  receipt <- persistenceRoseAnimationReceipt scene+  summary <- inspectGif gifBytes+  validateGifSummary receipt summary+  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)+  if marker `ByteString.isInfixOf` gifBytes+    then Right receipt+    else Left RoseGifProvenanceMismatch++data GifSummary = GifSummary+  { gifSummaryWidth :: !Int,+    gifSummaryHeight :: !Int,+    gifSummaryFrameCount :: !Int,+    gifSummaryFrameDelayCount :: !Int,+    gifSummaryDurationCentiseconds :: !Int+  }++inspectGif :: ByteString -> Either PersistenceRoseError GifSummary+inspectGif gifBytes = do+  dataStart <- gifDataStart gifBytes+  width <- gifLittleEndianWord16 gifBytes 6+  height <- gifLittleEndianWord16 gifBytes 8+  (frameCount, frameDelayCount, durationCentiseconds) <-+    countGifFrames gifBytes dataStart 0 0 0+  pure+    GifSummary+      { gifSummaryWidth = width,+        gifSummaryHeight = height,+        gifSummaryFrameCount = frameCount,+        gifSummaryFrameDelayCount = frameDelayCount,+        gifSummaryDurationCentiseconds = durationCentiseconds+      }++gifDataStart :: ByteString -> Either PersistenceRoseError Int+gifDataStart gifBytes = do+  signature <- maybe (Left (RoseGifTruncated 0)) Right (gifSlice gifBytes 0 6)+  if signature == ByteString.Char8.pack "GIF87a" || signature == ByteString.Char8.pack "GIF89a"+    then pure ()+    else Left RoseGifInvalidSignature+  packed <- maybe (Left (RoseGifTruncated 10)) Right (gifByteAt gifBytes 10)+  let globalColourTableBytes =+        if packed .&. 0x80 == 0+          then 0+          else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))+      dataStart = 13 + globalColourTableBytes+  maybe (Left (RoseGifTruncated dataStart)) (const (Right dataStart)) (gifSlice gifBytes 0 dataStart)++countGifFrames+  :: ByteString+  -> Int+  -> Int+  -> Int+  -> Int+  -> Either PersistenceRoseError (Int, Int, Int)+countGifFrames gifBytes offset frameCount frameDelayCount durationCentiseconds = do+  block <- maybe (Left (RoseGifTruncated offset)) Right (gifByteAt gifBytes offset)+  case block of+    0x3b -> Right (frameCount, frameDelayCount, durationCentiseconds)+    0x21 -> do+      extensionLabel <- maybe (Left (RoseGifTruncated (offset + 1))) Right (gifByteAt gifBytes (offset + 1))+      nextOffset <- skipGifSubBlocks gifBytes (offset + 2)+      if extensionLabel == 0xf9+        then do+          delay <- gifLittleEndianWord16 gifBytes (offset + 4)+          countGifFrames+            gifBytes+            nextOffset+            frameCount+            (frameDelayCount + 1)+            (durationCentiseconds + delay)+        else+          countGifFrames+            gifBytes+            nextOffset+            frameCount+            frameDelayCount+            durationCentiseconds+    0x2c -> do+      packed <- maybe (Left (RoseGifTruncated (offset + 9))) Right (gifByteAt gifBytes (offset + 9))+      let localColourTableBytes =+            if packed .&. 0x80 == 0+              then 0+              else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))+          imageDataStart = offset + 11 + localColourTableBytes+      maybe (Left (RoseGifTruncated imageDataStart)) (const (Right ())) (gifByteAt gifBytes (imageDataStart - 1))+      skipGifSubBlocks gifBytes imageDataStart >>= \next ->+        countGifFrames+          gifBytes+          next+          (frameCount + 1)+          frameDelayCount+          durationCentiseconds+    0x00 ->+      countGifFrames+        gifBytes+        (offset + 1)+        frameCount+        frameDelayCount+        durationCentiseconds+    unexpected -> Left (RoseGifUnexpectedBlock offset unexpected)++skipGifSubBlocks :: ByteString -> Int -> Either PersistenceRoseError Int+skipGifSubBlocks gifBytes offset = do+  blockLength <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)+  if blockLength == 0+    then Right (offset + 1)+    else do+      let nextOffset = offset + 1 + blockLength+      maybe (Left (RoseGifTruncated nextOffset)) (const (Right ())) (gifSlice gifBytes offset (1 + blockLength))+      skipGifSubBlocks gifBytes nextOffset++validateGifSummary+  :: PersistenceRoseAnimationReceipt+  -> GifSummary+  -> Either PersistenceRoseError ()+validateGifSummary receipt summary+  | actualWidth /= expectedWidth || actualHeight /= expectedHeight =+      Left (RoseGifDimensionMismatch expectedWidth expectedHeight actualWidth actualHeight)+  | actualFrameCount /= expectedFrameCount =+      Left (RoseGifFrameCountMismatch expectedFrameCount actualFrameCount)+  | actualFrameDelayCount /= expectedFrameCount =+      Left (RoseGifFrameDelayCountMismatch expectedFrameCount actualFrameDelayCount)+  | actualDurationCentiseconds /= canonicalAnimationDurationCentiseconds =+      Left+        ( RoseGifDurationMismatch+            canonicalAnimationDurationCentiseconds+            actualDurationCentiseconds+        )+  | otherwise = Right ()+ where+  expectedWidth = roseAnimationWidth receipt+  expectedHeight = roseAnimationHeight receipt+  actualWidth = gifSummaryWidth summary+  actualHeight = gifSummaryHeight summary+  expectedFrameCount = roseAnimationFrameCount receipt+  actualFrameCount = gifSummaryFrameCount summary+  actualFrameDelayCount = gifSummaryFrameDelayCount summary+  actualDurationCentiseconds = gifSummaryDurationCentiseconds summary++gifLittleEndianWord16 :: ByteString -> Int -> Either PersistenceRoseError Int+gifLittleEndianWord16 gifBytes offset = do+  low <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)+  high <- maybe (Left (RoseGifTruncated (offset + 1))) (Right . fromIntegral) (gifByteAt gifBytes (offset + 1))+  pure (low + 256 * high)++gifByteAt :: ByteString -> Int -> Maybe Word8+gifByteAt gifBytes offset = fst <$> ByteString.uncons (ByteString.drop offset gifBytes)++gifSlice :: ByteString -> Int -> Int -> Maybe ByteString+gifSlice gifBytes offset width =+  let selected = ByteString.take width (ByteString.drop offset gifBytes)+   in if ByteString.length selected == width then Just selected else Nothing++fingerprintSections :: [String] -> Word64+fingerprintSections =+  foldl'+    (\fingerprint section -> fingerprintByte (fingerprintText fingerprint section) 0)+    fnvOffsetBasis++fingerprintText :: Word64 -> String -> Word64+fingerprintText fingerprint =+  ByteString.foldl' fingerprintByte fingerprint+    . Text.Encoding.encodeUtf8+    . Text.pack++fingerprintByte :: Word64 -> Word8 -> Word64+fingerprintByte fingerprint byte =+  (fingerprint `xor` fromIntegral byte) * fnvPrime++renderWord64Hex :: Word64 -> String+renderWord64Hex value =+  let rendered = showHex value ""+   in replicate (16 - length rendered) '0' <> rendered++gifMarkerPrefix :: ByteString+gifMarkerPrefix = ByteString.Char8.pack "MOONLIGHT_PERSISTENCE_ROSE "++canonicalAnimationFrameCount :: Int+canonicalAnimationFrameCount = 65++canonicalAnimationFramesPerSecond :: Int+canonicalAnimationFramesPerSecond = 8++canonicalAnimationWidth :: Int+canonicalAnimationWidth = 960++canonicalAnimationHeight :: Int+canonicalAnimationHeight = 540++canonicalAnimationDurationCentiseconds :: Int+canonicalAnimationDurationCentiseconds = 813++fnvOffsetBasis :: Word64+fnvOffsetBasis = 14695981039346656037++fnvPrime :: Word64+fnvPrime = 1099511628211++frameProgressAt :: Int -> Int -> Double+frameProgressAt frameCount frameIndex+  | frameIndex <= 0 || frameIndex >= frameCount - 1 = 0+  | otherwise =+      let phase = fromIntegral frameIndex / fromIntegral (frameCount - 1)+       in 0.5 - 0.5 * cos (twoPi * phase)++renderScene :: PersistenceRoseScene -> Int -> Double -> Bool -> String+renderScene scene selectedRank progress poster =+  let selectedState = criticalStateAt selectedRank (sceneCriticalStates scene)+      finalRank = max 1 (criticalRank (NonEmpty.last (sceneCriticalStates scene)))+      activation = if poster then fromIntegral selectedRank / fromIntegral finalRank else progress+   in concat+        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1920 1080\" width=\"1920\" height=\"1080\" role=\"img\" aria-labelledby=\"title description\">\n",+          "<title id=\"title\">Moonlight Triangulation Persistence Rose</title>\n",+          "<desc id=\"description\">Exact Delaunay alpha filtration, persistent homology, Voronoi dual, Sibson interpolation, exact overlay, and Minkowski morphology composed into a seven-petal rose.</desc>\n",+          renderDefinitions (sceneMaskComponents scene),+          "<rect width=\"1920\" height=\"1080\" fill=\"url(#background)\"/>\n",+          renderAmbientOrbits activation,+          renderPersistenceHalo finalRank selectedRank (scenePersistenceArcs scene),+          "<g clip-path=\"url(#rose-clip)\">\n",+          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"url(#rose-wash)\" opacity=\"0.28\"/>\n",+          "<g filter=\"url(#field-soften)\">", concatMap renderFieldSample (sceneFieldSamples scene), "</g>\n",+          "<g opacity=\"0.58\">", concatMap renderVoronoiSegment (sceneVoronoiSegments scene), "</g>\n",+          concatMap (renderRankedTriangle finalRank activation) (sceneFaces scene),+          concatMap (renderRankedSegment finalRank activation) (sceneEdges scene),+          "<g filter=\"url(#hole-glow)\">", concatMap renderHoleComponent (criticalHoles selectedState), "</g>\n",+          concatMap (renderSite activation) (sceneSites scene),+          "</g>\n",+          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"none\" stroke=\"url(#mask-stroke)\" stroke-width=\"2.2\" opacity=\"0.92\" filter=\"url(#edge-glow)\"/>\n",+          renderTitle selectedState,+          "</svg>\n"+        ]++criticalStateAt :: Int -> NonEmpty CriticalState -> CriticalState+criticalStateAt target (firstState :| remainingStates) =+  foldl'+    (\chosen candidate -> if criticalRank candidate <= target then candidate else chosen)+    firstState+    remainingStates++renderDefinitions :: [MaskComponent] -> String+renderDefinitions maskComponents =+  concat+    [ "<defs>\n",+      "<radialGradient id=\"background\" cx=\"52%\" cy=\"48%\" r=\"72%\"><stop offset=\"0\" stop-color=\"#10143d\"/><stop offset=\"0.48\" stop-color=\"#070b24\"/><stop offset=\"1\" stop-color=\"#02040e\"/></radialGradient>\n",+      "<radialGradient id=\"rose-wash\" cx=\"48%\" cy=\"42%\" r=\"63%\"><stop offset=\"0\" stop-color=\"#ffe7a3\"/><stop offset=\"0.38\" stop-color=\"#ff5fb7\"/><stop offset=\"0.72\" stop-color=\"#7c4dff\"/><stop offset=\"1\" stop-color=\"#00d9ff\"/></radialGradient>\n",+      "<linearGradient id=\"mask-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#ffd86b\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#66efff\"/></linearGradient>\n",+      "<linearGradient id=\"hole-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#fff9db\"/><stop offset=\"0.52\" stop-color=\"#ff9ad7\"/><stop offset=\"1\" stop-color=\"#9ff7ff\"/></linearGradient>\n",+      "<filter id=\"field-soften\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"24\"/></filter>\n",+      "<filter id=\"hole-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",+      "<filter id=\"edge-glow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",+      "<clipPath id=\"rose-clip\"><path d=\"",+      renderMaskPath maskComponents,+      "\" fill-rule=\"evenodd\"/></clipPath>\n",+      "</defs>\n"+    ]++renderAmbientOrbits :: Double -> String+renderAmbientOrbits activation =+  concatMap+    ( \orbit ->+        let radius = 322 + fromIntegral orbit * 34+            opacity = 0.055 + activation * 0.025+         in "<circle cx=\"960\" cy=\"552\" r=\""+              <> formatDouble radius+              <> "\" fill=\"none\" stroke=\"#8ba5ff\" stroke-width=\"0.8\" stroke-dasharray=\"2 12\" opacity=\""+              <> formatDouble opacity+              <> "\"/>\n"+    )+    [0 .. 5 :: Int]++renderPersistenceHalo :: Int -> Int -> [PersistenceArc] -> String+renderPersistenceHalo finalRank selectedRank arcs =+  "<g fill=\"none\" stroke-linecap=\"round\">\n"+    <> concatMap renderArc (zip ([0 :: Int ..]) arcs)+    <> "</g>\n"+ where+  renderArc :: (Int, PersistenceArc) -> String+  renderArc (index, arc) =+    let active = persistenceArcBirthRank arc <= selectedRank && selectedRank < persistenceArcDeathRank arc+        radius = 414 + fromIntegral (index `mod` 7) * 7.5+        start = rankAngle finalRank (persistenceArcBirthRank arc)+        finish = rankAngle finalRank (persistenceArcDeathRank arc)+        points = arcPolyline radius start finish 42+        hue = 188 + (index * 17 `mod` 122)+        opacity = if active then 0.72 else 0.16+        width = if active then 2.6 else 1.1+     in "<polyline points=\""+          <> renderScreenPoints points+          <> "\" stroke=\"hsl("+          <> show hue+          <> " 92% 72%)\" stroke-width=\""+          <> formatDouble width+          <> "\" opacity=\""+          <> formatDouble opacity+          <> "\"/>\n"++rankAngle :: Int -> Int -> Double+rankAngle finalRank rankValue =+  -pi / 2 + twoPi * fromIntegral rankValue / fromIntegral (max 1 finalRank)++arcPolyline :: Double -> Double -> Double -> Int -> [Point]+arcPolyline radius start finish sampleCount =+  fmap+    ( \sampleIndex ->+        let fraction = fromIntegral sampleIndex / fromIntegral sampleCount+            angle = start + fraction * (finish - start)+         in screenPointFromPolar radius angle+    )+    [0 .. sampleCount]++screenPointFromPolar :: Double -> Double -> Point+screenPointFromPolar radius angle =+  Point (960 + radius * cos angle) (552 + radius * sin angle)++renderFieldSample :: FieldSample -> String+renderFieldSample FieldSample{fieldSamplePoint, fieldSampleValue, fieldSampleNeighborCount} =+  let Point x y = toScreen fieldSamplePoint+      hue = round (186 + 126 * fieldSampleValue) :: Int+      radius = 30 + min 5 (fromIntegral fieldSampleNeighborCount)+      opacity = 0.16 + 0.16 * fieldSampleValue+   in "<circle cx=\""+        <> formatDouble x+        <> "\" cy=\""+        <> formatDouble y+        <> "\" r=\""+        <> formatDouble radius+        <> "\" fill=\"hsl("+        <> show hue+        <> " 94% 61%)\" opacity=\""+        <> formatDouble opacity+        <> "\"/>\n"++renderVoronoiSegment :: Segment -> String+renderVoronoiSegment segment =+  renderLine segment "#61efff" 1.15 0.5 ""++renderRankedTriangle :: Int -> Double -> RankedTriangle -> String+renderRankedTriangle finalRank activation RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint), rankedTriangleBirthRank} =+  let opacity = activationOpacity finalRank activation rankedTriangleBirthRank+      hue :: Int+      hue = 282 - round (94 * fromIntegral rankedTriangleBirthRank / fromIntegral (max 1 finalRank) :: Double)+   in if opacity <= 0.001+        then ""+        else+          "<polygon points=\""+            <> renderWorldPoints [firstPoint, secondPoint, thirdPoint]+            <> "\" fill=\"hsl("+            <> show hue+            <> " 88% 60%)\" opacity=\""+            <> formatDouble (0.06 + 0.17 * opacity)+            <> "\"/>\n"++renderRankedSegment :: Int -> Double -> RankedSegment -> String+renderRankedSegment finalRank activation RankedSegment{rankedSegmentGeometry, rankedSegmentBirthRank} =+  let opacity = activationOpacity finalRank activation rankedSegmentBirthRank+      width = 0.75 + 1.25 * opacity+      color = if opacity > 0.72 then "#ffd56a" else "#dd78ff"+   in renderLine rankedSegmentGeometry color width (0.06 + 0.82 * opacity) "filter=\"url(#edge-glow)\""++activationOpacity :: Int -> Double -> Int -> Double+activationOpacity finalRank activation birthRank =+  let level = activation * fromIntegral finalRank+      distance = level - fromIntegral birthRank+   in smoothstep (distance + 0.6)++smoothstep :: Double -> Double+smoothstep value =+  let bounded = clamp 0 1 value+   in bounded * bounded * (3 - 2 * bounded)++renderHoleComponent :: HoleComponent -> String+renderHoleComponent (HoleComponent loops) =+  concatMap+    ( \points ->+        "<path d=\""+          <> renderClosedWorldPath points+          <> "\" fill=\"none\" stroke=\"url(#hole-stroke)\" stroke-width=\"5.4\" stroke-linejoin=\"round\" opacity=\"0.98\"/>\n"+    )+    loops++renderSite :: Double -> Point -> String+renderSite activation point =+  let Point x y = toScreen point+      radius = 2.5 + 1.5 * activation+   in "<circle cx=\""+        <> formatDouble x+        <> "\" cy=\""+        <> formatDouble y+        <> "\" r=\""+        <> formatDouble radius+        <> "\" fill=\"#fff8d9\" stroke=\"#ff9ad7\" stroke-width=\"1.2\" opacity=\"0.95\"/>\n"++renderLine :: Segment -> String -> Double -> Double -> String -> String+renderLine (Segment fromPoint toPoint) color width opacity extraAttributes =+  let Point fromX fromY = toScreen fromPoint+      Point toX toY = toScreen toPoint+   in "<line x1=\""+        <> formatDouble fromX+        <> "\" y1=\""+        <> formatDouble fromY+        <> "\" x2=\""+        <> formatDouble toX+        <> "\" y2=\""+        <> formatDouble toY+        <> "\" stroke=\""+        <> color+        <> "\" stroke-width=\""+        <> formatDouble width+        <> "\" stroke-linecap=\"round\" opacity=\""+        <> formatDouble opacity+        <> "\" "+        <> extraAttributes+        <> "/>\n"++renderTitle :: CriticalState -> String+renderTitle state =+  let alphaFraction = exactFractionSummary (criticalBirth state)+      alphaDecimal = formatDouble (alphaBirthToDouble (criticalBirth state))+      bettiMeaning = bettiOneMeaning (criticalBettiOne state)+   in concat+        [ "<g font-family=\"Inter, Avenir Next, Helvetica, Arial, sans-serif\" fill=\"#f7f3ff\">\n",+          "<text x=\"72\" y=\"82\" font-size=\"24\" font-weight=\"600\" letter-spacing=\"8\" fill=\"#9ff7ff\">MOONLIGHT TRIANGULATION</text>\n",+          "<text x=\"70\" y=\"145\" font-size=\"58\" font-weight=\"780\" letter-spacing=\"-1.5\">PERSISTENCE ROSE</text>\n",+          "<g transform=\"translate(1500 790)\">\n",+          "<text x=\"0\" y=\"0\" font-size=\"18\" letter-spacing=\"2.4\" fill=\"#91a8d8\">TOPOLOGICAL HOLES AT THIS SCALE</text>\n",+          "<text x=\"0\" y=\"74\" font-size=\"78\" font-weight=\"720\" fill=\"#fff5cd\">β₁ = ",+          show (criticalBettiOne state),+          "</text>\n",+          "<text x=\"2\" y=\"116\" font-size=\"17\" fill=\"#c9d3ff\">",+          bettiMeaning,+          "</text>\n",+          "<text x=\"2\" y=\"144\" font-size=\"15\" fill=\"#9ba8d4\">in the exact alpha complex</text>\n",+          "<text x=\"2\" y=\"176\" font-size=\"14\" fill=\"#7682aa\">critical rank ",+          show (criticalRank state),+          " · α² = ",+          alphaDecimal,+          "</text>\n",+          "<text x=\"2\" y=\"200\" font-size=\"12\" fill=\"#596486\">",+          alphaFraction,+          "</text>\n",+          "</g>\n",+          "</g>\n"+        ]++bettiOneMeaning :: Int -> String+bettiOneMeaning holeCount =+  case holeCount of+    0 -> "no independent one-dimensional holes"+    1 -> "1 independent one-dimensional hole"+    _ -> show holeCount <> " independent one-dimensional holes"++exactFractionSummary :: AlphaBirth -> String+exactFractionSummary birth =+  let numeratorDigits = length (show (abs (alphaBirthNumerator birth)))+      denominatorDigits = length (show (alphaBirthDenominator birth))+   in "exact " <> show numeratorDigits <> "-digit / " <> show denominatorDigits <> "-digit rational"++renderMaskPath :: [MaskComponent] -> String+renderMaskPath =+  intercalate " "+    . concatMap+      (\MaskComponent{maskOuterLoop, maskHoleLoops} -> renderClosedScreenPath maskOuterLoop : fmap renderClosedScreenPath maskHoleLoops)++renderClosedWorldPath :: [Point] -> String+renderClosedWorldPath = renderClosedPath . fmap toScreen++renderClosedScreenPath :: [Point] -> String+renderClosedScreenPath = renderClosedPath . fmap toScreen++renderClosedPath :: [Point] -> String+renderClosedPath points =+  case points of+    [] -> ""+    firstPoint : remainingPoints ->+      "M " <> renderPoint firstPoint <> concatMap (" L " <>) (fmap renderPoint remainingPoints) <> " Z"++renderWorldPoints :: [Point] -> String+renderWorldPoints = renderScreenPoints . fmap toScreen++renderScreenPoints :: [Point] -> String+renderScreenPoints = unwords . fmap renderPoint++renderPoint :: Point -> String+renderPoint (Point x y) = formatDouble x <> "," <> formatDouble y++toScreen :: Point -> Point+toScreen (Point x y) = Point (960 + x * 101) (552 + y * 101)++formatDouble :: Double -> String+formatDouble value = showFFloat (Just 3) value ""++clamp :: Double -> Double -> Double -> Double+clamp lower upper = min upper . max lower++clampInt :: Int -> Int -> Int -> Int+clampInt lower upper = min upper . max lower++twoPi :: Double+twoPi = 2 * pi
bench/dcel/Moonlight/Triangulation/DcelBench.hs view
@@ -36,7 +36,7 @@   let triangulation = buildTriangulation built       total = V.ifoldl' (lineCount triangulation queries queryCount) 0 (V.take queryCount queries)       shapeTotal = length circleEdges-  _ <- timedValue "line-and-shape-queries" (evaluate (force (total, shapeTotal)))+  _ <- timedValue "line-and-shape-queries" (pure (total, shapeTotal))   pure ()  where   lineCount@@ -66,7 +66,7 @@   analysed <-     timedValue       "face-components-and-boundaries"-      (evaluate (force (regionAnalysis triangulation)))+      (evaluate (regionAnalysis triangulation))   (components, boundaries) <- requireRight analysed   let faceCount = sum (fmap (length . faceComponentFaces . snd) components)       outerLoopCount = length boundaries@@ -122,16 +122,10 @@     timedValue       "alpha-face-membership"       ( evaluate-          ( force-              ( List.foldl'-                  (\count face ->-                     if containsFace face-                       then count + 1-                       else count-                  )-                  (0 :: Int)-                  (innerFaces triangulation)-              )+          ( List.foldl'+              (\count face -> if containsFace face then count + 1 else count)+              (0 :: Int)+              (innerFaces triangulation)           )       )   if admittedCount == 239_360
bench/join/Moonlight/Triangulation/JoinBench.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE NumericUnderscores #-}  -- | What the join costs, and against what.@@ -17,18 +18,284 @@ import Control.Monad (foldM, unless) import Data.List (sort, sortBy) import Data.Ord (comparing)+import qualified Data.Set as Set import qualified Data.Vector as V+import Data.Word (Word64) import Moonlight.Triangulation import Moonlight.Triangulation.BulkLoad (insertMany)+import qualified Moonlight.Triangulation.Dcel as Dcel+import Moonlight.Triangulation.Types (BuildStats, RefinementResult (refinementStats))+import Moonlight.Triangulation.Internal.BoxedPaged (boxedMaterializedPageCount)+import Moonlight.Triangulation.Internal.Paged (pagedOverlayPageCount) import Moonlight.Triangulation.Internal.PointIndex (lookupPointIndex) import Moonlight.Triangulation.Internal.Representation-  ( Triangulation (triPointIndex, triPointX, triPointY)+  ( Triangulation+      ( triConstraint+      , triDirectedData+      , triFaceData+      , triFaceEdge+      , triHalfTopology+      , triPointIndex+      , triPointX+      , triPointY+      , triUndirectedData+      , triVertexData+      , triVertexOut+      )   ) import System.Mem (performGC)  type Mesh = DelaunayTriangulation () type SiteMesh = DelaunayTriangulation (Point)+type ConstrainedMesh = ConstrainedDelaunayTriangulation () +data ExtensionSignature = ExtensionSignature+  { extensionOmegaGeometry :: !(Set.Set (Point, Point, Point))+  , extensionGammaGeometry :: !(Set.Set (Point, Point))+  , extensionSeamGeometry :: !(Set.Set (Point, Point, Point))+  , extensionConstraintGeometry :: !(Set.Set (Point, Point))+  }+  deriving stock (Eq, Show)++data PreparedSeparatedExtension = PreparedSeparatedExtension+  { preparedBase :: !ConstrainedMesh+  , preparedExtension :: !ConstrainedMesh+  , preparedSignature :: !ExtensionSignature+  }++-- | The scale comparison retains only observations emitted by the local+-- interpreters.  Resident face count is deliberately absent: it is the+-- independent variable, not permission to normalize local work by A.+data ExtensionLocalityReceipt = ExtensionLocalityReceipt+  { extensionLocalitySeamPublication :: !PublicationStats+  , extensionLocalityRefinementPublication :: !PublicationStats+  , extensionLocalityCachedFrontierPointReads :: !Int+  , extensionLocalityValidationClosure :: !ValidationClosureStats+  , extensionLocalityRefinementStats :: !BuildStats+  , extensionLocalityFinalOmegaGeometry :: !(Set.Set (Point, Point, Point))+  , extensionLocalityAddedVertices :: !Int+  , extensionLocalityFinalOmegaFaces :: !Int+  , extensionLocalityTouchedEdges :: !Int+  , extensionLocalityCreatedFaces :: !Int+  , extensionLocalityInterfaceReads :: !Int+  , extensionLocalityBoundaryCrossings :: !Int+  }+  deriving stock (Eq, Show)++rectangleBoundarySites :: Double -> Double -> [Point]+rectangleBoundarySites left right =+  [ Point left (-1)+  , Point right (-1)+  ]+    <> fmap (Point right) [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75]+    <> [Point right 1, Point left 1]++rectangleCollarSites :: Double -> Double -> [Point]+rectangleCollarSites left right =+  let ys = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75]+      width = right - left+   in fmap (Point (left + 0.2 * width)) ys+        <> fmap (Point (right - 0.2 * width)) ys++rectangleWorldSites :: Word64 -> Int -> Double -> Double -> [Point]+rectangleWorldSites seed count left right =+  rectangleFixedSites left right+    <> fmap+      (\(Point x y) ->+         Point+           (left + 0.35 * (right - left) + 0.15 * (right - left) * (x + 1))+           (-0.6 + 0.6 * (y + 1)))+      (randomPoints seed (max 0 (count - length (rectangleFixedSites left right))))++rectangleFixedSites :: Double -> Double -> [Point]+rectangleFixedSites left right = rectangleBoundarySites left right <> rectangleCollarSites left right++rectangleContour :: Int -> [(Int, Int)]+rectangleContour count =+  [ (index, (index + 1) `mod` count)+  | index <- [0 .. count - 1]+  ]++rectangleConstraintPairs :: [(Int, Int)]+rectangleConstraintPairs =+  rectangleContour boundaryCount+    <> chain collarLeftStart+    <> chain collarRightStart+ where+  boundaryCount = length (rectangleBoundarySites 0 1)+  collarLength = length (rectangleCollarSites 0 1) `quot` 2+  collarLeftStart = boundaryCount+  collarRightStart = collarLeftStart + collarLength+  chain start =+    [ (start + index, start + index + 1)+    | index <- [0 .. collarLength - 2]+    ]++contourPairs :: [Point] -> [(Point, Point)]+contourPairs points =+  case points of+    [] -> []+    first : rest -> zip points (rest <> [first])++orderedPointPair :: (Point, Point) -> (Point, Point)+orderedPointPair (first, second)+  | first <= second = (first, second)+  | otherwise = (second, first)++contourGeometry :: [Point] -> Set.Set (Point, Point)+contourGeometry = Set.fromList . fmap orderedPointPair . contourPairs++rectangleConstraintGeometry :: Double -> Double -> Set.Set (Point, Point)+rectangleConstraintGeometry left right =+  let boundary = rectangleBoundarySites left right+      collar = rectangleCollarSites left right+      collarLength = length collar `quot` 2+      (leftCollar, rightCollar) = splitAt collarLength collar+   in Set.unions+        [ contourGeometry boundary+        , openChainGeometry leftCollar+        , openChainGeometry rightCollar+        ]++openChainGeometry :: [Point] -> Set.Set (Point, Point)+openChainGeometry points =+  Set.fromList+    [ orderedPointPair pair+    | pair <- zip points (drop 1 points)+    ]++constraintGeometry :: ConstrainedMesh -> Set.Set (Point, Point)+constraintGeometry mesh =+  Set.fromList+    [ orderedPointPair (segmentStart segment, segmentEnd segment)+    | segment <- V.toList (constraintSegments mesh)+    ]++constrainedGeometryMesh :: [Point] -> IO ConstrainedMesh+constrainedGeometryMesh points =+  geometryOnlyPublication . buildTriangulation+    <$> requireRight+      ( constrainedDelaunay+          unitElementDefaults+          (V.fromList points)+          (V.fromList rectangleConstraintPairs)+      )++boundaryOfPermittedFaces+  :: ConstrainedMesh+  -> Set.Set FaceId+  -> Set.Set UndirectedEdgeId+boundaryOfPermittedFaces mesh faces =+  Set.fromList+    [ asUndirected edge+    | face <- Set.toList faces+    , edge <- Dcel.faceDirectedEdges mesh face+    , let adjacent = Dcel.incidentFace mesh (reverseEdge edge)+    , adjacent /= Dcel.outerFace+    , Set.notMember adjacent faces+    ]++faceGeometry :: ConstrainedMesh -> FaceId -> Maybe (Point, Point, Point)+faceGeometry mesh face =+  case sort (fmap (Dcel.vertexPoint mesh) (Dcel.faceVertices mesh face)) of+    [first, second, third] -> Just (first, second, third)+    _ -> Nothing++faceGeometrySet+  :: ConstrainedMesh+  -> Set.Set FaceId+  -> Either String (Set.Set (Point, Point, Point))+faceGeometrySet mesh faces =+  Set.fromList+    <$> traverse+      (\face ->+         maybe+           (Left ("local refinement receipt named a non-triangular face: " <> show face))+           Right+           (faceGeometry mesh face))+      (Set.toList faces)++edgeGeometry :: ConstrainedMesh -> UndirectedEdgeId -> (Point, Point)+edgeGeometry mesh edge =+  let (fromVertex, toVertex) = Dcel.undirectedEndpoints mesh edge+   in orderedPointPair+        (Dcel.vertexPoint mesh fromVertex, Dcel.vertexPoint mesh toVertex)++extensionSignature :: ConstrainedSeamResult () -> Either String ExtensionSignature+extensionSignature receipt =+  let joined = constrainedSeamResultTriangulation receipt+      permitted =+        Set.union+          (Set.fromList (fmap constrainedSeamTargetFace (V.toList (constrainedSeamRightFaceEvidence receipt))))+          (Set.fromList (V.toList (constrainedSeamJoinFaces receipt)))+      interface = boundaryOfPermittedFaces joined permitted+   in do+        seamFaces <-+          Set.fromList+            <$> traverse+              (\face ->+                 case faceGeometry joined face of+                   Just geometry -> Right geometry+                   Nothing -> Left "seam receipt named a non-triangular face")+              (V.toList (constrainedSeamJoinFaces receipt))+        let omegaFaces =+              Set.union+                ( Set.fromList+                    [ ( constrainedSeamFaceFirstPoint evidence+                      , constrainedSeamFaceSecondPoint evidence+                      , constrainedSeamFaceThirdPoint evidence+                      )+                    | evidence <- V.toList (constrainedSeamRightFaceEvidence receipt)+                    ]+                )+                seamFaces+        pure+          ExtensionSignature+            { extensionOmegaGeometry = omegaFaces+            , extensionGammaGeometry = Set.fromList (fmap (edgeGeometry joined) (Set.toList interface))+            , extensionSeamGeometry = seamFaces+            , extensionConstraintGeometry = constraintGeometry joined+            }++refinementInterfaceGeometry :: ConstrainedMesh -> Set.Set FaceId -> Set.Set (Point, Point)+refinementInterfaceGeometry mesh faces =+  Set.fromList+    [ edgeGeometry mesh edge+    | edge <- Set.toList (boundaryOfPermittedFaces mesh faces)+    ]++assertLocalPublicationStats :: String -> PublicationStats -> IO ()+assertLocalPublicationStats label stats = do+  unless (publicationUnboxedBasePageEnumerations stats == 0) $+    fail (label <> " enumerated resident unboxed base pages")+  unless (publicationUnboxedBasePageFreezes stats == 0) $+    fail (label <> " froze resident unboxed base pages")+  unless (publicationBoxedBasePageEnumerations stats == 0) $+    fail (label <> " enumerated resident boxed base pages")+  unless (publicationBoxedBasePageFreezes stats == 0) $+    fail (label <> " froze resident boxed base pages")++-- These are post-publication representation observations. They do not count+-- base-page opens or freeze enumeration and therefore are not a proof that+-- those events were absent from the timed action.+publicationPageReceipt :: ConstrainedMesh -> (Int, Int)+publicationPageReceipt mesh =+  ( sum+      [ pagedOverlayPageCount (triPointX mesh)+      , pagedOverlayPageCount (triPointY mesh)+      , pagedOverlayPageCount (triVertexOut mesh)+      , pagedOverlayPageCount (triHalfTopology mesh)+      , pagedOverlayPageCount (triFaceEdge mesh)+      , pagedOverlayPageCount (triConstraint mesh)+      ]+  , sum+      [ boxedMaterializedPageCount (triVertexData mesh)+      , boxedMaterializedPageCount (triDirectedData mesh)+      , boxedMaterializedPageCount (triUndirectedData mesh)+      , boxedMaterializedPageCount (triFaceData mesh)+      ]+  )+ benchmarks :: IO () benchmarks = do   benchmarkBalanced 20_000@@ -43,10 +310,127 @@  publicationBenchmarks :: IO () publicationBenchmarks = do-  benchmarkIndexedSupportContexts 1_000_000 5_000-  benchmarkPersistentSetOperations 1_000_000 5_000-  benchmarkPersistentPublication 5_000_000 5_000+  -- These are face bands, not site-count labels.  A planar Delaunay mesh is+  -- approximately twice as many faces as sites, so the resident fixtures use+  -- 125k and 250k sites to exercise the requested 250k/500k-face lanes, and+  -- the incoming small world uses 15k sites for its approximately 30k faces.+  prepared250k <- preparePersistentSeparatedExtension 125_000 15_000+  prepared500k <- preparePersistentSeparatedExtension 250_000 15_000+  assertFaceBand "250k-faces" 245_000 255_000 (preparedBase prepared250k)+  assertFaceBand "500k-faces" 490_000 510_000 (preparedBase prepared500k)+  assertFaceBand "30k-extension-faces" 29_000 31_000 (preparedExtension prepared250k)+  assertFaceBand "30k-extension-faces-repeat" 29_000 31_000 (preparedExtension prepared500k)+  unless (preparedSignature prepared250k == preparedSignature prepared500k) $+    fail "separated extension changed its fixed collar, Gamma, or seam geometry across A sizes"+  receipt250k <- benchmarkPersistentSeparatedExtension "250k-faces" prepared250k+  receipt500k <- benchmarkPersistentSeparatedExtension "500k-faces" prepared500k+  assertIdenticalFrontierLocality receipt250k receipt500k +assertIdenticalFrontierLocality+  :: ExtensionLocalityReceipt+  -> ExtensionLocalityReceipt+  -> IO ()+assertIdenticalFrontierLocality smaller larger = do+  assertPublicationAccounting "250k seam" (extensionLocalitySeamPublication smaller)+  assertPublicationAccounting "500k seam" (extensionLocalitySeamPublication larger)+  assertPublicationAccounting "250k refinement" (extensionLocalityRefinementPublication smaller)+  assertPublicationAccounting "500k refinement" (extensionLocalityRefinementPublication larger)+  assertPageAlignedPublicationLocality+    "seam"+    (extensionLocalitySeamPublication smaller)+    (extensionLocalitySeamPublication larger)+  assertPageAlignedPublicationLocality+    "refinement"+    (extensionLocalityRefinementPublication smaller)+    (extensionLocalityRefinementPublication larger)+  unless+    ( extensionLocalityCachedFrontierPointReads smaller+        == extensionLocalityCachedFrontierPointReads larger+    ) $+    fail "identical-frontier cached-read work grew with resident A"+  unless+    ( extensionLocalityValidationClosure smaller+        == extensionLocalityValidationClosure larger+    ) $+    fail "identical local refinement produced an A-dependent validation closure"+  unless+    ( and+        [ extensionLocalityRefinementStats smaller == extensionLocalityRefinementStats larger+        , extensionLocalityFinalOmegaGeometry smaller == extensionLocalityFinalOmegaGeometry larger+        , extensionLocalityAddedVertices smaller == extensionLocalityAddedVertices larger+        , extensionLocalityFinalOmegaFaces smaller == extensionLocalityFinalOmegaFaces larger+        , extensionLocalityTouchedEdges smaller == extensionLocalityTouchedEdges larger+        , extensionLocalityCreatedFaces smaller == extensionLocalityCreatedFaces larger+        , extensionLocalityInterfaceReads smaller == extensionLocalityInterfaceReads larger+        , extensionLocalityBoundaryCrossings smaller == extensionLocalityBoundaryCrossings larger+        ]+    ) $+    fail "identical-frontier refinement changed its canonical local work receipt"++-- The receipt counts page opens and copied cells at the storage boundary.  It+-- intentionally does not update a counter for every logical cell write: that+-- instrumentation used to put a strict fourteen-field STRef update on every+-- dense build mutation.  Local page accounting is exact and remains the+-- observation used by the extension lane.+assertPublicationAccounting :: String -> PublicationStats -> IO ()+assertPublicationAccounting label stats = do+  unless+    ( publicationUnboxedBasePageOpens stats+        == publicationUnboxedDirtyBasePages stats+        && publicationBoxedBasePageOpens stats+          == publicationBoxedDirtyBasePages stats+        && publicationUnboxedCopiedCells stats+          <= 1_024 * publicationUnboxedBasePageOpens stats+        && publicationBoxedCopiedCells stats+          <= 256 * publicationBoxedBasePageOpens stats+    ) $+    fail (label <> " page publication accounting is inconsistent")++-- A larger resident shifts the same local write intervals within fixed-size+-- pages.  At most two interval boundaries per store may therefore change page+-- classification or copy one extra page; the semantic local-work receipt below+-- remains exact.+assertPageAlignedPublicationLocality :: String -> PublicationStats -> PublicationStats -> IO ()+assertPageAlignedPublicationLocality label smaller larger = do+  let unboxedPageSlack :: Int+      unboxedPageSlack = 2 * 6+      boxedPageSlack :: Int+      boxedPageSlack = 2 * 4+      dirtyUnboxed :: PublicationStats -> Int+      dirtyUnboxed stats =+        publicationUnboxedDirtyBasePages stats+          + publicationUnboxedDirtyAppendedPages stats+      dirtyBoxed :: PublicationStats -> Int+      dirtyBoxed stats =+        publicationBoxedDirtyBasePages stats+          + publicationBoxedDirtyAppendedPages stats+      within :: Int -> Int -> Int -> Bool+      within slack left right = abs (left - right) <= slack+  unless+    ( and+        [ within unboxedPageSlack (dirtyUnboxed smaller) (dirtyUnboxed larger)+        , within (1_024 * unboxedPageSlack) (publicationUnboxedCopiedCells smaller) (publicationUnboxedCopiedCells larger)+        , within boxedPageSlack (dirtyBoxed smaller) (dirtyBoxed larger)+        , within (256 * boxedPageSlack) (publicationBoxedCopiedCells smaller) (publicationBoxedCopiedCells larger)+        ]+    ) $+    fail ("identical-frontier " <> label <> " publication escaped its fixed page-alignment envelope")++assertFaceBand :: String -> Int -> Int -> ConstrainedMesh -> IO ()+assertFaceBand label lower upper mesh = do+  let actual = numFaces mesh+  unless (lower <= actual && actual <= upper) $+    fail+      ( "publication-separated-"+          <> label+          <> " expected base faces in ["+          <> show lower+          <> ","+          <> show upper+          <> "], got "+          <> show actual+      )+ benchmarkIndexedSupportContexts :: Int -> Int -> IO () benchmarkIndexedSupportContexts baseCount deltaCount = do   let baseSites = randomPoints 0xcbbb9d5dc1059ed8 baseCount@@ -113,9 +497,9 @@   leftMesh <- geometryMesh left   rightMesh <- geometryMesh right   _ <- evaluate (force (leftMesh, rightMesh))-  _ <- timedValue "join-balanced" (evaluate . force =<< requireRight (union leftMesh rightMesh))-  _ <- timedValue "join-balanced-rebuild-input-order" (evaluate . force =<< geometryMesh sites)-  _ <- timedValue "join-balanced-rebuild-ranked-order" (evaluate . force =<< geometryMesh (canonical sites))+  _ <- timedValue "join-balanced" (requireRight (union leftMesh rightMesh))+  _ <- timedValue "join-balanced-rebuild-input-order" (geometryMesh sites)+  _ <- timedValue "join-balanced-rebuild-ranked-order" (geometryMesh (canonical sites))   pure ()  -- | Two operands whose sites are separated by a vertical line.@@ -143,7 +527,7 @@         let shifted = [Point (x + gap * extent) y | Point x y <- sites]         rightMesh <- geometryMesh shifted         _ <- evaluate (force rightMesh)-        _ <- timedValue ("join-separated-" <> name) (evaluate . force =<< requireRight (union leftMesh rightMesh))+        _ <- timedValue ("join-separated-" <> name) (requireRight (union leftMesh rightMesh))         pure ()     )     [("distant" :: String, 8), ("near", 2), ("abutting", 1.02)]@@ -165,45 +549,230 @@   _ <-     timedValue       "join-skew-rebuild"-      (evaluate . force =<< siteMesh (canonical (bulk <> addition)))+      (siteMesh (canonical (bulk <> addition)))   _ <-     timedValue       "join-skew-insert-many"-      ( evaluate . force . buildTriangulation-          =<< requireRight (insertMany bulkMesh (V.fromList addition))-      )+      (buildTriangulation <$> requireRight (insertMany bulkMesh (V.fromList addition)))   pure () -benchmarkPersistentPublication :: Int -> Int -> IO ()-benchmarkPersistentPublication baseCount extensionCount = do-  let baseSites = randomPoints 0x6a09e667f3bcc909 baseCount-      extensionSites =-        fmap-          (\(Point x y) -> Point (1.2 + 0.1 * x) y)-          (randomPoints 0xbb67ae8584caa73b extensionCount)-  base <- geometryMesh baseSites-  extension <- geometryMesh extensionSites+-- | Prepare the fixed-frontier workload once. The preview join is outside the+-- timed lanes: it certifies that changing only the resident interior did not+-- alter the B/J geometry or the exact Gamma section.+preparePersistentSeparatedExtension :: Int -> Int -> IO PreparedSeparatedExtension+preparePersistentSeparatedExtension baseCount extensionCount = do+  let baseSites = rectangleWorldSites 0x243f6a8885a308d3 baseCount (-1) 0+      extensionSites = rectangleWorldSites 0x13198a2e03707344 extensionCount 4 5+      expectedBaseConstraints = rectangleConstraintGeometry (-1) 0+      expectedExtensionConstraints = rectangleConstraintGeometry 4 5+  base <- constrainedGeometryMesh baseSites+  extension <- constrainedGeometryMesh extensionSites   _ <- evaluate (force (base, extension))-  putStrLn ("publication-base-sites: " <> show (numVertices base))-  putStrLn ("publication-base-faces: " <> show (numFaces base))-  putStrLn ("publication-extension-sites: " <> show (numVertices extension))-  putStrLn ("publication-extension-faces: " <> show (numFaces extension))-  joined <-+  unless (constraintGeometry base == expectedBaseConstraints) $+    fail "fixed A collar constraints were not retained by the source build"+  unless (constraintGeometry extension == expectedExtensionConstraints) $+    fail "fixed B collar constraints were not retained by the source build"+  preview <- requireRight (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters base extension)+  signature <- requireRight (extensionSignature preview)+  let expectedConstraints = Set.union expectedBaseConstraints expectedExtensionConstraints+  unless (extensionConstraintGeometry signature == expectedConstraints) $+    fail "source-preserving seam did not retain the fixed A/B contour constraints"+  pure+    PreparedSeparatedExtension+      { preparedBase = base+      , preparedExtension = extension+      , preparedSignature = signature+      }++-- | The extension lane is the operation the persistent-world plan actually+-- promises: geometry-only publication prepares the two frontier indexes once,+-- the caller-left constrained source stays resident, and refinement is+-- restricted to the right-source faces plus the seam faces emitted by that+-- interpretation. Construction, frontier preparation, receipt descent, and+-- the final validity observation are outside the timed extension actions;+-- neither action invokes generic union, canonical numbering, or global+-- validation.+benchmarkPersistentSeparatedExtension :: String -> PreparedSeparatedExtension -> IO ExtensionLocalityReceipt+benchmarkPersistentSeparatedExtension label prepared = do+  let base = preparedBase prepared+      extension = preparedExtension prepared+  joinedReceipt <-     timedValue-      "publication-skew-union"-      (evaluate . force =<< requireRight (union base extension))-  case validateTriangulation joined of-    [] -> pure ()-    violations -> fail ("publication-skew-union invalid: " <> show violations)-  canonicalResult <-+      ("publication-separated-" <> label <> "-join")+      (requireRight (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters base extension))+  let joined = constrainedSeamResultTriangulation joinedReceipt+      permitted =+        Set.union+          (Set.fromList (fmap constrainedSeamTargetFace (V.toList (constrainedSeamRightFaceEvidence joinedReceipt))))+          (Set.fromList (V.toList (constrainedSeamJoinFaces joinedReceipt)))+      interface = boundaryOfPermittedFaces joined permitted+      parameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 4_096+          , refineMaxArea = Just 0.0002+          , refineMaxRadiusEdgeRatio = Nothing+          , refineKeepConstraintEdges = True+          }+  signature <- requireRight (extensionSignature joinedReceipt)+  unless (signature == preparedSignature prepared) $+    fail ("publication-separated-" <> label <> " changed its prepared Omega/Gamma/J geometry")+  unless (refinementInterfaceGeometry joined permitted == extensionGammaGeometry signature) $+    fail ("publication-separated-" <> label <> " changed its certified Gamma geometry")+  refined <-     timedValue-      "publication-explicit-canonicalize"-      (evaluate . force =<< requireRight (canonicalize joined))-  unless (numVertices canonicalResult == numVertices joined) $-    fail "publication canonicalization changed the site count"-  putStrLn ("publication-result-sites: " <> show (numVertices joined))-  putStrLn ("publication-result-faces: " <> show (numFaces joined))+      ("publication-separated-" <> label <> "-local-refine")+      ( requireRight+          ( refineWithinDomain+              (const ())+              parameters+              permitted+              interface+              joined+          )+      )+  let result = refinementDomainResult refined+      receipt = refinementDomainReceipt refined+      seamPublication = constrainedSeamPublicationStats joinedReceipt+      refinementPublication = refinementPublicationStats receipt+      validationClosure = refinementValidationClosureStats receipt+      finalFaces = Set.fromList (V.toList (refinementFinalPermittedFaces receipt))+  finalOmegaGeometry <- requireRight (faceGeometrySet (refinedTriangulation result) finalFaces)+  assertLocalPublicationStats+    ("publication-separated-" <> label <> " seam")+    seamPublication+  assertLocalPublicationStats+    ("publication-separated-" <> label <> " refinement")+    refinementPublication+  unless+    ( publicationUnboxedDirtyBasePages seamPublication+        + publicationUnboxedDirtyAppendedPages seamPublication+        > 0+    ) $+    fail ("publication-separated-" <> label <> " seam published no appended unboxed writes")+  unless+    ( publicationUnboxedDirtyBasePages refinementPublication+        + publicationUnboxedDirtyAppendedPages refinementPublication+        > 0+    ) $+    fail ("publication-separated-" <> label <> " refinement published no appended unboxed writes")+  unless (refinementComplete result) $+    fail ("publication-separated-" <> label <> " exhausted its finite local refinement budget")+  unless (refinementInterfaceBoundaryReads receipt > 0) $+    fail ("publication-separated-" <> label <> " performed no positive Gamma-boundary descent")+  unless (refinementAddedVertices result > 0) $+    fail ("publication-separated-" <> label <> " performed no positive local refinement")+  case validateTriangulation (refinedTriangulation result) of+    [] -> pure ()+    violations -> fail ("publication-separated-" <> label <> " invalid: " <> show violations)+  putStrLn+    ( "publication-separated-"+        <> label+        <> "-base-faces="+        <> show (numFaces base)+        <> " extension-faces="+        <> show (numFaces extension)+        <> " receipt: omega="+        <> show (Set.size permitted)+        <> " b-faces="+        <> show (V.length (constrainedSeamRightFaceEvidence joinedReceipt))+        <> " j-faces="+        <> show (V.length (constrainedSeamJoinFaces joinedReceipt))+        <> " a-constraints="+        <> show (constrainedSeamLeftConstraintCount joinedReceipt)+        <> " b-constraints="+        <> show (V.length (constrainedSeamRightConstraintEvidence joinedReceipt))+        <> " gamma="+        <> show (Set.size interface)+        <> " final-omega="+        <> show (V.length (refinementFinalPermittedFaces receipt))+        <> " steiner="+        <> show (refinementAddedVertices result)+    )+  renderPublicationStats+    ("publication-separated-" <> label <> "-seam-publication")+    seamPublication+  putStrLn+    ( "publication-separated-"+        <> label+        <> "-seam-cached-frontier-point-reads="+        <> show (constrainedSeamCachedFrontierPointReads joinedReceipt)+    )+  renderPublicationStats+    ("publication-separated-" <> label <> "-refinement-publication")+    refinementPublication+  putStrLn+    ( "publication-separated-"+        <> label+        <> "-validation-closure: faces="+        <> show (validationClosureFaces validationClosure)+        <> " directed-edges="+        <> show (validationClosureDirectedEdges validationClosure)+        <> " vertices="+        <> show (validationClosureVertices validationClosure)+        <> " interface-pairs="+        <> show (validationClosureInterfacePairs validationClosure)+        <> " constraint-pairs="+        <> show (validationClosureConstraintPairs validationClosure)+    )+  let (unboxedOverlayPages, boxedMaterializedPages) = publicationPageReceipt (refinedTriangulation result)+  putStrLn+    ( "publication-separated-"+        <> label+        <> "-observed-final-pages: unboxed-overlay="+        <> show unboxedOverlayPages+        <> " boxed-materialized="+        <> show boxedMaterializedPages+        <> " local-touched-edges="+        <> show (V.length (refinementTouchedEdges receipt))+        <> " local-created-faces="+        <> show (V.length (refinementCreatedFaces receipt))+    )+  pure+    ExtensionLocalityReceipt+      { extensionLocalitySeamPublication = seamPublication+      , extensionLocalityRefinementPublication = refinementPublication+      , extensionLocalityCachedFrontierPointReads = constrainedSeamCachedFrontierPointReads joinedReceipt+      , extensionLocalityValidationClosure = validationClosure+      , extensionLocalityRefinementStats = refinementStats result+      , extensionLocalityFinalOmegaGeometry = finalOmegaGeometry+      , extensionLocalityAddedVertices = refinementAddedVertices result+      , extensionLocalityFinalOmegaFaces = Set.size finalFaces+      , extensionLocalityTouchedEdges = V.length (refinementTouchedEdges receipt)+      , extensionLocalityCreatedFaces = V.length (refinementCreatedFaces receipt)+      , extensionLocalityInterfaceReads = refinementInterfaceBoundaryReads receipt+      , extensionLocalityBoundaryCrossings = refinementAttemptedBoundaryCrossings receipt+      } +renderPublicationStats :: String -> PublicationStats -> IO ()+renderPublicationStats label stats =+  putStrLn+    ( label+        <> ": unboxed-base-enumerations="+        <> show (publicationUnboxedBasePageEnumerations stats)+        <> " unboxed-base-opens="+        <> show (publicationUnboxedBasePageOpens stats)+        <> " unboxed-base-freezes="+        <> show (publicationUnboxedBasePageFreezes stats)+        <> " unboxed-dirty-base-pages="+        <> show (publicationUnboxedDirtyBasePages stats)+        <> " unboxed-dirty-appended-pages="+        <> show (publicationUnboxedDirtyAppendedPages stats)+        <> " unboxed-copied-cells="+        <> show (publicationUnboxedCopiedCells stats)+        <> " boxed-base-enumerations="+        <> show (publicationBoxedBasePageEnumerations stats)+        <> " boxed-base-opens="+        <> show (publicationBoxedBasePageOpens stats)+        <> " boxed-base-freezes="+        <> show (publicationBoxedBasePageFreezes stats)+        <> " boxed-dirty-base-pages="+        <> show (publicationBoxedDirtyBasePages stats)+        <> " boxed-dirty-appended-pages="+        <> show (publicationBoxedDirtyAppendedPages stats)+        <> " boxed-copied-cells="+        <> show (publicationBoxedCopiedCells stats)+    )+ benchmarkPersistentSetOperations :: Int -> Int -> IO () benchmarkPersistentSetOperations baseCount deltaCount = do   let baseSites = randomPoints 0xcbbb9d5dc1059ed8 baseCount@@ -274,9 +843,9 @@   sameLeft <- geometryMesh (take half sites)   sameRight <- geometryMesh (reverse (take half sites))   _ <- evaluate (force (disjointLeft, disjointRight, halfLeft, halfRight, sameLeft, sameRight))-  _ <- timedValue "join-overlap-000" (evaluate . force =<< requireRight (union disjointLeft disjointRight))-  _ <- timedValue "join-overlap-050" (evaluate . force =<< requireRight (union halfLeft halfRight))-  _ <- timedValue "join-overlap-100" (evaluate . force =<< requireRight (union sameLeft sameRight))+  _ <- timedValue "join-overlap-000" (requireRight (union disjointLeft disjointRight))+  _ <- timedValue "join-overlap-050" (requireRight (union halfLeft halfRight))+  _ <- timedValue "join-overlap-100" (requireRight (union sameLeft sameRight))   pure ()  -- | 'unions' is a balanced tournament and not a fold, which is a cost claim@@ -304,8 +873,8 @@       (\shard -> geometryMesh [site | (index, site) <- indexed, index `mod` shardCount == shard])       [0 .. shardCount - 1]   _ <- evaluate (force shards)-  _ <- timedValue "join-tournament" (evaluate . force =<< requireRight (unions shards))-  _ <- timedValue "join-left-fold" (evaluate . force =<< requireRight (unionsLeftAssociated shards))+  _ <- timedValue "join-tournament" (requireRight (unions shards))+  _ <- timedValue "join-left-fold" (requireRight (unionsLeftAssociated shards))   pure ()  -- | Where the tournament's advantage over the fold actually appears.@@ -328,8 +897,8 @@             (\shard -> geometryMesh [site | (index, site) <- indexed, index `mod` shardCount == shard])             [0 .. shardCount - 1]         _ <- evaluate (force shards)-        _ <- timedValue ("join-shards-" <> show shardCount <> "-tournament") (evaluate . force =<< requireRight (unions shards))-        _ <- timedValue ("join-shards-" <> show shardCount <> "-fold") (evaluate . force =<< requireRight (unionsLeftAssociated shards))+        _ <- timedValue ("join-shards-" <> show shardCount <> "-tournament") (requireRight (unions shards))+        _ <- timedValue ("join-shards-" <> show shardCount <> "-fold") (requireRight (unionsLeftAssociated shards))         pure ()     )     [4 :: Int, 16, 64]@@ -358,7 +927,7 @@       (\shard -> geometryMesh (take width (drop (shard * width) ordered)))       [0 .. shardCount - 1]   _ <- evaluate (force shards)-  _ <- timedValue "join-spatial-tournament" (evaluate . force =<< requireRight (unions shards))+  _ <- timedValue "join-spatial-tournament" (requireRight (unions shards))   pure ()  -- | The renumbering pass on its own, against the construction it follows.@@ -367,7 +936,7 @@   let sites = randomPoints 0x27d4eb2f165667c5 total   mesh <- geometryMesh sites   _ <- evaluate (force mesh)-  _ <- timedValue "canonicalize-alone" (evaluate (force (canonicalize mesh)))+  _ <- timedValue "canonicalize-alone" (evaluate . force =<< requireRight (canonicalize mesh))   pure ()  -- | The shared canonical rebuild boundary under half overlap, on both its@@ -385,36 +954,36 @@   rightGeometry <- geometryMesh rightPoints   emptyGeometry <- requireRight (unions [])   _ <- evaluate (force (leftGeometry, rightGeometry, emptyGeometry))-  _ <- timedValue "set-intersection-unit" (evaluate . force =<< requireRight (intersection leftGeometry rightGeometry))-  _ <- timedValue "set-difference-unit" (evaluate . force =<< requireRight (difference leftGeometry rightGeometry))+  _ <- timedValue "set-intersection-unit" (requireRight (intersection leftGeometry rightGeometry))+  _ <- timedValue "set-difference-unit" (requireRight (difference leftGeometry rightGeometry))   _ <-     timedValue       "set-symmetric-difference-unit"-      (evaluate . force =<< requireRight (symmetricDifference leftGeometry rightGeometry))+      (requireRight (symmetricDifference leftGeometry rightGeometry))   _ <-     timedValue       "set-difference-right-empty"-      (evaluate . force =<< requireRight (difference leftGeometry emptyGeometry))+      (requireRight (difference leftGeometry emptyGeometry))   _ <-     timedValue       "set-symmetric-difference-left-empty"-      (evaluate . force =<< requireRight (symmetricDifference emptyGeometry leftGeometry))-  _ <- timedValue "set-relation-half-overlap" (evaluate (force (siteRelation leftGeometry rightGeometry)))+      (requireRight (symmetricDifference emptyGeometry leftGeometry))+  _ <- timedValue "set-relation-half-overlap" (evaluate (siteRelation leftGeometry rightGeometry))   leftAnnotated <- siteMesh leftPoints   rightAnnotated <- siteMesh rightPoints   _ <- evaluate (force (leftAnnotated, rightAnnotated))   _ <-     timedValue       "set-intersection-annotated"-      (evaluate . force =<< requireRight (intersectionWith (,) leftAnnotated rightAnnotated))+      (requireRight (intersectionWith (,) leftAnnotated rightAnnotated))   _ <-     timedValue       "set-difference-annotated"-      (evaluate . force =<< requireRight (difference leftAnnotated rightAnnotated))+      (requireRight (difference leftAnnotated rightAnnotated))   _ <-     timedValue       "set-symmetric-difference-annotated"-      (evaluate . force =<< requireRight (symmetricDifference leftAnnotated rightAnnotated))+      (requireRight (symmetricDifference leftAnnotated rightAnnotated))   pure ()  
bench/region/Moonlight/Triangulation/RegionBench.hs view
@@ -177,9 +177,7 @@          components <-            traverse              rectangleComponent-             [ (3 * index, 0, 3 * index + 1, 1)-             | index <- [0 .. size - 1]-             ]+             (disjointRectangles size)          length . planarRegionComponents <$> planarRegion components)   if componentCount == size     then@@ -197,15 +195,13 @@     requireRight       ( traverse           rectangleComponent-          [ (3 * index, 0, 3 * index + 1, 1)-          | index <- [0 .. size - 1]-          ]+          (disjointRectangles size)       )   region <- requireRight (planarRegion components)   valuations <-     timedValue       ("planar-region-valuations-n" <> show size)-      (evaluate . force =<< requireRight (regionValuations region))+      (requireRight (regionValuations region))   let receipt =         ( eulerCharacteristicValue (valuationEuler valuations)         , length@@ -231,7 +227,7 @@   result <-     timedValue       ("convex-minkowski-n" <> show (2 * halfSize))-      (evaluate (force (convexMinkowskiSum left right)))+      (evaluate (convexMinkowskiSum left right))   let outputVertices =         sum           [ length (exactLoopPoints (polygonOuterLoop component))@@ -342,10 +338,7 @@   case family of     DisjointFamily ->       (,)-        <$> layerFromRectangles-          [ (3 * index, 0, 3 * index + 1, 1)-          | index <- [0 .. size - 1]-          ]+        <$> layerFromRectangles (disjointRectangles size)         <*> planarLayer 0 Map.empty     GridCrossingFamily ->       (,)@@ -367,6 +360,13 @@           [ (3 * index + 1, 0, 3 * index + 3, 1)           | index <- [0 .. size - 1]           ]++disjointRectangles :: Int -> [(Int, Int, Int, Int)]+disjointRectangles size =+  [ (3 * index, 0, 3 * index + 1, 1)+  | index <- [0 .. size - 1]+  ]+{-# INLINE disjointRectangles #-}  layerFromRectangles   :: [(Int, Int, Int, Int)]
cbits/moonlight_runtime.c view
@@ -1,6 +1,13 @@ #include "moonlight_triangulation.h" #include "HsFFI.h" +_Static_assert(ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE == 60, "maximum-edge ABI code drift");+_Static_assert(ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE == 61, "maximum-edge ABI code drift");+_Static_assert(ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE == 62, "maximum-edge ABI code drift");+_Static_assert(ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED == 63, "seam-bridge ABI code drift");+_Static_assert(ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED == 64, "seam-bridge ABI code drift");+_Static_assert(ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE == 65, "boundary-split ABI code drift");+ #if defined(_WIN32) #include <windows.h> 
+ docs/README.md view
@@ -0,0 +1,29 @@+# Moonlight Triangulation exhibits++These are the full-size visual artifacts shipped with `moonlight-triangulation`.+They are derived observations of the package's existing geometry and Homology+owners, not a second visualization data model.++## Persistence rose++![Animated Moonlight persistence rose](./social/moonlight-triangulation-persistence-rose.gif)++[Open the animation directly.](./social/moonlight-triangulation-persistence-rose.gif)+· [Open the static vector poster.](./moonlight-triangulation-persistence-rose.svg)+· [Read its provenance and reproduction commands.](../README.md#persistence-rose)++The GIF carries the source fingerprint of the package-owned 65-frame SVG+section. `moonlight-triangulation-persistence-rose verify-artifacts` checks it+beside byte-exact regeneration of both retained SVGs.++## Alpha eclipse++![Moonlight alpha eclipse](./moonlight-triangulation-alpha-eclipse.svg)++[Open the full-size vector.](./moonlight-triangulation-alpha-eclipse.svg)+· [Read the exact-filtration semantics.](../README.md#exact-alpha-filtration-and-persistent-topology)++This is the real four-site square alpha complex: four resident boundary edges+create one hole at exact `α² = 1`, then the resident diagonal and two resident+faces kill it at exact `α² = 2`. No decorative centre site is smuggled into the+final panel. Geometry is not improv theatre.
+ docs/moonlight-triangulation-alpha-eclipse.svg view
@@ -0,0 +1,91 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 620" width="1440" height="620" role="img" aria-labelledby="title description">+<title id="title">Moonlight Triangulation alpha eclipse</title>+<desc id="description">One fixed four-site Delaunay DCEL at three exact alpha sublevels. Its resident boundary creates one hole at squared radius 1, and its resident diagonal and faces kill that hole at squared radius 2.</desc>+<defs>+<radialGradient id="eclipse-background" cx="50%" cy="38%" r="78%"><stop offset="0" stop-color="#111744"/><stop offset="0.55" stop-color="#070b25"/><stop offset="1" stop-color="#02040f"/></radialGradient>+<linearGradient id="eclipse-corona" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#66efff"/><stop offset="0.48" stop-color="#ff72c6"/><stop offset="1" stop-color="#ffd86b"/></linearGradient>+<linearGradient id="eclipse-face" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7c4dff"/><stop offset="0.5" stop-color="#ff4fa3"/><stop offset="1" stop-color="#ffbf5b"/></linearGradient>+<filter id="eclipse-soft-glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="7" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>+<filter id="eclipse-dot-glow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>+</defs>+<rect width="1440" height="620" fill="url(#eclipse-background)"/>+<circle cx="720" cy="300" r="246" fill="#ff68bb" opacity="0.035" filter="url(#eclipse-soft-glow)"/>+<text x="58" y="58" fill="#84f3ff" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="17" font-weight="600" letter-spacing="6">MOONLIGHT TRIANGULATION</text>+<text x="58" y="101" fill="#f7f3ff" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="38" font-weight="420">ALPHA ECLIPSE</text>+<text x="1382" y="61" text-anchor="end" fill="#9baedb" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.5">ONE FIXED DELAUNAY DCEL · EXACT CRITICAL BIRTHS</text>+<g transform="translate(245.000 292)">+<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#344070" stroke-opacity="0.55"/>+<text x="-162" y="-114" fill="#8fa4d6" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">BEFORE BIRTH</text>+<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>+<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"></g>+<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"></g>+<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"></g>+<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>+<circle cx="-101.823" cy="0.000" r="6"/>+<circle cx="101.823" cy="0.000" r="6"/>+<circle cx="0.000" cy="101.823" r="6"/>+</g>+<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>+<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">four isolated sites</text>+<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 0</text>+<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 0</text>+</g>+<g transform="translate(720.000 292)">+<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#ff6fbd" stroke-opacity="0.72"/>+<text x="-162" y="-114" fill="#ff89cb" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">LOOP BORN</text>+<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>+<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"></g>+<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"><line x1="0.000" y1="-101.823" x2="-101.823" y2="0.000"/>+<line x1="0.000" y1="-101.823" x2="101.823" y2="0.000"/>+<line x1="-101.823" y1="0.000" x2="0.000" y2="101.823"/>+<line x1="101.823" y1="0.000" x2="0.000" y2="101.823"/>+</g>+<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"><path d="M -101.823,0.000 L 0.000,-101.823 L 101.823,0.000 L 0.000,101.823 Z"/>+</g>+<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>+<circle cx="-101.823" cy="0.000" r="6"/>+<circle cx="101.823" cy="0.000" r="6"/>+<circle cx="0.000" cy="101.823" r="6"/>+</g>+<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>+<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">resident boundary closes</text>+<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 1</text>+<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 1</text>+</g>+<g transform="translate(1195.000 292)">+<rect x="-194" y="-150" width="388" height="292" rx="28" fill="#090d26" fill-opacity="0.72" stroke="#344070" stroke-opacity="0.55"/>+<text x="-162" y="-114" fill="#8fa4d6" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="13" letter-spacing="2.4">CYCLE KILLED</text>+<circle r="104" fill="#01020a" stroke="#7fefff" stroke-opacity="0.18" stroke-width="2"/>+<g fill="url(#eclipse-face)" fill-opacity="0.58" stroke="#ffd56d" stroke-opacity="0.48" stroke-width="1.2"><polygon points="0.000,-101.823 0.000,101.823 -101.823,0.000"/>+<polygon points="0.000,101.823 0.000,-101.823 101.823,0.000"/>+</g>+<g fill="none" stroke="url(#eclipse-corona)" stroke-width="5" stroke-linejoin="round" stroke-linecap="round" filter="url(#eclipse-soft-glow)"><line x1="0.000" y1="-101.823" x2="-101.823" y2="0.000"/>+<line x1="0.000" y1="101.823" x2="0.000" y2="-101.823"/>+<line x1="0.000" y1="-101.823" x2="101.823" y2="0.000"/>+<line x1="-101.823" y1="0.000" x2="0.000" y2="101.823"/>+<line x1="101.823" y1="0.000" x2="0.000" y2="101.823"/>+</g>+<g fill="none" stroke="#fff7da" stroke-width="3.2" stroke-linejoin="round" opacity="0.86"></g>+<g fill="#fff8df" stroke="#ff75c5" stroke-width="2" filter="url(#eclipse-dot-glow)"><circle cx="0.000" cy="-101.823" r="6"/>+<circle cx="-101.823" cy="0.000" r="6"/>+<circle cx="101.823" cy="0.000" r="6"/>+<circle cx="0.000" cy="101.823" r="6"/>+</g>+<rect x="-126" y="-18" width="252" height="36" rx="18" fill="#05071a" fill-opacity="0.82"/>+<text y="5" text-anchor="middle" fill="#fff7da" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="14" letter-spacing="1.2">resident faces fill it</text>+<text x="-162" y="121" fill="#fff4cc" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="25">β₁ = 0</text>+<text x="162" y="121" text-anchor="end" fill="#8495c4" font-family="Inter,ui-sans-serif,system-ui,sans-serif" font-size="12">α² = 2</text>+</g>+<g font-family="Inter,ui-sans-serif,system-ui,sans-serif">+<text x="58" y="516" fill="#9badd8" font-size="13" letter-spacing="2.5">ONE EXACT H₁ INTERVAL</text>+<line x1="58" y1="559" x2="1382" y2="559" stroke="#344070" stroke-width="3"/>+<line x1="535" y1="559" x2="1042" y2="559" stroke="#ff72c6" stroke-width="9" stroke-linecap="round"/>+<circle cx="535" cy="559" r="8" fill="#fff6d6" stroke="#ff72c6" stroke-width="3"/>+<rect x="1034" y="551" width="16" height="16" fill="#fff6d6" stroke="#66efff" stroke-width="3"/>+<text x="58" y="589" fill="#7588ba" font-size="12">α² = 0</text>+<text x="535" y="589" text-anchor="middle" fill="#f7f3ff" font-size="12">birth α² = 1</text>+<text x="788" y="548" text-anchor="middle" fill="#fff6d6" font-size="14">the eclipse hole persists</text>+<text x="1042" y="589" text-anchor="middle" fill="#f7f3ff" font-size="12">death α² = 2</text>+<text x="1382" y="589" text-anchor="end" fill="#7588ba" font-size="12">exact α² increases →</text>+</g>+</svg>
+ docs/moonlight-triangulation-persistence-rose.svg view
@@ -0,0 +1,1895 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" width="1920" height="1080" role="img" aria-labelledby="title description">+<title id="title">Moonlight Triangulation Persistence Rose</title>+<desc id="description">Exact Delaunay alpha filtration, persistent homology, Voronoi dual, Sibson interpolation, exact overlay, and Minkowski morphology composed into a seven-petal rose.</desc>+<defs>+<radialGradient id="background" cx="52%" cy="48%" r="72%"><stop offset="0" stop-color="#10143d"/><stop offset="0.48" stop-color="#070b24"/><stop offset="1" stop-color="#02040e"/></radialGradient>+<radialGradient id="rose-wash" cx="48%" cy="42%" r="63%"><stop offset="0" stop-color="#ffe7a3"/><stop offset="0.38" stop-color="#ff5fb7"/><stop offset="0.72" stop-color="#7c4dff"/><stop offset="1" stop-color="#00d9ff"/></radialGradient>+<linearGradient id="mask-stroke" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ffd86b"/><stop offset="0.48" stop-color="#ff72c6"/><stop offset="1" stop-color="#66efff"/></linearGradient>+<linearGradient id="hole-stroke" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff9db"/><stop offset="0.52" stop-color="#ff9ad7"/><stop offset="1" stop-color="#9ff7ff"/></linearGradient>+<filter id="field-soften" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="24"/></filter>+<filter id="hole-glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="7" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>+<filter id="edge-glow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>+<clipPath id="rose-clip"><path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill-rule="evenodd"/></clipPath>+</defs>+<rect width="1920" height="1080" fill="url(#background)"/>+<circle cx="960" cy="552" r="322.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>+<circle cx="960" cy="552" r="356.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>+<circle cx="960" cy="552" r="390.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>+<circle cx="960" cy="552" r="424.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>+<circle cx="960" cy="552" r="458.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>+<circle cx="960" cy="552" r="492.000" fill="none" stroke="#8ba5ff" stroke-width="0.8" stroke-dasharray="2 12" opacity="0.077"/>+<g fill="none" stroke-linecap="round">+<polyline points="1077.000,949.123 1051.592,955.741 1025.816,960.735 999.775,964.085 973.574,965.777 947.318,965.806 921.113,964.170 895.065,960.876 869.278,955.937 843.856,949.374 818.900,941.213 794.513,931.486 770.791,920.233 747.830,907.499 725.722,893.336 704.557,877.799 684.419,860.952 665.389,842.862 647.545,823.602 630.957,803.250 615.692,781.888 601.813,759.600 589.374,736.478 578.426,712.614 569.012,688.104 561.171,663.046 554.934,637.542 550.326,611.694 547.366,585.606 546.066,559.382 546.430,533.129 548.458,506.952 552.141,480.956 557.465,455.245 564.407,429.924 572.941,405.094 583.032,380.854 594.638,357.303 607.714,334.536 622.207,312.642 638.059,291.712 655.206,271.828 673.578,253.071" stroke="hsl(188 92% 72%)" stroke-width="2.600" opacity="0.720"/>+<polyline points="1232.734,230.630 1252.266,248.285 1270.657,267.124 1287.837,287.075 1303.737,308.059 1318.296,329.995 1331.457,352.798 1343.169,376.377 1353.387,400.642 1362.069,425.497 1369.182,450.846 1374.699,476.589 1378.598,502.627 1380.864,528.857 1381.488,555.178 1380.467,581.486 1377.806,607.679 1373.515,633.655 1367.610,659.313 1360.115,684.551 1351.059,709.273 1340.478,733.381 1328.411,756.781 1314.908,779.382 1300.020,801.096 1283.805,821.839 1266.326,841.528 1247.653,860.088 1227.857,877.446 1207.016,893.534 1185.212,908.289 1162.529,921.655 1139.055,933.578 1114.883,944.012 1090.107,952.917 1064.823,960.258 1039.130,966.006 1013.128,970.138 986.919,972.640 960.605,973.500 934.289,972.715 908.073,970.289 882.060,966.231" stroke="hsl(205 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1035.085,974.378 1008.772,978.219 982.273,980.421 955.687,980.978 929.118,979.887 902.668,977.152 876.438,972.783 850.530,966.798 825.041,959.219 800.072,950.075 775.716,939.402 752.069,927.241 729.221,913.638 707.259,898.645 686.268,882.321 666.329,864.728 647.519,845.933 629.908,826.008 613.567,805.031 598.556,783.082 584.933,760.245 572.752,736.608 562.059,712.262 552.894,687.300 545.294,661.818 539.287,635.914 534.896,609.688 532.139,583.240 531.025,556.672 531.560,530.086 533.741,503.585 537.559,477.269 543.001,451.240 550.044,425.599 558.663,400.443 568.823,375.870 580.487,351.973 593.608,328.845 608.138,306.574 624.019,285.246 641.191,264.943 659.588,245.743 679.139,227.719" stroke="hsl(222 92% 72%)" stroke-width="2.600" opacity="0.720"/>+<polyline points="1040.714,980.973 1014.190,985.123 987.460,987.635 960.627,988.500 933.791,987.712 907.055,985.277 880.518,981.203 854.283,975.505 828.447,968.204 803.109,959.330 778.365,948.914 754.307,936.997 731.028,923.624 708.615,908.844 687.153,892.715 666.723,875.297 647.403,856.656 629.265,836.862 612.378,815.991 596.806,794.121 582.608,771.335 569.838,747.719 558.544,723.363 548.769,698.359 540.549,672.801 533.916,646.786 528.894,620.413 525.504,593.780 523.757,566.990 523.661,540.143 525.215,513.341 528.414,486.685 533.246,460.276 539.692,434.214 547.728,408.598 557.324,383.524 568.443,359.087 581.043,335.381 595.077,312.493 610.491,290.512 627.228,269.520 645.223,249.597 664.409,230.817" stroke="hsl(239 92% 72%)" stroke-width="2.600" opacity="0.720"/>+<polyline points="1076.891,980.337 1050.961,986.583 1024.703,991.260 998.212,994.353 971.583,995.849 944.912,995.744 918.295,994.037 891.829,990.735 865.609,985.851 839.730,979.400 814.284,971.408 789.365,961.902 765.061,950.917 741.460,938.492 718.648,924.673 696.707,909.509 675.716,893.056 655.751,875.371 636.884,856.519 619.183,836.569 602.711,815.592 587.529,793.664 573.691,770.863 561.247,747.273 550.241,722.979 540.714,698.067 532.701,672.628 526.229,646.754 521.322,620.538 517.998,594.075 516.269,567.460 516.142,540.789 517.616,514.159 520.686,487.665 525.341,461.403 531.565,435.468 539.335,409.954 548.623,384.952 559.395,360.553 571.613,336.845 585.232,313.913 600.204,291.841 616.474,270.707" stroke="hsl(256 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1052.392,993.946 1026.117,998.633 999.611,1001.759 972.967,1003.314 946.277,1003.291 919.635,1001.692 893.135,998.521 866.868,993.790 840.926,987.515 815.401,979.719 790.381,970.428 765.954,959.674 742.204,947.496 719.216,933.936 697.069,919.042 675.841,902.864 655.606,885.461 636.434,866.893 618.393,847.224 601.546,826.523 585.952,804.864 571.664,782.320 558.734,758.972 547.205,734.901 537.119,710.190 528.511,684.927 521.411,659.199 515.843,633.097 511.827,606.711 509.377,580.134 508.502,553.459 509.205,526.779 511.483,500.186 515.328,473.775 520.727,447.637 527.661,421.864 536.106,396.546 546.032,371.771 557.404,347.625 570.184,324.194 584.325,301.559 599.780,279.799 616.493,258.990" stroke="hsl(273 92% 72%)" stroke-width="2.600" opacity="0.720"/>+<polyline points="1071.913,997.148 1045.954,1002.880 1019.707,1007.100 993.260,1009.793 966.702,1010.951 940.120,1010.569 913.606,1008.649 887.247,1005.198 861.132,1000.226 835.349,993.750 809.984,985.793 785.123,976.381 760.847,965.545 737.240,953.322 714.380,939.752 692.344,924.883 671.206,908.762 651.036,891.444 631.903,872.988 613.871,853.456 596.999,832.912 581.345,811.426 566.962,789.069 553.896,765.918 542.193,742.048 531.891,717.542 523.026,692.480 515.626,666.947 509.717,641.028 505.318,614.810 502.444,588.382 501.105,561.832 501.306,535.249 503.045,508.722 506.317,482.341 511.110,456.192 517.410,430.366 525.194,404.947 534.436,380.022 545.106,355.673 557.168,331.983 570.581,309.031 585.300,286.894" stroke="hsl(290 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1052.848,955.454 1029.335,960.153 1005.590,963.482 981.692,965.431 957.721,965.994 933.757,965.167 909.882,962.955 886.175,959.365 862.715,954.407 839.582,948.100 816.853,940.465 794.604,931.526 772.909,921.314 751.843,909.864 731.474,897.213 711.872,883.404 693.102,868.483 675.228,852.501 658.309,835.511 642.402,817.570 627.560,798.738 613.833,779.078 601.267,758.657 589.905,737.542 579.784,715.805 570.939,693.519 563.399,670.758 557.189,647.598 552.330,624.118 548.838,600.396 546.726,576.512 546.000,552.545 546.663,528.577 548.712,504.687 552.141,480.956 556.938,457.463 563.087,434.287 570.568,411.506 579.354,389.197 589.418,367.433 600.725,346.289 613.236,325.834 626.911,306.139" stroke="hsl(307 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1245.433,241.855 1255.448,251.380 1265.146,261.228 1274.515,271.389 1283.546,281.852 1292.229,292.605 1300.555,303.637 1308.515,314.936 1316.100,326.490 1323.303,338.286 1330.114,350.313 1336.528,362.556 1342.536,375.003 1348.134,387.640 1353.314,400.454 1358.071,413.430 1362.400,426.556 1366.297,439.817 1369.757,453.198 1372.776,466.686 1375.351,480.265 1377.479,493.921 1379.159,507.640 1380.388,521.407 1381.165,535.206 1381.489,549.024 1381.360,562.844 1380.778,576.654 1379.744,590.436 1378.258,604.177 1376.322,617.862 1373.939,631.477 1371.111,645.006 1367.841,658.434 1364.132,671.749 1359.988,684.934 1355.415,697.977 1350.416,710.863 1344.998,723.578 1339.165,736.108 1332.925,748.441 1326.284,760.562 1319.249,772.459" stroke="hsl(202 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1302.579,293.774 1309.377,303.049 1315.925,312.504 1322.217,322.130 1328.248,331.922 1334.015,341.872 1339.513,351.973 1344.739,362.218 1349.687,372.599 1354.356,383.109 1358.742,393.740 1362.841,404.486 1366.650,415.337 1370.167,426.286 1373.389,437.326 1376.315,448.448 1378.941,459.645 1381.266,470.907 1383.288,482.229 1385.006,493.600 1386.419,505.013 1387.525,516.460 1388.324,527.933 1388.816,539.423 1388.999,550.922 1388.873,562.422 1388.440,573.914 1387.699,585.390 1386.650,596.843 1385.295,608.263 1383.634,619.643 1381.668,630.974 1379.400,642.248 1376.830,653.458 1373.961,664.595 1370.794,675.651 1367.332,686.617 1363.577,697.488 1359.532,708.253 1355.200,718.907 1350.584,729.440 1345.688,739.846 1340.514,750.117" stroke="hsl(219 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1313.780,296.320 1320.034,305.205 1326.065,314.243 1331.870,323.428 1337.444,332.755 1342.784,342.218 1347.887,351.811 1352.750,361.528 1357.369,371.363 1361.743,381.310 1365.867,391.362 1369.739,401.514 1373.358,411.760 1376.721,422.092 1379.825,432.505 1382.669,442.992 1385.252,453.546 1387.571,464.161 1389.624,474.831 1391.412,485.549 1392.933,496.307 1394.185,507.101 1395.168,517.922 1395.881,528.764 1396.324,539.621 1396.497,550.485 1396.400,561.350 1396.032,572.210 1395.394,583.057 1394.486,593.884 1393.309,604.686 1391.863,615.455 1390.150,626.185 1388.170,636.869 1385.925,647.500 1383.416,658.072 1380.644,668.578 1377.612,679.012 1374.322,689.368 1370.774,699.638 1366.972,709.817 1362.918,719.898 1358.614,729.875" stroke="hsl(236 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1356.866,352.918 1360.834,361.033 1364.636,369.227 1368.270,377.497 1371.736,385.839 1375.031,394.250 1378.155,402.726 1381.105,411.264 1383.881,419.860 1386.481,428.511 1388.905,437.213 1391.152,445.962 1393.220,454.756 1395.109,463.589 1396.817,472.459 1398.345,481.362 1399.691,490.295 1400.856,499.253 1401.838,508.233 1402.637,517.230 1403.252,526.243 1403.685,535.266 1403.933,544.295 1403.998,553.328 1403.879,562.361 1403.576,571.389 1403.090,580.409 1402.421,589.418 1401.568,598.411 1400.532,607.384 1399.314,616.335 1397.914,625.259 1396.333,634.153 1394.572,643.013 1392.630,651.835 1390.510,660.616 1388.211,669.352 1385.735,678.039 1383.083,686.674 1380.255,695.254 1377.254,703.774 1374.080,712.231 1370.734,720.622" stroke="hsl(253 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1343.627,313.919 1348.226,321.494 1352.675,329.158 1356.973,336.907 1361.118,344.740 1365.108,352.652 1368.943,360.641 1372.619,368.704 1376.137,376.837 1379.495,385.038 1382.691,393.304 1385.724,401.630 1388.593,410.014 1391.298,418.453 1393.835,426.944 1396.206,435.482 1398.409,444.066 1400.443,452.691 1402.307,461.354 1404.001,470.053 1405.524,478.783 1406.875,487.541 1408.054,496.324 1409.060,505.128 1409.894,513.950 1410.554,522.787 1411.041,531.636 1411.353,540.492 1411.492,549.352 1411.457,558.214 1411.248,567.073 1410.866,575.926 1410.309,584.771 1409.579,593.602 1408.676,602.418 1407.600,611.214 1406.352,619.987 1404.932,628.734 1403.340,637.452 1401.577,646.136 1399.645,654.785 1397.543,663.394 1395.272,671.959" stroke="hsl(270 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1374.328,354.482 1377.670,361.652 1380.889,368.878 1383.982,376.158 1386.949,383.491 1389.790,390.873 1392.503,398.304 1395.087,405.780 1397.542,413.299 1399.867,420.860 1402.062,428.460 1404.125,436.096 1406.057,443.767 1407.856,451.470 1409.522,459.203 1411.054,466.963 1412.453,474.749 1413.717,482.558 1414.846,490.387 1415.840,498.234 1416.699,506.098 1417.422,513.975 1418.010,521.863 1418.461,529.761 1418.776,537.665 1418.955,545.573 1418.998,553.483 1418.904,561.393 1418.674,569.300 1418.308,577.202 1417.805,585.096 1417.167,592.980 1416.393,600.853 1415.483,608.710 1414.438,616.551 1413.258,624.373 1411.944,632.173 1410.495,639.950 1408.913,647.700 1407.197,655.422 1405.348,663.113 1403.367,670.771 1401.254,678.394" stroke="hsl(287 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1324.284,355.295 1327.575,361.514 1330.759,367.788 1333.835,374.115 1336.804,380.494 1339.665,386.922 1342.415,393.397 1345.055,399.919 1347.584,406.484 1350.001,413.092 1352.305,419.740 1354.496,426.425 1356.573,433.147 1358.535,439.904 1360.383,446.693 1362.115,453.512 1363.730,460.359 1365.229,467.234 1366.611,474.132 1367.876,481.053 1369.022,487.995 1370.051,494.955 1370.961,501.931 1371.753,508.922 1372.425,515.926 1372.979,522.940 1373.413,529.962 1373.728,536.990 1373.923,544.023 1373.999,551.059 1373.955,558.094 1373.792,565.128 1373.509,572.158 1373.107,579.182 1372.585,586.198 1371.944,593.205 1371.185,600.199 1370.306,607.180 1369.309,614.144 1368.194,621.091 1366.961,628.018 1365.610,634.923 1364.143,641.804" stroke="hsl(304 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="546.368,470.939 547.596,464.908 548.911,458.896 550.314,452.904 551.805,446.933 553.383,440.984 555.047,435.059 556.797,429.159 558.634,423.286 560.556,417.439 562.563,411.621 564.655,405.834 566.831,400.077 569.091,394.353 571.435,388.662 573.861,383.006 576.369,377.387 578.960,371.804 581.631,366.260 584.384,360.755 587.216,355.292 590.128,349.870 593.118,344.491 596.187,339.157 599.333,333.867 602.557,328.625 605.856,323.430 609.231,318.283 612.681,313.187 616.205,308.141 619.802,303.148 623.471,298.207 627.213,293.321 631.025,288.490 634.907,283.715 638.859,278.997 642.879,274.337 646.967,269.737 651.122,265.196 655.342,260.717 659.627,256.300 663.977,251.946 668.389,247.656" stroke="hsl(199 92% 72%)" stroke-width="2.600" opacity="0.720"/>+<polyline points="536.712,482.229 537.739,476.258 538.851,470.303 540.047,464.363 541.326,458.442 542.689,452.539 544.135,446.655 545.664,440.793 547.276,434.953 548.970,429.136 550.746,423.344 552.604,417.578 554.543,411.838 556.563,406.126 558.663,400.443 560.843,394.791 563.103,389.170 565.442,383.581 567.860,378.026 570.356,372.506 572.929,367.021 575.580,361.573 578.307,356.164 581.111,350.793 583.990,345.463 586.944,340.173 589.973,334.926 593.075,329.722 596.251,324.563 599.499,319.449 602.819,314.381 606.210,309.361 609.672,304.389 613.204,299.466 616.804,294.594 620.474,289.773 624.211,285.005 628.014,280.290 631.885,275.628 635.820,271.022 639.820,266.472 643.884,261.979 648.012,257.544" stroke="hsl(216 92% 72%)" stroke-width="2.600" opacity="0.720"/>+<polyline points="1391.710,616.489 1390.790,622.373 1389.790,628.243 1388.710,634.100 1387.550,639.941 1386.310,645.766 1384.991,651.573 1383.593,657.362 1382.116,663.132 1380.561,668.880 1378.927,674.607 1377.215,680.311 1375.426,685.991 1373.559,691.647 1371.615,697.276 1369.595,702.878 1367.498,708.452 1365.326,713.997 1363.078,719.512 1360.755,724.995 1358.358,730.447 1355.886,735.865 1353.341,741.249 1350.722,746.598 1348.031,751.910 1345.268,757.186 1342.432,762.423 1339.526,767.621 1336.549,772.779 1333.502,777.895 1330.385,782.970 1327.199,788.002 1323.946,792.989 1320.624,797.932 1317.235,802.829 1313.780,807.680 1310.258,812.483 1306.672,817.237 1303.021,821.942 1299.306,826.597 1295.528,831.200 1291.688,835.752 1287.786,840.251" stroke="hsl(233 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="521.911,479.789 522.937,473.819 524.045,467.863 525.233,461.923 526.503,456.000 527.853,450.095 529.283,444.209 530.794,438.342 532.384,432.497 534.055,426.674 535.804,420.875 537.632,415.100 539.540,409.350 541.525,403.627 543.588,397.931 545.729,392.265 547.947,386.628 550.241,381.021 552.612,375.447 555.059,369.905 557.581,364.398 560.178,358.925 562.849,353.488 565.594,348.088 568.413,342.726 571.304,337.403 574.268,332.120 577.304,326.878 580.411,321.678 583.589,316.521 586.836,311.407 590.154,306.338 593.540,301.315 596.994,296.339 600.516,291.410 604.104,286.530 607.759,281.699 611.480,276.919 615.265,272.190 619.115,267.512 623.028,262.888 627.003,258.318 631.041,253.802" stroke="hsl(250 92% 72%)" stroke-width="2.600" opacity="0.720"/>+<polyline points="1394.664,429.854 1396.010,434.752 1397.302,439.664 1398.538,444.591 1399.718,449.531 1400.843,454.484 1401.913,459.450 1402.926,464.427 1403.883,469.415 1404.784,474.414 1405.628,479.423 1406.417,484.440 1407.149,489.467 1407.824,494.501 1408.442,499.542 1409.004,504.590 1409.509,509.645 1409.957,514.704 1410.348,519.768 1410.682,524.837 1410.959,529.908 1411.179,534.983 1411.342,540.060 1411.448,545.138 1411.496,550.217 1411.488,555.296 1411.422,560.375 1411.300,565.453 1411.120,570.529 1410.883,575.603 1410.589,580.673 1410.238,585.741 1409.829,590.804 1409.364,595.861 1408.843,600.914 1408.264,605.960 1407.629,611.000 1406.936,616.031 1406.188,621.055 1405.383,626.070 1404.521,631.076 1403.604,636.072 1402.630,641.057" stroke="hsl(267 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1231.657,182.022 1235.714,185.035 1239.737,188.093 1243.727,191.195 1247.682,194.341 1251.603,197.530 1255.488,200.763 1259.337,204.037 1263.150,207.354 1266.927,210.713 1270.666,214.113 1274.367,217.554 1278.031,221.035 1281.655,224.557 1285.241,228.118 1288.787,231.719 1292.294,235.359 1295.760,239.036 1299.185,242.752 1302.570,246.505 1305.913,250.296 1309.214,254.123 1312.472,257.986 1315.688,261.884 1318.861,265.818 1321.990,269.787 1325.075,273.789 1328.116,277.826 1331.113,281.896 1334.064,285.998 1336.970,290.133 1339.831,294.299 1342.645,298.497 1345.413,302.725 1348.134,306.984 1350.808,311.272 1353.435,315.590 1356.014,319.936 1358.545,324.310 1361.028,328.712 1363.462,333.141 1365.847,337.597 1368.184,342.078" stroke="hsl(284 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1265.330,831.588 1262.369,834.788 1259.374,837.957 1256.346,841.094 1253.285,844.199 1250.191,847.271 1247.065,850.311 1243.908,853.318 1240.719,856.291 1237.498,859.231 1234.247,862.136 1230.966,865.007 1227.654,867.843 1224.313,870.645 1220.943,873.411 1217.543,876.141 1214.115,878.835 1210.659,881.494 1207.175,884.115 1203.664,886.700 1200.125,889.248 1196.560,891.758 1192.969,894.230 1189.352,896.665 1185.709,899.061 1182.041,901.419 1178.349,903.738 1174.633,906.018 1170.892,908.259 1167.129,910.460 1163.342,912.622 1159.533,914.743 1155.702,916.825 1151.848,918.865 1147.974,920.865 1144.079,922.825 1140.163,924.743 1136.228,926.619 1132.273,928.455 1128.298,930.248 1124.306,932.000 1120.294,933.709 1116.266,935.376" stroke="hsl(301 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1369.037,450.258 1370.062,454.474 1371.044,458.699 1371.983,462.935 1372.877,467.180 1373.729,471.434 1374.536,475.696 1375.299,479.967 1376.019,484.245 1376.694,488.530 1377.325,492.822 1377.912,497.121 1378.455,501.425 1378.953,505.734 1379.407,510.049 1379.817,514.368 1380.182,518.691 1380.502,523.017 1380.778,527.346 1381.010,531.679 1381.197,536.013 1381.339,540.349 1381.437,544.686 1381.489,549.024 1381.498,553.362 1381.461,557.700 1381.380,562.038 1381.255,566.374 1381.085,570.709 1380.870,575.042 1380.610,579.372 1380.306,583.700 1379.958,588.024 1379.565,592.345 1379.127,596.661 1378.645,600.972 1378.119,605.278 1377.549,609.579 1376.934,613.873 1376.275,618.161 1375.572,622.442 1374.825,626.716 1374.034,630.981" stroke="hsl(196 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="543.685,448.448 544.773,444.169 545.905,439.901 547.081,435.645 548.300,431.401 549.563,427.170 550.870,422.952 552.219,418.748 553.613,414.558 555.049,410.383 556.528,406.223 558.049,402.078 559.614,397.949 561.220,393.836 562.869,389.740 564.561,385.661 566.293,381.600 568.068,377.557 569.884,373.532 571.742,369.527 573.640,365.540 575.580,361.573 577.560,357.627 579.581,353.701 581.642,349.796 583.743,345.913 585.884,342.051 588.065,338.212 590.285,334.395 592.544,330.601 594.842,326.831 597.179,323.085 599.555,319.363 601.968,315.665 604.419,311.993 606.909,308.346 609.435,304.725 611.999,301.130 614.599,297.561 617.236,294.020 619.909,290.505 622.619,287.019 625.364,283.561" stroke="hsl(213 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1221.864,202.773 1225.279,205.360 1228.668,207.980 1232.031,210.633 1235.368,213.319 1238.678,216.038 1241.962,218.789 1245.218,221.572 1248.447,224.387 1251.648,227.233 1254.821,230.111 1257.966,233.020 1261.082,235.959 1264.169,238.929 1267.227,241.929 1270.255,244.959 1273.253,248.018 1276.221,251.107 1279.159,254.225 1282.066,257.371 1284.941,260.546 1287.786,263.749 1290.599,266.980 1293.380,270.238 1296.129,273.523 1298.846,276.835 1301.530,280.174 1304.181,283.538 1306.799,286.929 1309.384,290.345 1311.935,293.786 1314.452,297.252 1316.935,300.743 1319.383,304.258 1321.797,307.797 1324.176,311.359 1326.520,314.945 1328.829,318.553 1331.102,322.184 1333.339,325.837 1335.541,329.511 1337.706,333.207 1339.835,336.925" stroke="hsl(230 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="522.660,475.389 523.432,471.100 524.247,466.820 525.104,462.548 526.003,458.284 526.944,454.030 527.926,449.785 528.950,445.549 530.015,441.324 531.122,437.110 532.270,432.907 533.460,428.715 534.690,424.535 535.961,420.367 537.274,416.212 538.626,412.070 540.020,407.942 541.454,403.827 542.928,399.727 544.443,395.641 545.997,391.571 547.591,387.516 549.225,383.476 550.899,379.453 552.612,375.447 554.364,371.457 556.156,367.485 557.986,363.531 559.855,359.595 561.762,355.677 563.708,351.779 565.692,347.899 567.714,344.040 569.774,340.200 571.871,336.381 574.006,332.582 576.177,328.805 578.386,325.049 580.632,321.315 582.914,317.603 585.232,313.913 587.587,310.247 589.977,306.604" stroke="hsl(247 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="516.078,469.628 516.907,465.275 517.780,460.931 518.695,456.596 519.652,452.269 520.652,447.953 521.694,443.646 522.779,439.350 523.905,435.065 525.074,430.791 526.284,426.529 527.537,422.278 528.831,418.041 530.166,413.816 531.543,409.604 532.961,405.406 534.420,401.223 535.920,397.053 537.461,392.899 539.043,388.760 540.665,384.637 542.328,380.530 544.030,376.439 545.773,372.365 547.556,368.309 549.379,364.270 551.241,360.250 553.142,356.248 555.083,352.264 557.062,348.300 559.081,344.356 561.138,340.431 563.233,336.527 565.367,332.644 567.539,328.782 569.748,324.941 571.995,321.122 574.280,317.326 576.601,313.552 578.960,309.801 581.355,306.073 583.787,302.369 586.255,298.689" stroke="hsl(264 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1403.110,432.274 1404.208,436.415 1405.268,440.567 1406.289,444.728 1407.271,448.899 1408.214,453.079 1409.118,457.267 1409.982,461.464 1410.808,465.668 1411.594,469.880 1412.341,474.099 1413.049,478.325 1413.717,482.558 1414.345,486.796 1414.934,491.040 1415.483,495.290 1415.993,499.544 1416.463,503.803 1416.893,508.066 1417.283,512.333 1417.633,516.603 1417.944,520.877 1418.214,525.153 1418.445,529.432 1418.636,533.712 1418.786,537.994 1418.897,542.278 1418.968,546.562 1418.999,550.846 1418.989,555.131 1418.940,559.416 1418.851,563.699 1418.722,567.982 1418.552,572.264 1418.343,576.543 1418.094,580.821 1417.805,585.096 1417.476,589.368 1417.108,593.637 1416.699,597.902 1416.251,602.163 1415.762,606.420 1415.235,610.672" stroke="hsl(281 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1362.256,649.911 1361.324,653.661 1360.358,657.403 1359.356,661.136 1358.320,664.859 1357.249,668.572 1356.144,672.276 1355.004,675.968 1353.829,679.650 1352.620,683.321 1351.377,686.980 1350.100,690.628 1348.789,694.263 1347.444,697.887 1346.066,701.497 1344.653,705.094 1343.207,708.678 1341.728,712.249 1340.216,715.805 1338.670,719.347 1337.091,722.875 1335.480,726.387 1333.835,729.885 1332.159,733.367 1330.449,736.833 1328.708,740.283 1326.934,743.717 1325.129,747.134 1323.291,750.534 1321.422,753.916 1319.521,757.281 1317.589,760.628 1315.626,763.957 1313.632,767.268 1311.607,770.560 1309.552,773.832 1307.466,777.086 1305.349,780.319 1303.203,783.533 1301.027,786.727 1298.821,789.900 1296.585,793.053 1294.320,796.184" stroke="hsl(298 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1216.243,217.333 1218.878,219.367 1221.497,221.423 1224.100,223.499 1226.687,225.595 1229.257,227.712 1231.810,229.848 1234.346,232.005 1236.865,234.182 1239.367,236.379 1241.851,238.596 1244.318,240.832 1246.767,243.087 1249.198,245.362 1251.611,247.656 1254.006,249.969 1256.382,252.300 1258.740,254.651 1261.080,257.020 1263.400,259.407 1265.702,261.813 1267.984,264.237 1270.248,266.678 1272.492,269.138 1274.716,271.615 1276.921,274.109 1279.106,276.621 1281.272,279.151 1283.417,281.697 1285.542,284.260 1287.646,286.839 1289.731,289.436 1291.794,292.048 1293.837,294.677 1295.859,297.322 1297.860,299.983 1299.841,302.660 1301.799,305.352 1303.737,308.059 1305.653,310.782 1307.548,313.520 1309.421,316.272 1311.272,319.040" stroke="hsl(193 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1199.794,196.276 1202.512,198.123 1205.215,199.991 1207.904,201.879 1210.579,203.789 1213.239,205.718 1215.884,207.668 1218.513,209.638 1221.128,211.628 1223.727,213.638 1226.311,215.668 1228.880,217.718 1231.432,219.787 1233.969,221.876 1236.489,223.984 1238.994,226.111 1241.482,228.258 1243.953,230.423 1246.408,232.607 1248.846,234.811 1251.267,237.032 1253.671,239.272 1256.057,241.531 1258.427,243.808 1260.779,246.103 1263.113,248.415 1265.429,250.746 1267.728,253.094 1270.008,255.460 1272.270,257.843 1274.514,260.244 1276.740,262.661 1278.947,265.096 1281.135,267.547 1283.304,270.015 1285.454,272.500 1287.586,275.001 1289.698,277.518 1291.790,280.051 1293.864,282.601 1295.917,285.166 1297.951,287.746 1299.965,290.343" stroke="hsl(210 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1138.876,153.835 1141.825,155.173 1144.765,156.533 1147.694,157.915 1150.613,159.318 1153.522,160.743 1156.419,162.190 1159.306,163.658 1162.182,165.148 1165.047,166.659 1167.901,168.191 1170.743,169.744 1173.574,171.318 1176.392,172.914 1179.199,174.530 1181.994,176.166 1184.777,177.824 1187.547,179.502 1190.305,181.201 1193.050,182.920 1195.782,184.659 1198.501,186.419 1201.207,188.199 1203.900,189.999 1206.579,191.818 1209.245,193.658 1211.897,195.517 1214.535,197.396 1217.160,199.294 1219.770,201.212 1222.365,203.149 1224.947,205.106 1227.513,207.081 1230.065,209.076 1232.602,211.089 1235.124,213.121 1237.631,215.172 1240.123,217.241 1242.599,219.329 1245.060,221.435 1247.505,223.559 1249.934,225.702 1252.347,227.862" stroke="hsl(227 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="516.618,528.577 516.804,525.288 517.015,522.000 517.249,518.714 517.509,515.430 517.792,512.147 518.100,508.867 518.432,505.589 518.789,502.314 519.170,499.042 519.575,495.772 520.004,492.506 520.458,489.243 520.935,485.983 521.437,482.727 521.963,479.475 522.514,476.226 523.088,472.982 523.686,469.742 524.309,466.507 524.955,463.277 525.625,460.051 526.320,456.831 527.038,453.615 527.780,450.405 528.545,447.201 529.335,444.002 530.148,440.810 530.985,437.623 531.845,434.443 532.729,431.269 533.637,428.102 534.568,424.942 535.523,421.789 536.500,418.643 537.502,415.504 538.526,412.373 539.574,409.249 540.644,406.134 541.738,403.026 542.855,399.927 543.995,396.836 545.158,393.753" stroke="hsl(244 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="509.391,523.650 509.614,520.307 509.861,516.966 510.134,513.627 510.431,510.290 510.753,506.956 511.099,503.623 511.470,500.294 511.867,496.967 512.287,493.643 512.733,490.323 513.202,487.006 513.697,483.692 514.216,480.383 514.760,477.077 515.328,473.775 515.921,470.478 516.538,467.185 517.179,463.897 517.845,460.614 518.536,457.335 519.250,454.062 519.989,450.794 520.752,447.532 521.539,444.276 522.351,441.025 523.186,437.781 524.046,434.543 524.929,431.311 525.837,428.087 526.768,424.868 527.723,421.657 528.702,418.453 529.705,415.257 530.732,412.068 531.782,408.886 532.856,405.713 533.953,402.548 535.073,399.390 536.218,396.242 537.385,393.101 538.576,389.970 539.790,386.847" stroke="hsl(261 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1418.477,573.910 1418.314,577.092 1418.128,580.273 1417.921,583.452 1417.692,586.630 1417.440,589.806 1417.167,592.980 1416.871,596.153 1416.554,599.323 1416.214,602.491 1415.853,605.656 1415.470,608.819 1415.064,611.980 1414.637,615.137 1414.188,618.291 1413.717,621.442 1413.224,624.590 1412.709,627.734 1412.172,630.875 1411.614,634.012 1411.034,637.145 1410.432,640.273 1409.808,643.398 1409.163,646.518 1408.496,649.633 1407.808,652.744 1407.097,655.850 1406.366,658.951 1405.613,662.047 1404.838,665.137 1404.042,668.222 1403.225,671.302 1402.386,674.376 1401.526,677.443 1400.644,680.505 1399.742,683.561 1398.818,686.610 1397.873,689.653 1396.907,692.689 1395.920,695.718 1394.912,698.741 1393.883,701.756 1392.833,704.764" stroke="hsl(278 92% 72%)" stroke-width="1.100" opacity="0.160"/>+<polyline points="1238.820,858.032 1236.689,859.960 1234.544,861.873 1232.387,863.772 1230.216,865.655 1228.032,867.523 1225.836,869.376 1223.626,871.213 1221.404,873.036 1219.169,874.842 1216.922,876.634 1214.663,878.409 1212.391,880.169 1210.107,881.913 1207.811,883.641 1205.503,885.353 1203.183,887.049 1200.851,888.729 1198.508,890.393 1196.153,892.041 1193.787,893.672 1191.410,895.286 1189.022,896.884 1186.622,898.466 1184.212,900.030 1181.790,901.578 1179.359,903.109 1176.916,904.624 1174.463,906.121 1172.000,907.601 1169.526,909.064 1167.043,910.510 1164.549,911.938 1162.046,913.349 1159.533,914.743 1157.010,916.120 1154.478,917.478 1151.936,918.819 1149.385,920.143 1146.825,921.449 1144.256,922.737 1141.679,924.007 1139.092,925.259" stroke="hsl(295 92% 72%)" stroke-width="1.100" opacity="0.160"/>+</g>+<g clip-path="url(#rose-clip)">+<path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill="url(#rose-wash)" opacity="0.28"/>+<g filter="url(#field-soften)"><circle cx="925.660" cy="139.920" r="35.000" fill="hsl(271 94% 61%)" opacity="0.268"/>+<circle cx="960.000" cy="139.920" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>+<circle cx="994.340" cy="139.920" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="856.980" cy="174.260" r="35.000" fill="hsl(258 94% 61%)" opacity="0.251"/>+<circle cx="891.320" cy="174.260" r="35.000" fill="hsl(266 94% 61%)" opacity="0.262"/>+<circle cx="925.660" cy="174.260" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>+<circle cx="960.000" cy="174.260" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="994.340" cy="174.260" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>+<circle cx="1028.680" cy="174.260" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="1063.020" cy="174.260" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>+<circle cx="788.300" cy="208.600" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>+<circle cx="822.640" cy="208.600" r="35.000" fill="hsl(252 94% 61%)" opacity="0.243"/>+<circle cx="856.980" cy="208.600" r="35.000" fill="hsl(257 94% 61%)" opacity="0.250"/>+<circle cx="891.320" cy="208.600" r="35.000" fill="hsl(265 94% 61%)" opacity="0.260"/>+<circle cx="925.660" cy="208.600" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>+<circle cx="960.000" cy="208.600" r="35.000" fill="hsl(249 94% 61%)" opacity="0.241"/>+<circle cx="994.340" cy="208.600" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>+<circle cx="1028.680" cy="208.600" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>+<circle cx="1063.020" cy="208.600" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>+<circle cx="1097.360" cy="208.600" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>+<circle cx="1131.700" cy="208.600" r="35.000" fill="hsl(243 94% 61%)" opacity="0.233"/>+<circle cx="719.620" cy="242.940" r="35.000" fill="hsl(238 94% 61%)" opacity="0.225"/>+<circle cx="753.960" cy="242.940" r="35.000" fill="hsl(238 94% 61%)" opacity="0.227"/>+<circle cx="788.300" cy="242.940" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>+<circle cx="822.640" cy="242.940" r="35.000" fill="hsl(245 94% 61%)" opacity="0.235"/>+<circle cx="856.980" cy="242.940" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>+<circle cx="891.320" cy="242.940" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>+<circle cx="925.660" cy="242.940" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>+<circle cx="960.000" cy="242.940" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>+<circle cx="994.340" cy="242.940" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>+<circle cx="1028.680" cy="242.940" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>+<circle cx="1063.020" cy="242.940" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>+<circle cx="1097.360" cy="242.940" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>+<circle cx="1131.700" cy="242.940" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>+<circle cx="1166.040" cy="242.940" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>+<circle cx="1200.380" cy="242.940" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>+<circle cx="650.940" cy="277.280" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>+<circle cx="685.280" cy="277.280" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>+<circle cx="719.620" cy="277.280" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>+<circle cx="753.960" cy="277.280" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>+<circle cx="788.300" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>+<circle cx="822.640" cy="277.280" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>+<circle cx="856.980" cy="277.280" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>+<circle cx="891.320" cy="277.280" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>+<circle cx="925.660" cy="277.280" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>+<circle cx="960.000" cy="277.280" r="35.000" fill="hsl(249 94% 61%)" opacity="0.240"/>+<circle cx="994.340" cy="277.280" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>+<circle cx="1028.680" cy="277.280" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="1063.020" cy="277.280" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>+<circle cx="1097.360" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>+<circle cx="1131.700" cy="277.280" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="1166.040" cy="277.280" r="35.000" fill="hsl(226 94% 61%)" opacity="0.211"/>+<circle cx="1200.380" cy="277.280" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="1234.720" cy="277.280" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>+<circle cx="1269.060" cy="277.280" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>+<circle cx="616.600" cy="311.620" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>+<circle cx="650.940" cy="311.620" r="35.000" fill="hsl(229 94% 61%)" opacity="0.214"/>+<circle cx="685.280" cy="311.620" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>+<circle cx="719.620" cy="311.620" r="35.000" fill="hsl(213 94% 61%)" opacity="0.195"/>+<circle cx="753.960" cy="311.620" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>+<circle cx="788.300" cy="311.620" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>+<circle cx="822.640" cy="311.620" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="856.980" cy="311.620" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>+<circle cx="891.320" cy="311.620" r="34.000" fill="hsl(248 94% 61%)" opacity="0.239"/>+<circle cx="925.660" cy="311.620" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="960.000" cy="311.620" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="994.340" cy="311.620" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>+<circle cx="1028.680" cy="311.620" r="35.000" fill="hsl(249 94% 61%)" opacity="0.239"/>+<circle cx="1063.020" cy="311.620" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>+<circle cx="1097.360" cy="311.620" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>+<circle cx="1131.700" cy="311.620" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>+<circle cx="1166.040" cy="311.620" r="35.000" fill="hsl(224 94% 61%)" opacity="0.208"/>+<circle cx="1200.380" cy="311.620" r="35.000" fill="hsl(225 94% 61%)" opacity="0.209"/>+<circle cx="1234.720" cy="311.620" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>+<circle cx="1269.060" cy="311.620" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>+<circle cx="1303.400" cy="311.620" r="35.000" fill="hsl(263 94% 61%)" opacity="0.258"/>+<circle cx="616.600" cy="345.960" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>+<circle cx="650.940" cy="345.960" r="35.000" fill="hsl(225 94% 61%)" opacity="0.209"/>+<circle cx="685.280" cy="345.960" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>+<circle cx="719.620" cy="345.960" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>+<circle cx="753.960" cy="345.960" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>+<circle cx="788.300" cy="345.960" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>+<circle cx="822.640" cy="345.960" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>+<circle cx="856.980" cy="345.960" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>+<circle cx="891.320" cy="345.960" r="34.000" fill="hsl(244 94% 61%)" opacity="0.234"/>+<circle cx="925.660" cy="345.960" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="960.000" cy="345.960" r="35.000" fill="hsl(256 94% 61%)" opacity="0.250"/>+<circle cx="994.340" cy="345.960" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>+<circle cx="1028.680" cy="345.960" r="34.000" fill="hsl(261 94% 61%)" opacity="0.255"/>+<circle cx="1063.020" cy="345.960" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>+<circle cx="1097.360" cy="345.960" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>+<circle cx="1131.700" cy="345.960" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="1166.040" cy="345.960" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>+<circle cx="1200.380" cy="345.960" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>+<circle cx="1234.720" cy="345.960" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>+<circle cx="1269.060" cy="345.960" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>+<circle cx="1303.400" cy="345.960" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>+<circle cx="616.600" cy="380.300" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>+<circle cx="650.940" cy="380.300" r="35.000" fill="hsl(223 94% 61%)" opacity="0.206"/>+<circle cx="685.280" cy="380.300" r="35.000" fill="hsl(212 94% 61%)" opacity="0.193"/>+<circle cx="719.620" cy="380.300" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>+<circle cx="753.960" cy="380.300" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>+<circle cx="788.300" cy="380.300" r="35.000" fill="hsl(210 94% 61%)" opacity="0.190"/>+<circle cx="822.640" cy="380.300" r="35.000" fill="hsl(215 94% 61%)" opacity="0.196"/>+<circle cx="856.980" cy="380.300" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="891.320" cy="380.300" r="35.000" fill="hsl(242 94% 61%)" opacity="0.232"/>+<circle cx="925.660" cy="380.300" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>+<circle cx="960.000" cy="380.300" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>+<circle cx="994.340" cy="380.300" r="35.000" fill="hsl(270 94% 61%)" opacity="0.267"/>+<circle cx="1028.680" cy="380.300" r="35.000" fill="hsl(274 94% 61%)" opacity="0.271"/>+<circle cx="1063.020" cy="380.300" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>+<circle cx="1097.360" cy="380.300" r="34.000" fill="hsl(260 94% 61%)" opacity="0.254"/>+<circle cx="1131.700" cy="380.300" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>+<circle cx="1166.040" cy="380.300" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>+<circle cx="1200.380" cy="380.300" r="35.000" fill="hsl(239 94% 61%)" opacity="0.227"/>+<circle cx="1234.720" cy="380.300" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>+<circle cx="1269.060" cy="380.300" r="34.000" fill="hsl(245 94% 61%)" opacity="0.235"/>+<circle cx="1303.400" cy="380.300" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>+<circle cx="616.600" cy="414.640" r="35.000" fill="hsl(241 94% 61%)" opacity="0.229"/>+<circle cx="650.940" cy="414.640" r="35.000" fill="hsl(228 94% 61%)" opacity="0.214"/>+<circle cx="685.280" cy="414.640" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>+<circle cx="719.620" cy="414.640" r="35.000" fill="hsl(206 94% 61%)" opacity="0.186"/>+<circle cx="753.960" cy="414.640" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>+<circle cx="788.300" cy="414.640" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>+<circle cx="822.640" cy="414.640" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>+<circle cx="856.980" cy="414.640" r="35.000" fill="hsl(227 94% 61%)" opacity="0.213"/>+<circle cx="891.320" cy="414.640" r="34.000" fill="hsl(243 94% 61%)" opacity="0.232"/>+<circle cx="925.660" cy="414.640" r="35.000" fill="hsl(258 94% 61%)" opacity="0.251"/>+<circle cx="960.000" cy="414.640" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>+<circle cx="994.340" cy="414.640" r="35.000" fill="hsl(280 94% 61%)" opacity="0.280"/>+<circle cx="1028.680" cy="414.640" r="34.000" fill="hsl(285 94% 61%)" opacity="0.286"/>+<circle cx="1063.020" cy="414.640" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>+<circle cx="1097.360" cy="414.640" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>+<circle cx="1131.700" cy="414.640" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>+<circle cx="1166.040" cy="414.640" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="1200.380" cy="414.640" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>+<circle cx="1234.720" cy="414.640" r="34.000" fill="hsl(242 94% 61%)" opacity="0.231"/>+<circle cx="1269.060" cy="414.640" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>+<circle cx="1303.400" cy="414.640" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>+<circle cx="582.260" cy="448.980" r="35.000" fill="hsl(248 94% 61%)" opacity="0.238"/>+<circle cx="616.600" cy="448.980" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>+<circle cx="650.940" cy="448.980" r="35.000" fill="hsl(235 94% 61%)" opacity="0.222"/>+<circle cx="685.280" cy="448.980" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>+<circle cx="719.620" cy="448.980" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>+<circle cx="753.960" cy="448.980" r="34.000" fill="hsl(207 94% 61%)" opacity="0.187"/>+<circle cx="788.300" cy="448.980" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>+<circle cx="822.640" cy="448.980" r="35.000" fill="hsl(221 94% 61%)" opacity="0.204"/>+<circle cx="856.980" cy="448.980" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>+<circle cx="891.320" cy="448.980" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>+<circle cx="925.660" cy="448.980" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>+<circle cx="960.000" cy="448.980" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>+<circle cx="994.340" cy="448.980" r="35.000" fill="hsl(289 94% 61%)" opacity="0.291"/>+<circle cx="1028.680" cy="448.980" r="35.000" fill="hsl(294 94% 61%)" opacity="0.297"/>+<circle cx="1063.020" cy="448.980" r="35.000" fill="hsl(289 94% 61%)" opacity="0.291"/>+<circle cx="1097.360" cy="448.980" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>+<circle cx="1131.700" cy="448.980" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>+<circle cx="1166.040" cy="448.980" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>+<circle cx="1200.380" cy="448.980" r="34.000" fill="hsl(250 94% 61%)" opacity="0.242"/>+<circle cx="1234.720" cy="448.980" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>+<circle cx="1269.060" cy="448.980" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="1303.400" cy="448.980" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>+<circle cx="1337.740" cy="448.980" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>+<circle cx="582.260" cy="483.320" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>+<circle cx="616.600" cy="483.320" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>+<circle cx="650.940" cy="483.320" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>+<circle cx="685.280" cy="483.320" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="719.620" cy="483.320" r="35.000" fill="hsl(224 94% 61%)" opacity="0.209"/>+<circle cx="753.960" cy="483.320" r="35.000" fill="hsl(216 94% 61%)" opacity="0.199"/>+<circle cx="788.300" cy="483.320" r="35.000" fill="hsl(214 94% 61%)" opacity="0.196"/>+<circle cx="822.640" cy="483.320" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>+<circle cx="856.980" cy="483.320" r="35.000" fill="hsl(234 94% 61%)" opacity="0.220"/>+<circle cx="891.320" cy="483.320" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>+<circle cx="925.660" cy="483.320" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>+<circle cx="960.000" cy="483.320" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>+<circle cx="994.340" cy="483.320" r="35.000" fill="hsl(292 94% 61%)" opacity="0.294"/>+<circle cx="1028.680" cy="483.320" r="35.000" fill="hsl(297 94% 61%)" opacity="0.301"/>+<circle cx="1063.020" cy="483.320" r="35.000" fill="hsl(296 94% 61%)" opacity="0.299"/>+<circle cx="1097.360" cy="483.320" r="35.000" fill="hsl(291 94% 61%)" opacity="0.293"/>+<circle cx="1131.700" cy="483.320" r="34.000" fill="hsl(283 94% 61%)" opacity="0.283"/>+<circle cx="1166.040" cy="483.320" r="35.000" fill="hsl(267 94% 61%)" opacity="0.262"/>+<circle cx="1200.380" cy="483.320" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>+<circle cx="1234.720" cy="483.320" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="1269.060" cy="483.320" r="35.000" fill="hsl(229 94% 61%)" opacity="0.215"/>+<circle cx="1303.400" cy="483.320" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>+<circle cx="1337.740" cy="483.320" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="582.260" cy="517.660" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>+<circle cx="616.600" cy="517.660" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>+<circle cx="650.940" cy="517.660" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="685.280" cy="517.660" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>+<circle cx="719.620" cy="517.660" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>+<circle cx="753.960" cy="517.660" r="35.000" fill="hsl(230 94% 61%)" opacity="0.216"/>+<circle cx="788.300" cy="517.660" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>+<circle cx="822.640" cy="517.660" r="34.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="856.980" cy="517.660" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="891.320" cy="517.660" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>+<circle cx="925.660" cy="517.660" r="35.000" fill="hsl(261 94% 61%)" opacity="0.256"/>+<circle cx="960.000" cy="517.660" r="35.000" fill="hsl(276 94% 61%)" opacity="0.274"/>+<circle cx="994.340" cy="517.660" r="35.000" fill="hsl(290 94% 61%)" opacity="0.292"/>+<circle cx="1028.680" cy="517.660" r="33.000" fill="hsl(299 94% 61%)" opacity="0.303"/>+<circle cx="1063.020" cy="517.660" r="34.000" fill="hsl(301 94% 61%)" opacity="0.306"/>+<circle cx="1097.360" cy="517.660" r="34.000" fill="hsl(296 94% 61%)" opacity="0.300"/>+<circle cx="1131.700" cy="517.660" r="35.000" fill="hsl(284 94% 61%)" opacity="0.285"/>+<circle cx="1166.040" cy="517.660" r="34.000" fill="hsl(268 94% 61%)" opacity="0.265"/>+<circle cx="1200.380" cy="517.660" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>+<circle cx="1234.720" cy="517.660" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>+<circle cx="1269.060" cy="517.660" r="35.000" fill="hsl(224 94% 61%)" opacity="0.209"/>+<circle cx="1303.400" cy="517.660" r="35.000" fill="hsl(219 94% 61%)" opacity="0.201"/>+<circle cx="1337.740" cy="517.660" r="35.000" fill="hsl(219 94% 61%)" opacity="0.201"/>+<circle cx="582.260" cy="552.000" r="35.000" fill="hsl(259 94% 61%)" opacity="0.252"/>+<circle cx="616.600" cy="552.000" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>+<circle cx="650.940" cy="552.000" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>+<circle cx="685.280" cy="552.000" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>+<circle cx="719.620" cy="552.000" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>+<circle cx="753.960" cy="552.000" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>+<circle cx="788.300" cy="552.000" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>+<circle cx="822.640" cy="552.000" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="856.980" cy="552.000" r="35.000" fill="hsl(238 94% 61%)" opacity="0.225"/>+<circle cx="891.320" cy="552.000" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>+<circle cx="925.660" cy="552.000" r="35.000" fill="hsl(258 94% 61%)" opacity="0.252"/>+<circle cx="960.000" cy="552.000" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>+<circle cx="994.340" cy="552.000" r="35.000" fill="hsl(283 94% 61%)" opacity="0.283"/>+<circle cx="1028.680" cy="552.000" r="35.000" fill="hsl(293 94% 61%)" opacity="0.296"/>+<circle cx="1063.020" cy="552.000" r="35.000" fill="hsl(295 94% 61%)" opacity="0.298"/>+<circle cx="1097.360" cy="552.000" r="35.000" fill="hsl(290 94% 61%)" opacity="0.293"/>+<circle cx="1131.700" cy="552.000" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>+<circle cx="1166.040" cy="552.000" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>+<circle cx="1200.380" cy="552.000" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="1234.720" cy="552.000" r="35.000" fill="hsl(235 94% 61%)" opacity="0.222"/>+<circle cx="1269.060" cy="552.000" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>+<circle cx="1303.400" cy="552.000" r="35.000" fill="hsl(211 94% 61%)" opacity="0.192"/>+<circle cx="1337.740" cy="552.000" r="35.000" fill="hsl(209 94% 61%)" opacity="0.190"/>+<circle cx="547.920" cy="586.340" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>+<circle cx="582.260" cy="586.340" r="34.000" fill="hsl(268 94% 61%)" opacity="0.264"/>+<circle cx="616.600" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>+<circle cx="650.940" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>+<circle cx="685.280" cy="586.340" r="35.000" fill="hsl(271 94% 61%)" opacity="0.267"/>+<circle cx="719.620" cy="586.340" r="35.000" fill="hsl(263 94% 61%)" opacity="0.258"/>+<circle cx="753.960" cy="586.340" r="35.000" fill="hsl(254 94% 61%)" opacity="0.247"/>+<circle cx="788.300" cy="586.340" r="35.000" fill="hsl(246 94% 61%)" opacity="0.237"/>+<circle cx="822.640" cy="586.340" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>+<circle cx="856.980" cy="586.340" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>+<circle cx="891.320" cy="586.340" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>+<circle cx="925.660" cy="586.340" r="34.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="960.000" cy="586.340" r="35.000" fill="hsl(262 94% 61%)" opacity="0.256"/>+<circle cx="994.340" cy="586.340" r="35.000" fill="hsl(273 94% 61%)" opacity="0.270"/>+<circle cx="1028.680" cy="586.340" r="35.000" fill="hsl(283 94% 61%)" opacity="0.283"/>+<circle cx="1063.020" cy="586.340" r="35.000" fill="hsl(284 94% 61%)" opacity="0.285"/>+<circle cx="1097.360" cy="586.340" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>+<circle cx="1131.700" cy="586.340" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>+<circle cx="1166.040" cy="586.340" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>+<circle cx="1200.380" cy="586.340" r="35.000" fill="hsl(250 94% 61%)" opacity="0.242"/>+<circle cx="1234.720" cy="586.340" r="35.000" fill="hsl(235 94% 61%)" opacity="0.223"/>+<circle cx="1269.060" cy="586.340" r="35.000" fill="hsl(220 94% 61%)" opacity="0.204"/>+<circle cx="1303.400" cy="586.340" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>+<circle cx="1337.740" cy="586.340" r="35.000" fill="hsl(200 94% 61%)" opacity="0.177"/>+<circle cx="1372.080" cy="586.340" r="35.000" fill="hsl(207 94% 61%)" opacity="0.186"/>+<circle cx="547.920" cy="620.680" r="35.000" fill="hsl(259 94% 61%)" opacity="0.252"/>+<circle cx="582.260" cy="620.680" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>+<circle cx="616.600" cy="620.680" r="35.000" fill="hsl(280 94% 61%)" opacity="0.280"/>+<circle cx="650.940" cy="620.680" r="35.000" fill="hsl(284 94% 61%)" opacity="0.284"/>+<circle cx="685.280" cy="620.680" r="35.000" fill="hsl(282 94% 61%)" opacity="0.281"/>+<circle cx="719.620" cy="620.680" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>+<circle cx="753.960" cy="620.680" r="35.000" fill="hsl(266 94% 61%)" opacity="0.262"/>+<circle cx="788.300" cy="620.680" r="35.000" fill="hsl(257 94% 61%)" opacity="0.250"/>+<circle cx="822.640" cy="620.680" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>+<circle cx="856.980" cy="620.680" r="33.000" fill="hsl(243 94% 61%)" opacity="0.232"/>+<circle cx="891.320" cy="620.680" r="35.000" fill="hsl(240 94% 61%)" opacity="0.229"/>+<circle cx="925.660" cy="620.680" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>+<circle cx="960.000" cy="620.680" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>+<circle cx="994.340" cy="620.680" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>+<circle cx="1028.680" cy="620.680" r="34.000" fill="hsl(268 94% 61%)" opacity="0.265"/>+<circle cx="1063.020" cy="620.680" r="35.000" fill="hsl(275 94% 61%)" opacity="0.273"/>+<circle cx="1097.360" cy="620.680" r="35.000" fill="hsl(274 94% 61%)" opacity="0.272"/>+<circle cx="1131.700" cy="620.680" r="35.000" fill="hsl(270 94% 61%)" opacity="0.266"/>+<circle cx="1166.040" cy="620.680" r="35.000" fill="hsl(261 94% 61%)" opacity="0.255"/>+<circle cx="1200.380" cy="620.680" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>+<circle cx="1234.720" cy="620.680" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>+<circle cx="1269.060" cy="620.680" r="35.000" fill="hsl(223 94% 61%)" opacity="0.207"/>+<circle cx="1303.400" cy="620.680" r="35.000" fill="hsl(212 94% 61%)" opacity="0.192"/>+<circle cx="1337.740" cy="620.680" r="35.000" fill="hsl(203 94% 61%)" opacity="0.181"/>+<circle cx="1372.080" cy="620.680" r="35.000" fill="hsl(198 94% 61%)" opacity="0.175"/>+<circle cx="547.920" cy="655.020" r="35.000" fill="hsl(261 94% 61%)" opacity="0.255"/>+<circle cx="582.260" cy="655.020" r="35.000" fill="hsl(275 94% 61%)" opacity="0.274"/>+<circle cx="616.600" cy="655.020" r="35.000" fill="hsl(286 94% 61%)" opacity="0.287"/>+<circle cx="650.940" cy="655.020" r="35.000" fill="hsl(293 94% 61%)" opacity="0.295"/>+<circle cx="685.280" cy="655.020" r="35.000" fill="hsl(293 94% 61%)" opacity="0.296"/>+<circle cx="719.620" cy="655.020" r="35.000" fill="hsl(288 94% 61%)" opacity="0.290"/>+<circle cx="753.960" cy="655.020" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>+<circle cx="788.300" cy="655.020" r="35.000" fill="hsl(266 94% 61%)" opacity="0.261"/>+<circle cx="822.640" cy="655.020" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="856.980" cy="655.020" r="34.000" fill="hsl(242 94% 61%)" opacity="0.232"/>+<circle cx="891.320" cy="655.020" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>+<circle cx="925.660" cy="655.020" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="960.000" cy="655.020" r="34.000" fill="hsl(237 94% 61%)" opacity="0.225"/>+<circle cx="994.340" cy="655.020" r="35.000" fill="hsl(245 94% 61%)" opacity="0.234"/>+<circle cx="1028.680" cy="655.020" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>+<circle cx="1063.020" cy="655.020" r="34.000" fill="hsl(260 94% 61%)" opacity="0.254"/>+<circle cx="1097.360" cy="655.020" r="35.000" fill="hsl(264 94% 61%)" opacity="0.259"/>+<circle cx="1131.700" cy="655.020" r="34.000" fill="hsl(263 94% 61%)" opacity="0.258"/>+<circle cx="1166.040" cy="655.020" r="35.000" fill="hsl(260 94% 61%)" opacity="0.254"/>+<circle cx="1200.380" cy="655.020" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>+<circle cx="1234.720" cy="655.020" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="1269.060" cy="655.020" r="35.000" fill="hsl(226 94% 61%)" opacity="0.211"/>+<circle cx="1303.400" cy="655.020" r="35.000" fill="hsl(215 94% 61%)" opacity="0.197"/>+<circle cx="1337.740" cy="655.020" r="35.000" fill="hsl(206 94% 61%)" opacity="0.186"/>+<circle cx="1372.080" cy="655.020" r="35.000" fill="hsl(201 94% 61%)" opacity="0.179"/>+<circle cx="582.260" cy="689.360" r="35.000" fill="hsl(272 94% 61%)" opacity="0.269"/>+<circle cx="616.600" cy="689.360" r="35.000" fill="hsl(287 94% 61%)" opacity="0.288"/>+<circle cx="650.940" cy="689.360" r="35.000" fill="hsl(294 94% 61%)" opacity="0.297"/>+<circle cx="685.280" cy="689.360" r="35.000" fill="hsl(295 94% 61%)" opacity="0.298"/>+<circle cx="719.620" cy="689.360" r="35.000" fill="hsl(290 94% 61%)" opacity="0.292"/>+<circle cx="753.960" cy="689.360" r="35.000" fill="hsl(280 94% 61%)" opacity="0.279"/>+<circle cx="788.300" cy="689.360" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>+<circle cx="822.640" cy="689.360" r="34.000" fill="hsl(254 94% 61%)" opacity="0.246"/>+<circle cx="856.980" cy="689.360" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>+<circle cx="891.320" cy="689.360" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>+<circle cx="925.660" cy="689.360" r="35.000" fill="hsl(225 94% 61%)" opacity="0.210"/>+<circle cx="960.000" cy="689.360" r="34.000" fill="hsl(224 94% 61%)" opacity="0.208"/>+<circle cx="994.340" cy="689.360" r="35.000" fill="hsl(230 94% 61%)" opacity="0.215"/>+<circle cx="1028.680" cy="689.360" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="1063.020" cy="689.360" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>+<circle cx="1097.360" cy="689.360" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="1131.700" cy="689.360" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>+<circle cx="1166.040" cy="689.360" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>+<circle cx="1200.380" cy="689.360" r="35.000" fill="hsl(245 94% 61%)" opacity="0.235"/>+<circle cx="1234.720" cy="689.360" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>+<circle cx="1269.060" cy="689.360" r="35.000" fill="hsl(228 94% 61%)" opacity="0.213"/>+<circle cx="1303.400" cy="689.360" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>+<circle cx="1337.740" cy="689.360" r="35.000" fill="hsl(210 94% 61%)" opacity="0.191"/>+<circle cx="616.600" cy="723.700" r="35.000" fill="hsl(271 94% 61%)" opacity="0.268"/>+<circle cx="650.940" cy="723.700" r="35.000" fill="hsl(281 94% 61%)" opacity="0.281"/>+<circle cx="685.280" cy="723.700" r="35.000" fill="hsl(286 94% 61%)" opacity="0.287"/>+<circle cx="719.620" cy="723.700" r="35.000" fill="hsl(285 94% 61%)" opacity="0.285"/>+<circle cx="753.960" cy="723.700" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>+<circle cx="788.300" cy="723.700" r="35.000" fill="hsl(268 94% 61%)" opacity="0.265"/>+<circle cx="822.640" cy="723.700" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>+<circle cx="856.980" cy="723.700" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>+<circle cx="891.320" cy="723.700" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="925.660" cy="723.700" r="35.000" fill="hsl(216 94% 61%)" opacity="0.198"/>+<circle cx="960.000" cy="723.700" r="35.000" fill="hsl(213 94% 61%)" opacity="0.195"/>+<circle cx="994.340" cy="723.700" r="35.000" fill="hsl(216 94% 61%)" opacity="0.198"/>+<circle cx="1028.680" cy="723.700" r="35.000" fill="hsl(225 94% 61%)" opacity="0.210"/>+<circle cx="1063.020" cy="723.700" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="1097.360" cy="723.700" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>+<circle cx="1131.700" cy="723.700" r="35.000" fill="hsl(248 94% 61%)" opacity="0.238"/>+<circle cx="1166.040" cy="723.700" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>+<circle cx="1200.380" cy="723.700" r="35.000" fill="hsl(243 94% 61%)" opacity="0.232"/>+<circle cx="1234.720" cy="723.700" r="35.000" fill="hsl(236 94% 61%)" opacity="0.224"/>+<circle cx="1269.060" cy="723.700" r="35.000" fill="hsl(229 94% 61%)" opacity="0.214"/>+<circle cx="1303.400" cy="723.700" r="35.000" fill="hsl(221 94% 61%)" opacity="0.205"/>+<circle cx="616.600" cy="758.040" r="35.000" fill="hsl(255 94% 61%)" opacity="0.247"/>+<circle cx="650.940" cy="758.040" r="35.000" fill="hsl(269 94% 61%)" opacity="0.266"/>+<circle cx="685.280" cy="758.040" r="35.000" fill="hsl(277 94% 61%)" opacity="0.275"/>+<circle cx="719.620" cy="758.040" r="35.000" fill="hsl(279 94% 61%)" opacity="0.278"/>+<circle cx="753.960" cy="758.040" r="35.000" fill="hsl(276 94% 61%)" opacity="0.274"/>+<circle cx="788.300" cy="758.040" r="35.000" fill="hsl(269 94% 61%)" opacity="0.265"/>+<circle cx="822.640" cy="758.040" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>+<circle cx="856.980" cy="758.040" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="891.320" cy="758.040" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>+<circle cx="925.660" cy="758.040" r="35.000" fill="hsl(209 94% 61%)" opacity="0.189"/>+<circle cx="960.000" cy="758.040" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>+<circle cx="994.340" cy="758.040" r="35.000" fill="hsl(205 94% 61%)" opacity="0.184"/>+<circle cx="1028.680" cy="758.040" r="35.000" fill="hsl(214 94% 61%)" opacity="0.196"/>+<circle cx="1063.020" cy="758.040" r="35.000" fill="hsl(226 94% 61%)" opacity="0.210"/>+<circle cx="1097.360" cy="758.040" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>+<circle cx="1131.700" cy="758.040" r="34.000" fill="hsl(246 94% 61%)" opacity="0.236"/>+<circle cx="1166.040" cy="758.040" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>+<circle cx="1200.380" cy="758.040" r="35.000" fill="hsl(244 94% 61%)" opacity="0.233"/>+<circle cx="1234.720" cy="758.040" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="1269.060" cy="758.040" r="35.000" fill="hsl(231 94% 61%)" opacity="0.218"/>+<circle cx="650.940" cy="792.380" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>+<circle cx="685.280" cy="792.380" r="35.000" fill="hsl(267 94% 61%)" opacity="0.263"/>+<circle cx="719.620" cy="792.380" r="35.000" fill="hsl(272 94% 61%)" opacity="0.270"/>+<circle cx="753.960" cy="792.380" r="35.000" fill="hsl(273 94% 61%)" opacity="0.270"/>+<circle cx="788.300" cy="792.380" r="34.000" fill="hsl(266 94% 61%)" opacity="0.262"/>+<circle cx="822.640" cy="792.380" r="35.000" fill="hsl(252 94% 61%)" opacity="0.244"/>+<circle cx="856.980" cy="792.380" r="35.000" fill="hsl(237 94% 61%)" opacity="0.224"/>+<circle cx="891.320" cy="792.380" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>+<circle cx="925.660" cy="792.380" r="35.000" fill="hsl(211 94% 61%)" opacity="0.191"/>+<circle cx="960.000" cy="792.380" r="35.000" fill="hsl(207 94% 61%)" opacity="0.187"/>+<circle cx="994.340" cy="792.380" r="35.000" fill="hsl(203 94% 61%)" opacity="0.182"/>+<circle cx="1028.680" cy="792.380" r="35.000" fill="hsl(207 94% 61%)" opacity="0.186"/>+<circle cx="1063.020" cy="792.380" r="35.000" fill="hsl(220 94% 61%)" opacity="0.203"/>+<circle cx="1097.360" cy="792.380" r="35.000" fill="hsl(233 94% 61%)" opacity="0.220"/>+<circle cx="1131.700" cy="792.380" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>+<circle cx="1166.040" cy="792.380" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>+<circle cx="1200.380" cy="792.380" r="35.000" fill="hsl(247 94% 61%)" opacity="0.238"/>+<circle cx="1234.720" cy="792.380" r="35.000" fill="hsl(241 94% 61%)" opacity="0.230"/>+<circle cx="1269.060" cy="792.380" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>+<circle cx="685.280" cy="826.720" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>+<circle cx="719.620" cy="826.720" r="35.000" fill="hsl(265 94% 61%)" opacity="0.261"/>+<circle cx="753.960" cy="826.720" r="35.000" fill="hsl(268 94% 61%)" opacity="0.264"/>+<circle cx="788.300" cy="826.720" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>+<circle cx="822.640" cy="826.720" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>+<circle cx="856.980" cy="826.720" r="35.000" fill="hsl(237 94% 61%)" opacity="0.225"/>+<circle cx="891.320" cy="826.720" r="34.000" fill="hsl(221 94% 61%)" opacity="0.205"/>+<circle cx="925.660" cy="826.720" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>+<circle cx="960.000" cy="826.720" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>+<circle cx="994.340" cy="826.720" r="35.000" fill="hsl(208 94% 61%)" opacity="0.188"/>+<circle cx="1028.680" cy="826.720" r="34.000" fill="hsl(203 94% 61%)" opacity="0.181"/>+<circle cx="1063.020" cy="826.720" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>+<circle cx="1097.360" cy="826.720" r="35.000" fill="hsl(232 94% 61%)" opacity="0.218"/>+<circle cx="1131.700" cy="826.720" r="35.000" fill="hsl(247 94% 61%)" opacity="0.237"/>+<circle cx="1166.040" cy="826.720" r="35.000" fill="hsl(255 94% 61%)" opacity="0.248"/>+<circle cx="1200.380" cy="826.720" r="35.000" fill="hsl(251 94% 61%)" opacity="0.243"/>+<circle cx="1234.720" cy="826.720" r="35.000" fill="hsl(242 94% 61%)" opacity="0.232"/>+<circle cx="719.620" cy="861.060" r="35.000" fill="hsl(257 94% 61%)" opacity="0.251"/>+<circle cx="753.960" cy="861.060" r="35.000" fill="hsl(263 94% 61%)" opacity="0.257"/>+<circle cx="788.300" cy="861.060" r="35.000" fill="hsl(259 94% 61%)" opacity="0.253"/>+<circle cx="822.640" cy="861.060" r="35.000" fill="hsl(251 94% 61%)" opacity="0.242"/>+<circle cx="856.980" cy="861.060" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>+<circle cx="891.320" cy="861.060" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>+<circle cx="925.660" cy="861.060" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="960.000" cy="861.060" r="35.000" fill="hsl(223 94% 61%)" opacity="0.206"/>+<circle cx="994.340" cy="861.060" r="35.000" fill="hsl(218 94% 61%)" opacity="0.201"/>+<circle cx="1028.680" cy="861.060" r="35.000" fill="hsl(213 94% 61%)" opacity="0.194"/>+<circle cx="1063.020" cy="861.060" r="35.000" fill="hsl(217 94% 61%)" opacity="0.199"/>+<circle cx="1097.360" cy="861.060" r="35.000" fill="hsl(232 94% 61%)" opacity="0.219"/>+<circle cx="1131.700" cy="861.060" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>+<circle cx="1166.040" cy="861.060" r="35.000" fill="hsl(262 94% 61%)" opacity="0.257"/>+<circle cx="1200.380" cy="861.060" r="35.000" fill="hsl(254 94% 61%)" opacity="0.246"/>+<circle cx="753.960" cy="895.400" r="35.000" fill="hsl(256 94% 61%)" opacity="0.248"/>+<circle cx="788.300" cy="895.400" r="35.000" fill="hsl(256 94% 61%)" opacity="0.249"/>+<circle cx="822.640" cy="895.400" r="35.000" fill="hsl(253 94% 61%)" opacity="0.245"/>+<circle cx="856.980" cy="895.400" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>+<circle cx="891.320" cy="895.400" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>+<circle cx="925.660" cy="895.400" r="35.000" fill="hsl(238 94% 61%)" opacity="0.226"/>+<circle cx="960.000" cy="895.400" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="994.340" cy="895.400" r="35.000" fill="hsl(231 94% 61%)" opacity="0.217"/>+<circle cx="1028.680" cy="895.400" r="35.000" fill="hsl(227 94% 61%)" opacity="0.212"/>+<circle cx="1063.020" cy="895.400" r="35.000" fill="hsl(222 94% 61%)" opacity="0.206"/>+<circle cx="1097.360" cy="895.400" r="35.000" fill="hsl(234 94% 61%)" opacity="0.221"/>+<circle cx="1131.700" cy="895.400" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>+<circle cx="1166.040" cy="895.400" r="35.000" fill="hsl(266 94% 61%)" opacity="0.261"/>+<circle cx="753.960" cy="929.740" r="34.000" fill="hsl(251 94% 61%)" opacity="0.243"/>+<circle cx="788.300" cy="929.740" r="35.000" fill="hsl(256 94% 61%)" opacity="0.248"/>+<circle cx="822.640" cy="929.740" r="34.000" fill="hsl(257 94% 61%)" opacity="0.250"/>+<circle cx="856.980" cy="929.740" r="35.000" fill="hsl(253 94% 61%)" opacity="0.246"/>+<circle cx="891.320" cy="929.740" r="35.000" fill="hsl(250 94% 61%)" opacity="0.242"/>+<circle cx="925.660" cy="929.740" r="35.000" fill="hsl(248 94% 61%)" opacity="0.239"/>+<circle cx="960.000" cy="929.740" r="35.000" fill="hsl(246 94% 61%)" opacity="0.236"/>+<circle cx="994.340" cy="929.740" r="35.000" fill="hsl(244 94% 61%)" opacity="0.234"/>+<circle cx="1028.680" cy="929.740" r="35.000" fill="hsl(242 94% 61%)" opacity="0.231"/>+<circle cx="1063.020" cy="929.740" r="35.000" fill="hsl(240 94% 61%)" opacity="0.228"/>+<circle cx="1097.360" cy="929.740" r="35.000" fill="hsl(236 94% 61%)" opacity="0.223"/>+<circle cx="1131.700" cy="929.740" r="35.000" fill="hsl(250 94% 61%)" opacity="0.241"/>+<circle cx="1166.040" cy="929.740" r="34.000" fill="hsl(262 94% 61%)" opacity="0.257"/>+</g>+<g opacity="0.58"><line x1="983.332" y1="534.766" x2="973.544" y2="526.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="988.499" y1="546.596" x2="983.332" y2="534.766" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="983.332" y1="534.766" x2="1006.907" y2="517.352" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="981.994" y1="570.912" x2="988.022" y2="559.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="988.022" y1="559.496" x2="988.499" y2="546.596" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1016.824" y1="567.202" x2="988.022" y2="559.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="988.499" y1="546.596" x2="1027.405" y2="539.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="991.808" y1="491.757" x2="973.544" y2="526.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="973.544" y1="526.349" x2="961.073" y2="523.013" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="962.146" y1="494.029" x2="961.073" y2="523.013" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="961.073" y1="523.013" x2="948.390" y2="525.418" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="971.610" y1="578.582" x2="981.994" y2="570.912" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="981.994" y1="570.912" x2="1012.465" y2="597.114" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="932.876" y1="489.898" x2="948.390" y2="525.418" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="948.390" y1="525.418" x2="938.006" y2="533.088" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="958.927" y1="580.987" x2="971.610" y2="578.582" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="971.610" y1="578.582" x2="983.824" y2="606.545" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="913.124" y1="511.692" x2="938.006" y2="533.088" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="938.006" y1="533.088" x2="931.978" y2="544.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="946.456" y1="577.651" x2="958.927" y2="580.987" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="958.927" y1="580.987" x2="957.415" y2="621.827" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="890.959" y1="533.530" x2="931.978" y2="544.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="931.978" y1="544.504" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="936.668" y1="569.234" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="899.643" y1="563.445" x2="931.501" y2="557.404" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="936.668" y1="569.234" x2="946.456" y2="577.651" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="903.183" y1="593.968" x2="936.668" y2="569.234" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="946.456" y1="577.651" x2="931.800" y2="605.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="972.412" y1="484.686" x2="962.146" y2="494.029" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="962.146" y1="494.029" x2="947.830" y2="484.058" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1018.788" y1="518.699" x2="1006.907" y2="517.352" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1006.907" y1="517.352" x2="1004.363" y2="498.501" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1016.824" y1="567.202" x2="1029.925" y2="551.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1029.925" y1="551.208" x2="1027.405" y2="539.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1022.389" y1="575.335" x2="1016.824" y2="567.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="983.824" y1="606.545" x2="1006.276" y2="604.953" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1006.276" y1="604.953" x2="1012.465" y2="597.114" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="981.859" y1="613.815" x2="983.824" y2="606.545" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="927.316" y1="607.504" x2="931.800" y2="605.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="931.800" y1="605.409" x2="950.032" y2="621.996" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="897.765" y1="562.566" x2="899.643" y2="563.445" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="899.643" y1="563.445" x2="900.347" y2="590.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="912.015" y1="511.701" x2="913.124" y2="511.692" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="912.015" y1="511.701" x2="891.092" y2="532.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="926.036" y1="486.913" x2="932.876" y2="489.898" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="926.036" y1="486.913" x2="913.124" y2="511.692" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="879.746" y1="488.235" x2="912.015" y2="511.701" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="888.385" y1="535.872" x2="897.765" y2="562.566" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="897.765" y1="562.566" x2="861.003" y2="574.923" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="903.269" y1="597.046" x2="927.316" y2="607.504" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="927.316" y1="607.504" x2="916.556" y2="643.053" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="959.674" y1="623.670" x2="981.859" y2="613.815" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="981.859" y1="613.815" x2="1003.828" y2="642.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1015.388" y1="596.606" x2="1022.389" y2="575.335" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1022.389" y1="575.335" x2="1057.456" y2="573.982" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1029.059" y1="536.487" x2="1018.788" y2="518.699" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1018.788" y1="518.699" x2="1037.830" y2="489.758" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="990.734" y1="488.378" x2="991.808" y2="491.757" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="990.734" y1="488.378" x2="972.412" y2="484.686" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="452.426" x2="972.412" y2="484.686" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="452.426" x2="947.830" y2="484.058" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="947.830" y1="484.058" x2="932.876" y2="489.898" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="924.801" y1="485.323" x2="926.036" y2="486.913" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1037.830" y1="489.758" x2="1004.363" y2="498.501" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1004.363" y1="498.501" x2="991.808" y2="491.757" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="998.095" y1="470.935" x2="990.734" y2="488.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1057.456" y1="573.982" x2="1029.925" y2="551.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1027.405" y1="539.218" x2="1029.059" y2="536.487" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1044.753" y1="530.377" x2="1029.059" y2="536.487" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1003.828" y1="642.233" x2="1006.276" y2="604.953" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1012.465" y1="597.114" x2="1015.388" y2="596.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1028.621" y1="602.722" x2="1015.388" y2="596.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="950.032" y1="621.996" x2="957.415" y2="621.827" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="957.415" y1="621.827" x2="959.674" y2="623.670" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="916.556" y1="643.053" x2="950.032" y2="621.996" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="963.524" y1="635.004" x2="959.674" y2="623.670" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="900.347" y1="590.671" x2="903.183" y2="593.968" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="903.183" y1="593.968" x2="903.269" y2="597.046" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="861.003" y1="574.923" x2="900.347" y2="590.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="899.115" y1="604.826" x2="903.269" y2="597.046" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="879.746" y1="488.235" x2="891.092" y2="532.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="891.092" y1="532.785" x2="890.959" y2="533.530" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="888.385" y1="535.872" x2="890.959" y2="533.530" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="883.649" y1="537.102" x2="888.385" y2="535.872" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="924.690" y1="468.840" x2="924.801" y2="485.323" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="924.801" y1="485.323" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="858.806" y1="525.718" x2="883.649" y2="537.102" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="883.649" y1="537.102" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="877.335" y1="612.112" x2="899.115" y2="604.826" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="899.115" y1="604.826" x2="913.901" y2="646.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="954.849" y1="651.750" x2="963.524" y2="635.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="963.524" y1="635.004" x2="1005.424" y2="647.755" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1033.065" y1="616.531" x2="1028.621" y2="602.722" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1028.621" y1="602.722" x2="1064.264" y2="576.363" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1053.303" y1="534.525" x2="1044.753" y2="530.377" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1044.753" y1="530.377" x2="1044.903" y2="484.740" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="442.292" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="442.292" x2="924.690" y2="468.840" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="907.607" y1="456.774" x2="924.690" y2="468.840" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1001.349" y1="469.664" x2="998.095" y2="470.935" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="998.095" y1="470.935" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1046.205" y1="482.726" x2="1001.349" y2="469.664" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1001.349" y1="469.664" x2="1012.440" y2="438.687" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1069.001" y1="576.148" x2="1053.303" y2="534.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1053.303" y1="534.525" x2="1077.607" y2="520.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1009.761" y1="653.301" x2="1033.065" y2="616.531" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1033.065" y1="616.531" x2="1056.813" y2="621.206" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="911.243" y1="655.315" x2="954.849" y2="651.750" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="954.849" y1="651.750" x2="966.607" y2="668.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="847.298" y1="578.490" x2="877.335" y2="612.112" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="877.335" y1="612.112" x2="874.824" y2="627.402" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="867.916" y1="479.092" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="867.916" y1="479.092" x2="858.806" y2="525.718" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="850.800" y1="531.184" x2="858.806" y2="525.718" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="912.200" y1="437.122" x2="907.607" y2="456.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="907.607" y1="456.774" x2="866.518" y2="476.847" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="822.234" y1="515.708" x2="850.800" y2="531.184" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="850.800" y1="531.184" x2="841.435" y2="578.126" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="847.328" y1="633.483" x2="874.824" y2="627.402" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="874.824" y1="627.402" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1005.424" y1="647.755" x2="1009.761" y2="653.301" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1013.053" y1="664.981" x2="1009.761" y2="653.301" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="966.607" y1="668.164" x2="1013.053" y2="664.981" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="952.990" y1="687.766" x2="966.607" y2="668.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1064.264" y1="576.363" x2="1069.001" y2="576.148" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1083.326" y1="581.231" x2="1069.001" y2="576.148" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1056.813" y1="621.206" x2="1083.326" y2="581.231" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1059.998" y1="639.787" x2="1056.813" y2="621.206" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1077.607" y1="520.313" x2="1061.111" y2="472.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1061.111" y1="472.202" x2="1046.205" y2="482.726" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1088.004" y1="527.396" x2="1077.607" y2="520.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="420.997" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="420.997" x2="912.200" y2="437.122" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="890.064" y1="422.515" x2="912.200" y2="437.122" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1016.991" y1="437.643" x2="1012.440" y2="438.687" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1012.440" y1="438.687" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1063.356" y1="468.620" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1044.903" y1="484.740" x2="1037.830" y2="489.758" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1016.991" y1="437.643" x2="1063.356" y2="468.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1031.409" y1="399.015" x2="1016.991" y2="437.643" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1091.646" y1="580.750" x2="1088.004" y2="527.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1088.004" y1="527.396" x2="1119.190" y2="510.664" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1020.755" y1="674.626" x2="1059.998" y2="639.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1059.998" y1="639.787" x2="1089.392" y2="647.174" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="901.037" y1="677.977" x2="952.990" y2="687.766" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="952.990" y1="687.766" x2="966.417" y2="709.035" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="841.435" y1="578.126" x2="822.368" y2="584.702" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="822.368" y1="584.702" x2="847.328" y2="633.483" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="843.098" y1="652.260" x2="847.328" y2="633.483" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="866.518" y1="476.847" x2="867.916" y2="479.092" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="879.746" y1="488.235" x2="879.076" y2="487.187" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="847.026" y1="462.778" x2="866.518" y2="476.847" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="844.902" y1="459.236" x2="847.026" y2="462.778" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="822.234" y1="515.708" x2="847.026" y2="462.778" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="811.924" y1="521.940" x2="822.234" y2="515.708" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="892.538" y1="398.415" x2="890.064" y2="422.515" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="890.064" y1="422.515" x2="844.902" y2="459.236" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="817.477" y1="439.555" x2="844.902" y2="459.236" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="774.927" y1="505.831" x2="811.924" y2="521.940" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="811.924" y1="521.940" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="911.243" y1="655.315" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="901.037" y1="677.977" x2="905.566" y2="662.340" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="843.098" y1="652.260" x2="892.095" y2="688.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="811.028" y1="664.055" x2="843.098" y2="652.260" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="966.417" y1="709.035" x2="1025.682" y2="692.759" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1020.755" y1="674.626" x2="1013.053" y2="664.981" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="954.383" y1="734.417" x2="966.417" y2="709.035" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1089.392" y1="647.174" x2="1113.737" y2="588.747" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1091.646" y1="580.750" x2="1083.326" y2="581.231" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1096.731" y1="667.435" x2="1089.392" y2="647.174" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1063.356" y1="468.620" x2="1061.111" y2="472.202" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1046.205" y1="482.726" x2="1044.903" y2="484.740" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1119.190" y1="510.664" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1089.518" y1="447.313" x2="1086.418" y2="452.449" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="420.997" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="452.426" x2="960.000" y2="442.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="388.149" x2="960.000" y2="420.997" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="388.149" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="892.538" y1="398.415" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="870.081" y1="377.240" x2="892.538" y2="398.415" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1031.409" y1="399.015" x2="960.000" y2="388.149" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="388.149" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1119.476" y1="426.455" x2="1089.518" y2="447.313" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1089.518" y1="447.313" x2="1036.135" y2="397.016" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1125.495" y1="587.895" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1125.495" y1="587.895" x2="1132.028" y2="516.372" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1036.713" y1="706.233" x2="1096.731" y2="667.435" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1096.731" y1="667.435" x2="1129.234" y2="681.108" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="874.114" y1="724.752" x2="885.923" y2="710.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="885.923" y1="710.844" x2="954.383" y2="734.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="964.714" y1="760.668" x2="954.383" y2="734.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="786.510" y1="593.409" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="822.368" y1="584.702" x2="813.251" y2="584.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="811.028" y1="664.055" x2="786.510" y2="593.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="814.773" y1="434.875" x2="817.477" y2="439.555" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="817.477" y1="439.555" x2="774.927" y2="505.831" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="870.081" y1="377.240" x2="814.773" y2="434.875" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="814.773" y1="434.875" x2="781.631" y2="411.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="867.820" y1="350.841" x2="870.081" y2="377.240" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="847.298" y1="578.490" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="841.435" y1="578.126" x2="847.298" y2="578.490" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="763.070" y1="509.820" x2="774.629" y2="592.324" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="802.354" y1="682.911" x2="874.114" y2="724.752" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="901.037" y1="677.977" x2="892.095" y2="688.774" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="964.714" y1="760.668" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1020.755" y1="674.626" x2="1025.682" y2="692.759" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1003.828" y1="642.233" x2="1005.424" y2="647.755" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1025.682" y1="692.759" x2="1036.713" y2="706.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1036.713" y1="706.233" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1056.399" y1="746.009" x2="1042.887" y2="729.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1091.646" y1="580.750" x2="1113.737" y2="588.747" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1064.264" y1="576.363" x2="1057.456" y2="573.982" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1129.234" y1="681.108" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1113.737" y1="588.747" x2="1125.495" y2="587.895" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1168.319" y1="597.246" x2="1154.077" y2="598.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1171.121" y1="500.189" x2="1119.476" y2="426.455" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1119.476" y1="426.455" x2="1123.156" y2="420.161" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="345.580" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="345.580" x2="867.820" y2="350.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1055.165" y1="351.011" x2="960.000" y2="345.580" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="345.580" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1157.051" y1="396.671" x2="1123.156" y2="420.161" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1123.156" y1="420.161" x2="1059.283" y2="348.098" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1200.567" y1="609.315" x2="1168.319" y2="597.246" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1168.319" y1="597.246" x2="1185.153" y2="503.235" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1063.217" y1="772.753" x2="1056.399" y2="746.009" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1056.399" y1="746.009" x2="1140.858" y2="700.809" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="866.867" y1="751.471" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="874.114" y1="724.752" x2="866.867" y2="751.471" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="956.014" y1="790.291" x2="866.867" y2="751.471" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="774.629" y1="592.324" x2="786.510" y2="593.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="742.414" y1="603.842" x2="767.921" y2="700.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="778.616" y1="405.859" x2="781.631" y2="411.208" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="781.631" y1="411.208" x2="719.271" y2="494.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="846.478" y1="324.667" x2="778.616" y2="405.859" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="778.616" y1="405.859" x2="743.401" y2="380.760" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="706.813" y1="496.189" x2="728.954" y2="602.485" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="742.414" y1="603.842" x2="728.954" y2="602.485" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="892.095" y1="688.774" x2="885.923" y2="710.844" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="916.556" y1="643.053" x2="913.901" y2="646.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="755.980" y1="718.378" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="913.901" y1="646.396" x2="911.243" y2="655.315" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="845.762" y1="795.518" x2="853.374" y2="767.111" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="963.309" y1="819.481" x2="1063.217" y2="772.753" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1063.217" y1="772.753" x2="1077.647" y2="789.877" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1174.640" y1="719.101" x2="1200.567" y2="609.315" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1200.567" y1="609.315" x2="1215.710" y2="607.957" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1229.348" y1="488.029" x2="1157.051" y2="396.671" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1157.051" y1="396.671" x2="1160.932" y2="389.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="297.480" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="297.480" x2="840.722" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1081.464" y1="297.480" x2="960.000" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="297.480" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1194.827" y1="366.457" x2="1160.932" y2="389.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1160.932" y1="389.948" x2="1085.015" y2="294.014" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1247.958" y1="620.026" x2="1215.710" y2="607.957" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1215.710" y1="607.957" x2="1243.784" y2="489.085" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1084.465" y1="816.620" x2="1077.647" y2="789.877" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1077.647" y1="789.877" x2="1188.966" y2="737.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="832.269" y1="811.159" x2="845.762" y2="795.518" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="845.762" y1="795.518" x2="957.220" y2="851.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="742.414" y1="603.842" x2="774.629" y2="592.324" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="694.769" y1="614.787" x2="720.749" y2="739.517" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="740.385" y1="375.411" x2="743.401" y2="380.760" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="743.401" y1="380.760" x2="659.219" y2="481.477" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="820.635" y1="268.446" x2="740.385" y2="375.411" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="740.385" y1="375.411" x2="707.243" y2="351.744" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="728.954" y1="602.485" x2="694.769" y2="614.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="681.309" y1="613.430" x2="694.769" y2="614.787" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="646.566" y1="481.855" x2="681.309" y2="613.430" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="706.783" y1="756.515" x2="832.269" y2="811.159" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="832.269" y1="811.159" x2="825.021" y2="837.878" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="962.311" y1="881.993" x2="1084.465" y2="816.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1084.465" y1="816.620" x2="1097.977" y2="832.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1223.079" y1="758.972" x2="1247.958" y2="620.026" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1247.958" y1="620.026" x2="1262.200" y2="618.818" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1290.828" y1="474.767" x2="1194.827" y2="366.457" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1194.827" y1="366.457" x2="1198.507" y2="360.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="249.380" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="249.380" x2="812.520" y2="240.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1109.071" y1="240.873" x2="960.000" y2="249.380" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="249.380" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1228.466" y1="339.305" x2="1198.507" y2="360.164" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1198.507" y1="360.164" x2="1112.243" y2="237.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1290.782" y1="629.377" x2="1262.200" y2="618.818" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1262.200" y1="618.818" x2="1305.599" y2="474.431" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1104.151" y1="856.396" x2="1097.977" y2="832.785" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1097.977" y1="832.785" x2="1239.349" y2="777.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="813.213" y1="851.786" x2="825.021" y2="837.878" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="825.021" y1="837.878" x2="958.045" y2="915.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="649.094" y1="624.948" x2="681.309" y2="613.430" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="861.003" y1="574.923" x2="858.236" y2="574.779" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="637.213" y1="623.863" x2="649.094" y2="624.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="670.857" y1="780.283" x2="649.094" y2="624.948" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="704.539" y1="347.063" x2="707.243" y2="351.744" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="707.243" y1="351.744" x2="596.046" y2="467.620" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="792.977" y1="209.314" x2="704.539" y2="347.063" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="704.539" y1="347.063" x2="677.115" y2="327.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="582.735" y1="466.892" x2="637.213" y2="623.863" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="637.213" y1="623.863" x2="610.472" y2="633.269" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="654.802" y1="797.239" x2="813.213" y2="851.786" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="813.213" y1="851.786" x2="807.041" y2="873.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="961.660" y1="948.507" x2="1104.151" y2="856.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1104.151" y1="856.396" x2="1115.182" y2="869.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1275.085" y1="801.240" x2="1290.782" y2="629.377" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1290.782" y1="629.377" x2="1302.541" y2="628.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1356.776" y1="460.198" x2="1228.466" y2="339.305" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1228.466" y1="339.305" x2="1231.566" y2="334.170" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="206.811" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="206.811" x2="782.440" y2="179.793" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1138.787" y1="179.606" x2="960.000" y2="206.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="206.811" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1254.627" y1="317.999" x2="1231.566" y2="334.170" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1231.566" y1="334.170" x2="1141.961" y2="174.998" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1324.631" y1="636.522" x2="1302.541" y2="628.525" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1302.541" y1="628.525" x2="1373.328" y2="458.637" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1120.109" y1="888.003" x2="1115.182" y2="869.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1115.182" y1="869.870" x2="1294.663" y2="820.579" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="798.099" y1="884.652" x2="807.041" y2="873.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="807.041" y1="873.855" x2="958.533" y2="986.753" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="601.355" y1="632.570" x2="610.472" y2="633.269" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="610.472" y1="633.269" x2="614.586" y2="825.722" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="674.990" y1="323.841" x2="677.115" y2="327.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="677.115" y1="327.383" x2="524.370" y2="451.576" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="760.577" y1="141.117" x2="674.990" y2="323.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="674.990" y1="323.841" x2="655.498" y2="309.772" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="507.715" y1="449.567" x2="601.355" y2="632.570" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="601.355" y1="632.570" x2="582.288" y2="639.146" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="593.158" y1="846.038" x2="798.099" y2="884.652" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="798.099" y1="884.652" x2="793.570" y2="900.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="961.349" y1="1028.947" x2="1120.109" y2="888.003" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1120.109" y1="888.003" x2="1127.812" y2="897.649" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1339.402" y1="852.893" x2="1324.631" y2="636.522" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1324.631" y1="636.522" x2="1332.952" y2="636.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1439.513" y1="441.523" x2="1254.627" y2="317.999" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1254.627" y1="317.999" x2="1256.873" y2="314.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="173.963" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="173.963" x2="744.361" y2="101.218" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1176.815" y1="100.821" x2="960.000" y2="173.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="173.963" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1271.779" y1="303.892" x2="1256.873" y2="314.417" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1256.873" y1="314.417" x2="1181.142" y2="93.546" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1347.277" y1="641.124" x2="1332.952" y2="636.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1332.952" y1="636.041" x2="1464.468" y2="437.742" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1131.103" y1="909.328" x2="1127.812" y2="897.649" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1127.812" y1="897.649" x2="1370.608" y2="881.008" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="787.893" y1="907.314" x2="793.570" y2="900.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="793.570" y1="900.289" x2="958.625" y2="1087.870" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="576.425" y1="638.782" x2="582.288" y2="639.146" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="582.288" y1="639.146" x2="531.894" y2="891.743" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="654.101" y1="307.526" x2="655.498" y2="309.772" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="655.498" y1="309.772" x2="415.868" y2="426.834" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="707.944" y1="31.960" x2="654.101" y2="307.526" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="654.101" y1="307.526" x2="642.940" y2="299.432" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="381.778" y1="420.908" x2="576.425" y2="638.782" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="576.425" y1="638.782" x2="565.486" y2="642.493" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="485.594" y1="932.026" x2="787.893" y2="907.314" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="787.893" y1="907.314" x2="785.235" y2="916.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="961.588" y1="1176.797" x2="1131.103" y2="909.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1131.103" y1="909.328" x2="1135.441" y2="914.874" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1464.690" y1="952.440" x2="1347.277" y2="641.124" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1347.277" y1="641.124" x2="1352.013" y2="640.909" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1609.901" y1="402.354" x2="1271.779" y2="303.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1271.779" y1="303.892" x2="1273.081" y2="301.879" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="152.668" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="152.668" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1260.498" y1="-73.265" x2="960.000" y2="152.668" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="960.000" y1="152.668" x2="960.000" y2="142.534" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1280.154" y1="296.861" x2="1273.081" y2="301.879" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1273.081" y1="301.879" x2="1274.399" y2="-99.719" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1358.822" y1="643.290" x2="1352.013" y2="640.909" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1352.013" y1="640.909" x2="1698.162" y2="384.929" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1137.036" y1="920.396" x2="1135.441" y2="914.874" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1135.441" y1="914.874" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="782.580" y1="919.577" x2="785.235" y2="916.233" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="785.235" y1="916.233" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="562.720" y1="642.349" x2="565.486" y2="642.493" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="565.486" y1="642.493" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="642.270" y1="298.383" x2="642.940" y2="299.432" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="642.940" y1="299.432" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="428.039" y1="-542.846" x2="642.270" y2="298.383" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="-470.272" y1="228.865" x2="562.720" y2="642.349" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="-509.558" y1="1732.361" x2="782.580" y2="919.577" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="972.408" y1="3428.116" x2="1137.036" y2="920.396" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="7397.757" y1="5638.793" x2="1358.822" y2="643.290" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="846.478" y1="324.667" x2="867.820" y2="350.841" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="846.478" y1="324.667" x2="840.722" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="840.722" y1="297.480" x2="820.635" y2="268.446" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="812.520" y1="240.963" x2="820.635" y2="268.446" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="792.977" y1="209.314" x2="812.520" y2="240.963" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="782.440" y1="179.793" x2="792.977" y2="209.314" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="782.440" y1="179.793" x2="760.577" y2="141.117" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="744.361" y1="101.218" x2="760.577" y2="141.117" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="744.361" y1="101.218" x2="707.944" y2="31.960" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="707.944" y1="31.960" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="428.039" y1="-542.846" x2="661.656" y2="-71.645" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="763.070" y1="509.820" x2="774.927" y2="505.831" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="763.070" y1="509.820" x2="719.271" y2="494.328" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="719.271" y1="494.328" x2="706.813" y2="496.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="659.219" y1="481.477" x2="706.813" y2="496.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="659.219" y1="481.477" x2="646.566" y2="481.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="596.046" y1="467.620" x2="646.566" y2="481.855" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="596.046" y1="467.620" x2="582.735" y2="466.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="524.370" y1="451.576" x2="582.735" y2="466.892" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="524.370" y1="451.576" x2="507.715" y2="449.567" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="507.715" y1="449.567" x2="415.868" y2="426.834" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="415.868" y1="426.834" x2="381.778" y2="420.908" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="381.778" y1="420.908" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="-470.272" y1="228.865" x2="-18.861" y2="326.409" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="802.354" y1="682.911" x2="811.028" y2="664.055" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="802.354" y1="682.911" x2="767.921" y2="700.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="767.921" y1="700.439" x2="755.980" y2="718.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="720.749" y1="739.517" x2="755.980" y2="718.378" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="706.783" y1="756.515" x2="720.749" y2="739.517" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="670.857" y1="780.283" x2="706.783" y2="756.515" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="670.857" y1="780.283" x2="654.802" y2="797.239" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="614.586" y1="825.722" x2="654.802" y2="797.239" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="614.586" y1="825.722" x2="593.158" y2="846.038" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="593.158" y1="846.038" x2="531.894" y2="891.743" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="531.894" y1="891.743" x2="485.594" y2="932.026" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="485.594" y1="932.026" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="-509.558" y1="1732.361" x2="243.214" y2="1120.292" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="964.714" y1="760.668" x2="956.014" y2="790.291" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="956.014" y1="790.291" x2="963.309" y2="819.481" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="957.220" y1="851.289" x2="963.309" y2="819.481" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="962.311" y1="881.993" x2="957.220" y2="851.289" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="958.045" y1="915.439" x2="962.311" y2="881.993" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="961.660" y1="948.507" x2="958.045" y2="915.439" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="958.533" y1="986.753" x2="961.660" y2="948.507" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="958.533" y1="986.753" x2="961.349" y2="1028.947" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="958.625" y1="1087.870" x2="961.349" y2="1028.947" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="958.625" y1="1087.870" x2="961.588" y2="1176.797" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="961.588" y1="1176.797" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="972.408" y1="3428.116" x2="957.480" y2="1400.496" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1129.234" y1="681.108" x2="1140.858" y2="700.809" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1140.858" y1="700.809" x2="1174.640" y2="719.101" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1188.966" y1="737.811" x2="1174.640" y2="719.101" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1223.079" y1="758.972" x2="1188.966" y2="737.811" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1239.349" y1="777.041" x2="1223.079" y2="758.972" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1275.085" y1="801.240" x2="1239.349" y2="777.041" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1294.663" y1="820.579" x2="1275.085" y2="801.240" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1294.663" y1="820.579" x2="1339.402" y2="852.893" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1370.608" y1="881.008" x2="1339.402" y2="852.893" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1370.608" y1="881.008" x2="1464.690" y2="952.440" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1464.690" y1="952.440" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="7397.757" y1="5638.793" x2="1582.195" y2="1050.836" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1119.190" y1="510.664" x2="1132.028" y2="516.372" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1132.028" y1="516.372" x2="1171.121" y2="500.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1185.153" y1="503.235" x2="1171.121" y2="500.189" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1229.348" y1="488.029" x2="1185.153" y2="503.235" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1243.784" y1="489.085" x2="1229.348" y2="488.029" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1290.828" y1="474.767" x2="1243.784" y2="489.085" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1305.599" y1="474.431" x2="1290.828" y2="474.767" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1356.776" y1="460.198" x2="1305.599" y2="474.431" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1373.328" y1="458.637" x2="1356.776" y2="460.198" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1373.328" y1="458.637" x2="1439.513" y2="441.523" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1464.468" y1="437.742" x2="1439.513" y2="441.523" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1464.468" y1="437.742" x2="1609.901" y2="402.354" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1609.901" y1="402.354" x2="1698.162" y2="384.929" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1031.409" y1="399.015" x2="1036.135" y2="397.016" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1036.135" y1="397.016" x2="1055.165" y2="351.011" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1059.283" y1="348.098" x2="1055.165" y2="351.011" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1081.464" y1="297.480" x2="1059.283" y2="348.098" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1085.015" y1="294.014" x2="1081.464" y2="297.480" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1109.071" y1="240.873" x2="1085.015" y2="294.014" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1112.243" y1="237.004" x2="1109.071" y2="240.873" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1138.787" y1="179.606" x2="1112.243" y2="237.004" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1141.961" y1="174.998" x2="1138.787" y2="179.606" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1141.961" y1="174.998" x2="1176.815" y2="100.821" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1181.142" y1="93.546" x2="1176.815" y2="100.821" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1181.142" y1="93.546" x2="1260.498" y2="-73.265" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+<line x1="1260.498" y1="-73.265" x2="1274.399" y2="-99.719" stroke="#61efff" stroke-width="1.150" stroke-linecap="round" opacity="0.500" />+</g>+<polygon points="960.000,552.000 996.876,509.114 1011.832,529.361" fill="hsl(232 88% 60%)" opacity="0.230"/>+<polygon points="960.000,552.000 1016.521,554.092 1010.016,578.409" fill="hsl(233 88% 60%)" opacity="0.230"/>+<polygon points="1016.521,554.092 960.000,552.000 1011.832,529.361" fill="hsl(234 88% 60%)" opacity="0.230"/>+<polygon points="996.876,509.114 960.000,552.000 974.617,497.361" fill="hsl(232 88% 60%)" opacity="0.230"/>+<polygon points="974.617,497.361 960.000,552.000 949.463,496.430" fill="hsl(234 88% 60%)" opacity="0.230"/>+<polygon points="960.000,552.000 1010.016,578.409 993.605,597.495" fill="hsl(233 88% 60%)" opacity="0.230"/>+<polygon points="949.463,496.430 960.000,552.000 926.395,506.505" fill="hsl(233 88% 60%)" opacity="0.230"/>+<polygon points="960.000,552.000 993.605,597.495 970.537,607.570" fill="hsl(233 88% 60%)" opacity="0.230"/>+<polygon points="926.395,506.505 960.000,552.000 909.984,525.591" fill="hsl(234 88% 60%)" opacity="0.230"/>+<polygon points="960.000,552.000 970.537,607.570 945.383,606.639" fill="hsl(234 88% 60%)" opacity="0.230"/>+<polygon points="909.984,525.591 960.000,552.000 903.479,549.908" fill="hsl(234 88% 60%)" opacity="0.230"/>+<polygon points="960.000,552.000 908.168,574.639 903.479,549.908" fill="hsl(233 88% 60%)" opacity="0.230"/>+<polygon points="960.000,552.000 923.124,594.886 908.168,574.639" fill="hsl(233 88% 60%)" opacity="0.230"/>+<polygon points="923.124,594.886 960.000,552.000 945.383,606.639" fill="hsl(233 88% 60%)" opacity="0.230"/>+<polygon points="974.617,497.361 949.463,496.430 960.000,481.300" fill="hsl(270 88% 60%)" opacity="0.230"/>+<polygon points="1011.832,529.361 996.876,509.114 1014.395,506.750" fill="hsl(270 88% 60%)" opacity="0.230"/>+<polygon points="1016.521,554.092 1043.355,548.453 1029.736,564.916" fill="hsl(267 88% 60%)" opacity="0.230"/>+<polygon points="1010.016,578.409 1016.521,554.092 1029.736,564.916" fill="hsl(269 88% 60%)" opacity="0.230"/>+<polygon points="993.605,597.495 1016.470,615.548 994.784,614.125" fill="hsl(265 88% 60%)" opacity="0.230"/>+<polygon points="970.537,607.570 993.605,597.495 994.784,614.125" fill="hsl(268 88% 60%)" opacity="0.230"/>+<polygon points="923.124,594.886 945.383,606.639 934.305,618.816" fill="hsl(267 88% 60%)" opacity="0.230"/>+<polygon points="903.479,549.908 908.168,574.639 891.708,575.064" fill="hsl(266 88% 60%)" opacity="0.230"/>+<polygon points="909.984,525.591 909.772,497.843 926.395,506.505" fill="hsl(266 88% 60%)" opacity="0.230"/>+<polygon points="926.395,506.505 940.647,473.854 949.463,496.430" fill="hsl(258 88% 60%)" opacity="0.230"/>+<polygon points="909.772,497.843 909.984,525.591 898.140,513.838" fill="hsl(266 88% 60%)" opacity="0.230"/>+<polygon points="903.479,549.908 891.708,575.064 885.389,556.265" fill="hsl(267 88% 60%)" opacity="0.230"/>+<polygon points="923.124,594.886 934.305,618.816 915.227,613.042" fill="hsl(268 88% 60%)" opacity="0.230"/>+<polygon points="970.537,607.570 994.784,614.125 978.903,626.402" fill="hsl(270 88% 60%)" opacity="0.230"/>+<polygon points="1010.016,578.409 1029.736,564.916 1030.517,585.156" fill="hsl(271 88% 60%)" opacity="0.230"/>+<polygon points="1011.832,529.361 1014.395,506.750 1031.501,518.004" fill="hsl(271 88% 60%)" opacity="0.230"/>+<polygon points="974.617,497.361 1009.078,486.399 996.876,509.114" fill="hsl(257 88% 60%)" opacity="0.230"/>+<polygon points="979.353,473.854 974.617,497.361 960.000,481.300" fill="hsl(270 88% 60%)" opacity="0.230"/>+<polygon points="960.000,481.300 949.463,496.430 940.647,473.854" fill="hsl(274 88% 60%)" opacity="0.230"/>+<polygon points="940.647,473.854 926.395,506.505 909.772,497.843" fill="hsl(252 88% 60%)" opacity="0.230"/>+<polygon points="1014.395,506.750 996.876,509.114 1009.078,486.399" fill="hsl(270 88% 60%)" opacity="0.230"/>+<polygon points="1009.078,486.399 974.617,497.361 979.353,473.854" fill="hsl(256 88% 60%)" opacity="0.230"/>+<polygon points="1043.355,548.453 1016.521,554.092 1011.832,529.361" fill="hsl(256 88% 60%)" opacity="0.230"/>+<polygon points="1043.355,548.453 1011.832,529.361 1031.501,518.004" fill="hsl(255 88% 60%)" opacity="0.230"/>+<polygon points="1016.470,615.548 993.605,597.495 1010.016,578.409" fill="hsl(255 88% 60%)" opacity="0.230"/>+<polygon points="1016.470,615.548 1010.016,578.409 1030.517,585.156" fill="hsl(254 88% 60%)" opacity="0.230"/>+<polygon points="945.383,606.639 970.537,607.570 946.088,637.549" fill="hsl(253 88% 60%)" opacity="0.230"/>+<polygon points="934.305,618.816 945.383,606.639 946.088,637.549" fill="hsl(263 88% 60%)" opacity="0.230"/>+<polygon points="946.088,637.549 970.537,607.570 978.903,626.402" fill="hsl(253 88% 60%)" opacity="0.230"/>+<polygon points="908.168,574.639 923.124,594.886 883.325,596.007" fill="hsl(251 88% 60%)" opacity="0.230"/>+<polygon points="891.708,575.064 908.168,574.639 883.325,596.007" fill="hsl(258 88% 60%)" opacity="0.230"/>+<polygon points="883.325,596.007 923.124,594.886 915.227,613.042" fill="hsl(251 88% 60%)" opacity="0.230"/>+<polygon points="898.140,513.838 909.984,525.591 875.839,519.518" fill="hsl(251 88% 60%)" opacity="0.230"/>+<polygon points="875.839,519.518 909.984,525.591 903.479,549.908" fill="hsl(250 88% 60%)" opacity="0.230"/>+<polygon points="875.839,519.518 903.479,549.908 885.389,556.265" fill="hsl(250 88% 60%)" opacity="0.230"/>+<polygon points="940.647,473.854 909.772,497.843 908.803,474.067" fill="hsl(252 88% 60%)" opacity="0.230"/>+<polygon points="875.839,519.518 885.389,556.265 865.231,542.668" fill="hsl(254 88% 60%)" opacity="0.230"/>+<polygon points="883.325,596.007 915.227,613.042 891.810,621.371" fill="hsl(257 88% 60%)" opacity="0.230"/>+<polygon points="946.088,637.549 978.903,626.402 971.504,650.715" fill="hsl(259 88% 60%)" opacity="0.230"/>+<polygon points="1016.470,615.548 1030.517,585.156 1045.972,606.054" fill="hsl(259 88% 60%)" opacity="0.230"/>+<polygon points="1043.355,548.453 1031.501,518.004 1058.086,518.092" fill="hsl(257 88% 60%)" opacity="0.230"/>+<polygon points="940.647,473.854 979.353,473.854 960.000,481.300" fill="hsl(235 88% 60%)" opacity="0.230"/>+<polygon points="924.305,452.119 940.647,473.854 908.803,474.067" fill="hsl(261 88% 60%)" opacity="0.230"/>+<polygon points="1009.078,486.399 979.353,473.854 995.695,452.119" fill="hsl(254 88% 60%)" opacity="0.230"/>+<polygon points="1009.078,486.399 995.695,452.119 1016.855,459.695" fill="hsl(256 88% 60%)" opacity="0.230"/>+<polygon points="1043.355,548.453 1058.086,518.092 1069.970,538.415" fill="hsl(260 88% 60%)" opacity="0.230"/>+<polygon points="1016.470,615.548 1045.972,606.054 1041.038,631.119" fill="hsl(262 88% 60%)" opacity="0.230"/>+<polygon points="946.088,637.549 971.504,650.715 948.510,667.186" fill="hsl(262 88% 60%)" opacity="0.230"/>+<polygon points="883.325,596.007 891.810,621.371 860.659,616.256" fill="hsl(260 88% 60%)" opacity="0.230"/>+<polygon points="875.839,519.518 909.772,497.843 898.140,513.838" fill="hsl(230 88% 60%)" opacity="0.230"/>+<polygon points="845.359,513.562 875.839,519.518 865.231,542.668" fill="hsl(257 88% 60%)" opacity="0.230"/>+<polygon points="924.305,452.119 908.803,474.067 894.702,445.202" fill="hsl(260 88% 60%)" opacity="0.230"/>+<polygon points="845.359,513.562 865.231,542.668 833.067,536.251" fill="hsl(256 88% 60%)" opacity="0.230"/>+<polygon points="860.659,616.256 891.810,621.371 866.681,643.482" fill="hsl(258 88% 60%)" opacity="0.230"/>+<polygon points="971.504,650.715 1016.470,615.548 1041.038,631.119" fill="hsl(226 88% 60%)" opacity="0.230"/>+<polygon points="971.504,650.715 1041.038,631.119 973.839,684.781" fill="hsl(222 88% 60%)" opacity="0.230"/>+<polygon points="948.510,667.186 971.504,650.715 973.839,684.781" fill="hsl(257 88% 60%)" opacity="0.230"/>+<polygon points="1045.972,606.054 1043.355,548.453 1069.970,538.415" fill="hsl(226 88% 60%)" opacity="0.230"/>+<polygon points="1045.972,606.054 1069.970,538.415 1074.989,625.299" fill="hsl(222 88% 60%)" opacity="0.230"/>+<polygon points="1041.038,631.119 1045.972,606.054 1074.989,625.299" fill="hsl(255 88% 60%)" opacity="0.230"/>+<polygon points="1058.086,518.092 1016.855,459.695 1091.658,506.581" fill="hsl(222 88% 60%)" opacity="0.230"/>+<polygon points="1069.970,538.415 1058.086,518.092 1091.658,506.581" fill="hsl(252 88% 60%)" opacity="0.230"/>+<polygon points="995.695,452.119 940.647,473.854 924.305,452.119" fill="hsl(226 88% 60%)" opacity="0.230"/>+<polygon points="912.747,417.857 924.305,452.119 894.702,445.202" fill="hsl(253 88% 60%)" opacity="0.230"/>+<polygon points="1016.855,459.695 995.695,452.119 1007.253,417.857" fill="hsl(249 88% 60%)" opacity="0.230"/>+<polygon points="1009.078,486.399 1058.086,518.092 1031.501,518.004" fill="hsl(227 88% 60%)" opacity="0.230"/>+<polygon points="1009.078,486.399 1031.501,518.004 1014.395,506.750" fill="hsl(234 88% 60%)" opacity="0.230"/>+<polygon points="1037.312,429.076 1016.855,459.695 1007.253,417.857" fill="hsl(248 88% 60%)" opacity="0.230"/>+<polygon points="1069.970,538.415 1091.658,506.581 1107.369,535.862" fill="hsl(249 88% 60%)" opacity="0.230"/>+<polygon points="1041.038,631.119 1074.989,625.299 1066.359,659.641" fill="hsl(250 88% 60%)" opacity="0.230"/>+<polygon points="948.510,667.186 973.839,684.781 941.328,705.304" fill="hsl(250 88% 60%)" opacity="0.230"/>+<polygon points="883.325,596.007 885.389,556.265 891.708,575.064" fill="hsl(231 88% 60%)" opacity="0.230"/>+<polygon points="825.558,634.217 860.659,616.256 866.681,643.482" fill="hsl(248 88% 60%)" opacity="0.230"/>+<polygon points="908.803,474.067 875.839,519.518 845.359,513.562" fill="hsl(224 88% 60%)" opacity="0.230"/>+<polygon points="845.359,513.562 894.702,445.202 908.803,474.067" fill="hsl(223 88% 60%)" opacity="0.230"/>+<polygon points="894.702,445.202 845.359,513.562 809.079,496.569" fill="hsl(218 88% 60%)" opacity="0.230"/>+<polygon points="909.772,497.843 875.839,519.518 908.803,474.067" fill="hsl(229 88% 60%)" opacity="0.230"/>+<polygon points="809.079,496.569 845.359,513.562 833.067,536.251" fill="hsl(245 88% 60%)" opacity="0.230"/>+<polygon points="912.747,417.857 894.702,445.202 868.801,413.347" fill="hsl(246 88% 60%)" opacity="0.230"/>+<polygon points="868.801,413.347 894.702,445.202 809.079,496.569" fill="hsl(216 88% 60%)" opacity="0.230"/>+<polygon points="809.079,496.569 833.067,536.251 791.413,537.142" fill="hsl(241 88% 60%)" opacity="0.230"/>+<polygon points="891.810,621.371 948.510,667.186 866.681,643.482" fill="hsl(223 88% 60%)" opacity="0.230"/>+<polygon points="866.681,643.482 941.328,705.304 841.428,677.368" fill="hsl(216 88% 60%)" opacity="0.230"/>+<polygon points="825.558,634.217 866.681,643.482 841.428,677.368" fill="hsl(242 88% 60%)" opacity="0.230"/>+<polygon points="973.839,684.781 1066.359,659.641 985.185,726.096" fill="hsl(215 88% 60%)" opacity="0.230"/>+<polygon points="941.328,705.304 973.839,684.781 985.185,726.096" fill="hsl(241 88% 60%)" opacity="0.230"/>+<polygon points="1074.989,625.299 1107.369,535.862 1115.066,641.998" fill="hsl(215 88% 60%)" opacity="0.230"/>+<polygon points="1066.359,659.641 1074.989,625.299 1115.066,641.998" fill="hsl(237 88% 60%)" opacity="0.230"/>+<polygon points="1058.086,518.092 1009.078,486.399 1016.855,459.695" fill="hsl(226 88% 60%)" opacity="0.230"/>+<polygon points="1091.658,506.581 1037.312,429.076 1129.979,485.008" fill="hsl(214 88% 60%)" opacity="0.230"/>+<polygon points="1037.312,429.076 1091.658,506.581 1016.855,459.695" fill="hsl(221 88% 60%)" opacity="0.230"/>+<polygon points="995.695,452.119 924.305,452.119 912.747,417.857" fill="hsl(220 88% 60%)" opacity="0.230"/>+<polygon points="912.747,417.857 1007.253,417.857 995.695,452.119" fill="hsl(220 88% 60%)" opacity="0.230"/>+<polygon points="1007.253,417.857 912.747,417.857 906.049,373.842" fill="hsl(211 88% 60%)" opacity="0.230"/>+<polygon points="979.353,473.854 940.647,473.854 995.695,452.119" fill="hsl(227 88% 60%)" opacity="0.230"/>+<polygon points="906.049,373.842 912.747,417.857 868.801,413.347" fill="hsl(237 88% 60%)" opacity="0.230"/>+<polygon points="1007.253,417.857 906.049,373.842 1013.951,373.842" fill="hsl(211 88% 60%)" opacity="0.230"/>+<polygon points="1129.979,485.008 1037.312,429.076 1068.207,396.285" fill="hsl(212 88% 60%)" opacity="0.230"/>+<polygon points="1107.369,535.862 1074.989,625.299 1069.970,538.415" fill="hsl(220 88% 60%)" opacity="0.230"/>+<polygon points="1066.359,659.641 1115.066,641.998 1091.369,698.330" fill="hsl(230 88% 60%)" opacity="0.230"/>+<polygon points="946.088,637.549 915.227,613.042 934.305,618.816" fill="hsl(231 88% 60%)" opacity="0.230"/>+<polygon points="926.172,749.320 941.328,705.304 985.185,726.096" fill="hsl(230 88% 60%)" opacity="0.230"/>+<polygon points="825.558,634.217 833.067,536.251 860.659,616.256" fill="hsl(218 88% 60%)" opacity="0.230"/>+<polygon points="860.659,616.256 865.231,542.668 883.325,596.007" fill="hsl(224 88% 60%)" opacity="0.230"/>+<polygon points="868.801,413.347 809.079,496.569 769.133,470.923" fill="hsl(207 88% 60%)" opacity="0.230"/>+<polygon points="868.801,413.347 769.133,470.923 834.058,380.006" fill="hsl(206 88% 60%)" opacity="0.230"/>+<polygon points="906.049,373.842 868.801,413.347 834.058,380.006" fill="hsl(227 88% 60%)" opacity="0.230"/>+<polygon points="833.067,536.251 825.558,634.217 791.413,537.142" fill="hsl(217 88% 60%)" opacity="0.230"/>+<polygon points="731.345,662.781 692.378,554.491 743.326,543.879" fill="hsl(201 88% 60%)" opacity="0.230"/>+<polygon points="841.428,677.368 926.172,749.320 816.777,719.646" fill="hsl(208 88% 60%)" opacity="0.230"/>+<polygon points="985.185,726.096 1091.369,698.330 1003.272,771.965" fill="hsl(207 88% 60%)" opacity="0.230"/>+<polygon points="1016.470,615.548 971.504,650.715 978.903,626.402" fill="hsl(228 88% 60%)" opacity="0.230"/>+<polygon points="1016.470,615.548 978.903,626.402 994.784,614.125" fill="hsl(231 88% 60%)" opacity="0.230"/>+<polygon points="1066.359,659.641 973.839,684.781 1041.038,631.119" fill="hsl(219 88% 60%)" opacity="0.230"/>+<polygon points="1091.369,698.330 985.185,726.096 1066.359,659.641" fill="hsl(213 88% 60%)" opacity="0.230"/>+<polygon points="1043.355,548.453 1045.972,606.054 1030.517,585.156" fill="hsl(228 88% 60%)" opacity="0.230"/>+<polygon points="1115.066,641.998 1152.748,540.007 1162.616,656.290" fill="hsl(205 88% 60%)" opacity="0.230"/>+<polygon points="1043.355,548.453 1030.517,585.156 1029.736,564.916" fill="hsl(232 88% 60%)" opacity="0.230"/>+<polygon points="1152.748,540.007 1115.066,641.998 1107.369,535.862" fill="hsl(213 88% 60%)" opacity="0.230"/>+<polygon points="1129.979,485.008 1068.207,396.285 1170.914,456.336" fill="hsl(203 88% 60%)" opacity="0.230"/>+<polygon points="1170.914,456.336 1068.207,396.285 1106.050,362.743" fill="hsl(203 88% 60%)" opacity="0.230"/>+<polygon points="1162.616,656.290 1152.748,540.007 1202.785,548.966" fill="hsl(204 88% 60%)" opacity="0.230"/>+<polygon points="1003.272,771.965 1091.369,698.330 1115.469,743.362" fill="hsl(205 88% 60%)" opacity="0.230"/>+<polygon points="926.172,749.320 841.428,677.368 941.328,705.304" fill="hsl(212 88% 60%)" opacity="0.230"/>+<polygon points="941.328,705.304 866.681,643.482 948.510,667.186" fill="hsl(219 88% 60%)" opacity="0.230"/>+<polygon points="791.413,537.142 825.558,634.217 781.137,649.633" fill="hsl(210 88% 60%)" opacity="0.230"/>+<polygon points="692.378,554.491 731.345,662.781 680.396,673.393" fill="hsl(201 88% 60%)" opacity="0.230"/>+<polygon points="891.810,621.371 946.088,637.549 948.510,667.186" fill="hsl(225 88% 60%)" opacity="0.230"/>+<polygon points="816.777,719.646 905.691,796.353 793.444,766.277" fill="hsl(202 88% 60%)" opacity="0.230"/>+<polygon points="915.227,613.042 946.088,637.549 891.810,621.371" fill="hsl(228 88% 60%)" opacity="0.230"/>+<polygon points="905.691,796.353 816.777,719.646 926.172,749.320" fill="hsl(204 88% 60%)" opacity="0.230"/>+<polygon points="1003.272,771.965 1115.469,743.362 1025.396,819.267" fill="hsl(202 88% 60%)" opacity="0.230"/>+<polygon points="1213.493,668.306 1202.785,548.966 1253.661,560.982" fill="hsl(201 88% 60%)" opacity="0.170"/>+<polygon points="1025.396,819.267 1115.469,743.362 1137.592,790.664" fill="hsl(202 88% 60%)" opacity="0.230"/>+<polygon points="793.444,766.277 905.691,796.353 882.359,842.983" fill="hsl(202 88% 60%)" opacity="0.230"/>+<polygon points="781.137,649.633 743.326,543.879 791.413,537.142" fill="hsl(207 88% 60%)" opacity="0.230"/>+<polygon points="865.231,542.668 860.659,616.256 833.067,536.251" fill="hsl(223 88% 60%)" opacity="0.230"/>+<polygon points="692.378,554.491 680.396,673.393 642.585,567.639" fill="hsl(202 88% 60%)" opacity="0.230"/>+<polygon points="793.444,766.277 882.359,842.983 772.964,813.309" fill="hsl(204 88% 60%)" opacity="0.230"/>+<polygon points="1025.396,819.267 1137.592,790.664 1049.496,864.300" fill="hsl(205 88% 60%)" opacity="0.230"/>+<polygon points="1213.493,668.306 1253.661,560.982 1263.530,677.265" fill="hsl(204 88% 60%)" opacity="0.230"/>+<polygon points="1211.934,423.876 1147.069,330.282 1249.776,390.334" fill="hsl(203 88% 60%)" opacity="0.230"/>+<polygon points="1249.776,390.334 1147.069,330.282 1188.004,301.611" fill="hsl(203 88% 60%)" opacity="0.230"/>+<polygon points="1263.530,677.265 1253.661,560.982 1301.211,575.274" fill="hsl(205 88% 60%)" opacity="0.230"/>+<polygon points="1049.496,864.300 1137.592,790.664 1155.679,836.533" fill="hsl(207 88% 60%)" opacity="0.230"/>+<polygon points="772.964,813.309 882.359,842.983 857.708,885.262" fill="hsl(208 88% 60%)" opacity="0.230"/>+<polygon points="743.326,543.879 781.137,649.633 731.345,662.781" fill="hsl(202 88% 60%)" opacity="0.230"/>+<polygon points="642.585,567.639 680.396,673.393 632.310,680.130" fill="hsl(207 88% 60%)" opacity="0.230"/>+<polygon points="883.325,596.007 865.231,542.668 885.389,556.265" fill="hsl(229 88% 60%)" opacity="0.230"/>+<polygon points="752.883,315.696 687.959,406.613 653.215,373.272" fill="hsl(206 88% 60%)" opacity="0.230"/>+<polygon points="752.883,315.696 653.215,373.272 712.938,290.050" fill="hsl(207 88% 60%)" opacity="0.230"/>+<polygon points="642.585,567.639 632.310,680.130 598.165,583.055" fill="hsl(210 88% 60%)" opacity="0.230"/>+<polygon points="772.964,813.309 857.708,885.262 757.808,857.325" fill="hsl(211 88% 60%)" opacity="0.230"/>+<polygon points="1049.496,864.300 1155.679,836.533 1074.505,902.988" fill="hsl(212 88% 60%)" opacity="0.230"/>+<polygon points="1263.530,677.265 1301.211,575.274 1308.909,681.410" fill="hsl(213 88% 60%)" opacity="0.230"/>+<polygon points="1249.776,390.334 1188.004,301.611 1280.671,357.543" fill="hsl(212 88% 60%)" opacity="0.230"/>+<polygon points="1013.951,221.118 906.049,221.118 912.747,177.103" fill="hsl(211 88% 60%)" opacity="0.230"/>+<polygon points="1013.951,221.118 912.747,177.103 1007.253,177.103" fill="hsl(211 88% 60%)" opacity="0.230"/>+<polygon points="1280.671,357.543 1188.004,301.611 1226.325,280.038" fill="hsl(213 88% 60%)" opacity="0.230"/>+<polygon points="1308.909,681.410 1301.211,575.274 1341.288,591.973" fill="hsl(215 88% 60%)" opacity="0.230"/>+<polygon points="1074.505,902.988 1155.679,836.533 1167.025,877.848" fill="hsl(215 88% 60%)" opacity="0.230"/>+<polygon points="757.808,857.325 857.708,885.262 832.455,919.147" fill="hsl(216 88% 60%)" opacity="0.230"/>+<polygon points="598.165,583.055 632.310,680.130 590.656,681.021" fill="hsl(217 88% 60%)" opacity="0.230"/>+<polygon points="712.938,290.050 653.215,373.272 627.315,341.417" fill="hsl(216 88% 60%)" opacity="0.230"/>+<polygon points="712.938,290.050 627.315,341.417 676.658,273.057" fill="hsl(217 88% 60%)" opacity="0.230"/>+<polygon points="598.165,583.055 590.656,681.021 563.063,601.016" fill="hsl(218 88% 60%)" opacity="0.230"/>+<polygon points="757.808,857.325 832.455,919.147 750.625,895.443" fill="hsl(219 88% 60%)" opacity="0.230"/>+<polygon points="1074.505,902.988 1167.025,877.848 1099.827,931.510" fill="hsl(219 88% 60%)" opacity="0.230"/>+<polygon points="1308.909,681.410 1341.288,591.973 1346.307,678.857" fill="hsl(220 88% 60%)" opacity="0.230"/>+<polygon points="1280.671,357.543 1226.325,280.038 1301.129,326.923" fill="hsl(220 88% 60%)" opacity="0.230"/>+<polygon points="1007.253,177.103 912.747,177.103 924.305,142.841" fill="hsl(220 88% 60%)" opacity="0.230"/>+<polygon points="1007.253,177.103 924.305,142.841 995.695,142.841" fill="hsl(220 88% 60%)" opacity="0.230"/>+<polygon points="1301.129,326.923 1226.325,280.038 1259.898,268.527" fill="hsl(222 88% 60%)" opacity="0.230"/>+<polygon points="1346.307,678.857 1341.288,591.973 1370.306,611.218" fill="hsl(222 88% 60%)" opacity="0.230"/>+<polygon points="1099.827,931.510 1167.025,877.848 1169.360,911.914" fill="hsl(222 88% 60%)" opacity="0.230"/>+<polygon points="750.625,895.443 832.455,919.147 807.326,941.258" fill="hsl(223 88% 60%)" opacity="0.230"/>+<polygon points="563.063,601.016 590.656,681.021 558.492,674.604" fill="hsl(223 88% 60%)" opacity="0.230"/>+<polygon points="676.658,273.057 627.315,341.417 613.214,312.551" fill="hsl(223 88% 60%)" opacity="0.230"/>+<polygon points="676.658,273.057 613.214,312.551 646.178,267.101" fill="hsl(224 88% 60%)" opacity="0.230"/>+<polygon points="563.063,601.016 558.492,674.604 540.398,621.265" fill="hsl(224 88% 60%)" opacity="0.230"/>+<polygon points="750.625,895.443 807.326,941.258 753.048,925.081" fill="hsl(225 88% 60%)" opacity="0.230"/>+<polygon points="1099.827,931.510 1169.360,911.914 1124.394,947.081" fill="hsl(225 88% 60%)" opacity="0.230"/>+<polygon points="1346.307,678.857 1370.306,611.218 1372.922,668.819" fill="hsl(226 88% 60%)" opacity="0.230"/>+<polygon points="1301.129,326.923 1259.898,268.527 1308.905,300.219" fill="hsl(226 88% 60%)" opacity="0.230"/>+<polygon points="924.305,142.841 979.353,121.106 995.695,142.841" fill="hsl(227 88% 60%)" opacity="0.230"/>+<polygon points="979.353,121.106 924.305,142.841 940.647,121.106" fill="hsl(227 88% 60%)" opacity="0.230"/>+<polygon points="1308.905,300.219 1259.898,268.527 1286.483,268.614" fill="hsl(227 88% 60%)" opacity="0.230"/>+<polygon points="1372.922,668.819 1370.306,611.218 1385.760,632.116" fill="hsl(228 88% 60%)" opacity="0.230"/>+<polygon points="1124.394,947.081 1169.360,911.914 1161.961,936.228" fill="hsl(228 88% 60%)" opacity="0.230"/>+<polygon points="753.048,925.081 807.326,941.258 783.908,949.587" fill="hsl(228 88% 60%)" opacity="0.230"/>+<polygon points="540.398,621.265 558.492,674.604 538.334,661.008" fill="hsl(229 88% 60%)" opacity="0.230"/>+<polygon points="646.178,267.101 613.214,312.551 612.245,288.776" fill="hsl(229 88% 60%)" opacity="0.230"/>+<polygon points="646.178,267.101 612.245,288.776 623.876,272.780" fill="hsl(230 88% 60%)" opacity="0.230"/>+<polygon points="540.398,621.265 538.334,661.008 532.015,642.208" fill="hsl(230 88% 60%)" opacity="0.230"/>+<polygon points="753.048,925.081 783.908,949.587 764.831,943.813" fill="hsl(231 88% 60%)" opacity="0.230"/>+<polygon points="1124.394,947.081 1161.961,936.228 1146.080,948.504" fill="hsl(232 88% 60%)" opacity="0.230"/>+<polygon points="1372.922,668.819 1385.760,632.116 1386.541,652.356" fill="hsl(232 88% 60%)" opacity="0.230"/>+<polygon points="1308.905,300.219 1286.483,268.614 1303.589,279.869" fill="hsl(234 88% 60%)" opacity="0.230"/>+<polygon points="979.353,121.106 940.647,121.106 960.000,113.660" fill="hsl(235 88% 60%)" opacity="0.230"/>+<polygon points="903.184,323.640 906.049,373.842 834.058,380.006" fill="hsl(222 88% 60%)" opacity="0.230"/>+<polygon points="794.221,346.709 903.184,323.640 834.058,380.006" fill="hsl(209 88% 60%)" opacity="0.230"/>+<polygon points="769.133,470.923 809.079,496.569 791.413,537.142" fill="hsl(228 88% 60%)" opacity="0.230"/>+<polygon points="769.133,470.923 743.326,543.879 727.796,439.910" fill="hsl(213 88% 60%)" opacity="0.230"/>+<polygon points="743.326,543.879 769.133,470.923 791.413,537.142" fill="hsl(225 88% 60%)" opacity="0.230"/>+<polygon points="692.378,554.491 727.796,439.910 743.326,543.879" fill="hsl(202 88% 60%)" opacity="0.230"/>+<polygon points="781.137,649.633 825.558,634.217 841.428,677.368" fill="hsl(229 88% 60%)" opacity="0.230"/>+<polygon points="781.137,649.633 816.777,719.646 731.345,662.781" fill="hsl(215 88% 60%)" opacity="0.230"/>+<polygon points="816.777,719.646 781.137,649.633 841.428,677.368" fill="hsl(225 88% 60%)" opacity="0.230"/>+<polygon points="926.172,749.320 1003.272,771.965 905.691,796.353" fill="hsl(218 88% 60%)" opacity="0.230"/>+<polygon points="1003.272,771.965 926.172,749.320 985.185,726.096" fill="hsl(224 88% 60%)" opacity="0.230"/>+<polygon points="1091.369,698.330 1162.616,656.290 1115.469,743.362" fill="hsl(218 88% 60%)" opacity="0.230"/>+<polygon points="1162.616,656.290 1091.369,698.330 1115.066,641.998" fill="hsl(224 88% 60%)" opacity="0.230"/>+<polygon points="1107.369,535.862 1129.979,485.008 1152.748,540.007" fill="hsl(230 88% 60%)" opacity="0.230"/>+<polygon points="1152.748,540.007 1170.914,456.336 1202.785,548.966" fill="hsl(218 88% 60%)" opacity="0.230"/>+<polygon points="1129.979,485.008 1107.369,535.862 1091.658,506.581" fill="hsl(237 88% 60%)" opacity="0.230"/>+<polygon points="1170.914,456.336 1152.748,540.007 1129.979,485.008" fill="hsl(223 88% 60%)" opacity="0.230"/>+<polygon points="1037.312,429.076 1013.951,373.842 1068.207,396.285" fill="hsl(229 88% 60%)" opacity="0.230"/>+<polygon points="1068.207,396.285 1016.816,323.640 1106.050,362.743" fill="hsl(219 88% 60%)" opacity="0.230"/>+<polygon points="1013.951,373.842 1037.312,429.076 1007.253,417.857" fill="hsl(231 88% 60%)" opacity="0.230"/>+<polygon points="1016.816,323.640 1068.207,396.285 1013.951,373.842" fill="hsl(221 88% 60%)" opacity="0.230"/>+<line x1="960.000" y1="552.000" x2="996.876" y2="509.114" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="1011.832" y2="529.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="996.876" y1="509.114" x2="1011.832" y2="529.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="1010.016" y2="578.409" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1010.016" y1="578.409" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1011.832" y1="529.361" x2="1016.521" y2="554.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="996.876" y1="509.114" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="974.617" y1="497.361" x2="949.463" y2="496.430" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="949.463" y2="496.430" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="993.605" y2="597.495" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1010.016" y1="578.409" x2="993.605" y2="597.495" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="949.463" y1="496.430" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="970.537" y2="607.570" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="993.605" y1="597.495" x2="970.537" y2="607.570" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="926.395" y1="506.505" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="945.383" y2="606.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="970.537" y1="607.570" x2="945.383" y2="606.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="909.984" y1="525.591" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="908.168" y1="574.639" x2="960.000" y2="552.000" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="903.479" y1="549.908" x2="908.168" y2="574.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="552.000" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="908.168" y1="574.639" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="945.383" y1="606.639" x2="923.124" y2="594.886" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="974.617" y1="497.361" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="949.463" y1="496.430" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1011.832" y1="529.361" x2="1014.395" y2="506.750" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="996.876" y1="509.114" x2="1014.395" y2="506.750" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.521" y1="554.092" x2="1029.736" y2="564.916" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.521" y1="554.092" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1010.016" y1="578.409" x2="1029.736" y2="564.916" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="993.605" y1="597.495" x2="994.784" y2="614.125" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="993.605" y1="597.495" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="970.537" y1="607.570" x2="994.784" y2="614.125" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="923.124" y1="594.886" x2="934.305" y2="618.816" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="945.383" y1="606.639" x2="934.305" y2="618.816" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="903.479" y1="549.908" x2="891.708" y2="575.064" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="908.168" y1="574.639" x2="891.708" y2="575.064" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="909.772" y1="497.843" x2="909.984" y2="525.591" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="909.984" y1="525.591" x2="898.140" y2="513.838" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="940.647" y1="473.854" x2="926.395" y2="506.505" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="926.395" y1="506.505" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="909.772" y1="497.843" x2="898.140" y2="513.838" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="903.479" y1="549.908" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="891.708" y1="575.064" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="923.124" y1="594.886" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="934.305" y1="618.816" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="970.537" y1="607.570" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="994.784" y1="614.125" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1010.016" y1="578.409" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1029.736" y1="564.916" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1011.832" y1="529.361" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1014.395" y1="506.750" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1009.078" y1="486.399" x2="974.617" y2="497.361" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="974.617" y1="497.361" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="979.353" y1="473.854" x2="960.000" y2="481.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="960.000" y1="481.300" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="949.463" y1="496.430" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="940.647" y1="473.854" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1014.395" y1="506.750" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="996.876" y1="509.114" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1009.078" y1="486.399" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1029.736" y1="564.916" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1011.832" y1="529.361" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1043.355" y1="548.453" x2="1031.501" y2="518.004" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="994.784" y1="614.125" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1010.016" y1="578.409" x2="1016.470" y2="615.548" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.470" y1="615.548" x2="1030.517" y2="585.156" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="945.383" y1="606.639" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="970.537" y1="607.570" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="934.305" y1="618.816" x2="946.088" y2="637.549" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="946.088" y1="637.549" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="908.168" y1="574.639" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="923.124" y1="594.886" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="891.708" y1="575.064" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="883.325" y1="596.007" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="898.140" y1="513.838" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="909.984" y1="525.591" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="875.839" y1="519.518" x2="903.479" y2="549.908" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="875.839" y1="519.518" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="940.647" y1="473.854" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="909.772" y1="497.843" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="875.839" y1="519.518" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="885.389" y1="556.265" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="883.325" y1="596.007" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="915.227" y1="613.042" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="946.088" y1="637.549" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="978.903" y1="626.402" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.470" y1="615.548" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1030.517" y1="585.156" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1043.355" y1="548.453" x2="1058.086" y2="518.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1031.501" y1="518.004" x2="1058.086" y2="518.092" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="995.695" y1="452.119" x2="940.647" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="940.647" y1="473.854" x2="924.305" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="924.305" y1="452.119" x2="908.803" y2="474.067" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1009.078" y1="486.399" x2="995.695" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="979.353" y1="473.854" x2="995.695" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1009.078" y1="486.399" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="995.695" y1="452.119" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1043.355" y1="548.453" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1058.086" y1="518.092" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.470" y1="615.548" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1045.972" y1="606.054" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="946.088" y1="637.549" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="971.504" y1="650.715" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="883.325" y1="596.007" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="891.810" y1="621.371" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="908.803" y1="474.067" x2="875.839" y2="519.518" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="875.839" y1="519.518" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="845.359" y1="513.562" x2="865.231" y2="542.668" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="924.305" y1="452.119" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="908.803" y1="474.067" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="845.359" y1="513.562" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="865.231" y1="542.668" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="860.659" y1="616.256" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="891.810" y1="621.371" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.470" y1="615.548" x2="971.504" y2="650.715" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="971.504" y1="650.715" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="971.504" y1="650.715" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="948.510" y1="667.186" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1043.355" y1="548.453" x2="1045.972" y2="606.054" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1045.972" y1="606.054" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1045.972" y1="606.054" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1041.038" y1="631.119" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1058.086" y1="518.092" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1058.086" y1="518.092" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1069.970" y1="538.415" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="995.695" y1="452.119" x2="924.305" y2="452.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="924.305" y1="452.119" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="912.747" y1="417.857" x2="894.702" y2="445.202" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.855" y1="459.695" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="995.695" y1="452.119" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1037.312" y1="429.076" x2="1091.658" y2="506.581" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1031.501" y1="518.004" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1037.312" y1="429.076" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1037.312" y1="429.076" x2="1007.253" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1069.970" y1="538.415" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1091.658" y1="506.581" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1041.038" y1="631.119" x2="1066.359" y2="659.641" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1074.989" y1="625.299" x2="1066.359" y2="659.641" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="948.510" y1="667.186" x2="941.328" y2="705.304" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="973.839" y1="684.781" x2="941.328" y2="705.304" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="860.659" y1="616.256" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="860.659" y1="616.256" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="825.558" y1="634.217" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="908.803" y1="474.067" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="875.839" y1="519.518" x2="909.772" y2="497.843" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="894.702" y1="445.202" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="894.702" y1="445.202" x2="809.079" y2="496.569" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="809.079" y1="496.569" x2="845.359" y2="513.562" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="809.079" y1="496.569" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="912.747" y1="417.857" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="894.702" y1="445.202" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="868.801" y1="413.347" x2="809.079" y2="496.569" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="809.079" y1="496.569" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="833.067" y1="536.251" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="948.510" y1="667.186" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="866.681" y1="643.482" x2="948.510" y2="667.186" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="866.681" y1="643.482" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="825.558" y1="634.217" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="973.839" y1="684.781" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="973.839" y1="684.781" x2="1041.038" y2="631.119" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="941.328" y1="705.304" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1074.989" y1="625.299" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1074.989" y1="625.299" x2="1069.970" y2="538.415" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1066.359" y1="659.641" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1091.658" y1="506.581" x2="1016.855" y2="459.695" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1058.086" y1="518.092" x2="1009.078" y2="486.399" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1091.658" y1="506.581" x2="1129.979" y2="485.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1129.979" y1="485.008" x2="1037.312" y2="429.076" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="995.695" y1="452.119" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="940.647" y1="473.854" x2="979.353" y2="473.854" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1007.253" y1="417.857" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1007.253" y1="417.857" x2="906.049" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="906.049" y1="373.842" x2="912.747" y2="417.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="906.049" y1="373.842" x2="868.801" y2="413.347" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1007.253" y1="417.857" x2="1013.951" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="906.049" y1="373.842" x2="1013.951" y2="373.842" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1129.979" y1="485.008" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1037.312" y1="429.076" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1152.748" y1="540.007" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1107.369" y1="535.862" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1066.359" y1="659.641" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1115.066" y1="641.998" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="841.428" y1="677.368" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="941.328" y1="705.304" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="926.172" y1="749.320" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="791.413" y1="537.142" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="825.558" y1="634.217" x2="833.067" y2="536.251" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="781.137" y1="649.633" x2="825.558" y2="634.217" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="868.801" y1="413.347" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="809.079" y1="496.569" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="868.801" y1="413.347" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="769.133" y1="470.923" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="906.049" y1="373.842" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="865.231" y1="542.668" x2="883.325" y2="596.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="865.231" y1="542.668" x2="860.659" y2="616.256" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="791.413" y1="537.142" x2="743.326" y2="543.879" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="841.428" y1="677.368" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="941.328" y1="705.304" x2="866.681" y2="643.482" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="985.185" y1="726.096" x2="1003.272" y2="771.965" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1066.359" y1="659.641" x2="973.839" y2="684.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.470" y1="615.548" x2="978.903" y2="626.402" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1066.359" y1="659.641" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1091.369" y1="698.330" x2="985.185" y2="726.096" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1003.272" y1="771.965" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1107.369" y1="535.862" x2="1074.989" y2="625.299" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1030.517" y1="585.156" x2="1043.355" y2="548.453" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1115.066" y1="641.998" x2="1162.616" y2="656.290" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1107.369" y1="535.862" x2="1115.066" y2="641.998" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1162.616" y1="656.290" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1129.979" y1="485.008" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1068.207" y1="396.285" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1013.951" y1="373.842" x2="903.184" y2="323.640" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="906.049" y1="373.842" x2="903.184" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1013.951" y1="373.842" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="903.184" y1="323.640" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1170.914" y1="456.336" x2="1106.050" y2="362.743" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1068.207" y1="396.285" x2="1106.050" y2="362.743" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1162.616" y1="656.290" x2="1202.785" y2="548.966" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1152.748" y1="540.007" x2="1202.785" y2="548.966" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1003.272" y1="771.965" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1091.369" y1="698.330" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="905.691" y1="796.353" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="926.172" y1="749.320" x2="816.777" y2="719.646" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="905.691" y1="796.353" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="791.413" y1="537.142" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="781.137" y1="649.633" x2="731.345" y2="662.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="834.058" y1="380.006" x2="727.796" y2="439.910" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="769.133" y1="470.923" x2="727.796" y2="439.910" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="834.058" y1="380.006" x2="794.221" y2="346.709" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="727.796" y1="439.910" x2="794.221" y2="346.709" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="743.326" y1="543.879" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="731.345" y1="662.781" x2="743.326" y2="543.879" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="941.328" y1="705.304" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="946.088" y1="637.549" x2="915.227" y2="613.042" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="816.777" y1="719.646" x2="793.444" y2="766.277" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="946.088" y1="637.549" x2="891.810" y2="621.371" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="793.444" y1="766.277" x2="905.691" y2="796.353" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1003.272" y1="771.965" x2="1025.396" y2="819.267" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1115.469" y1="743.362" x2="1025.396" y2="819.267" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1162.616" y1="656.290" x2="1213.493" y2="668.306" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1202.785" y1="548.966" x2="1213.493" y2="668.306" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1170.914" y1="456.336" x2="1211.934" y2="423.876" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1106.050" y1="362.743" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1016.816" y1="323.640" x2="903.184" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="903.184" y1="323.640" x2="903.184" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.816" y1="323.640" x2="1016.816" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="903.184" y1="271.320" x2="1016.816" y2="271.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1211.934" y1="423.876" x2="1147.069" y2="330.282" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1106.050" y1="362.743" x2="1147.069" y2="330.282" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1213.493" y1="668.306" x2="1253.661" y2="560.982" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1202.785" y1="548.966" x2="1253.661" y2="560.982" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1025.396" y1="819.267" x2="1137.592" y2="790.664" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1115.469" y1="743.362" x2="1137.592" y2="790.664" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="793.444" y1="766.277" x2="882.359" y2="842.983" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="905.691" y1="796.353" x2="882.359" y2="842.983" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="743.326" y1="543.879" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="731.345" y1="662.781" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="794.221" y1="346.709" x2="687.959" y2="406.613" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="727.796" y1="439.910" x2="687.959" y2="406.613" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="794.221" y1="346.709" x2="752.883" y2="315.696" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="687.959" y1="406.613" x2="752.883" y2="315.696" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="731.345" y1="662.781" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="692.378" y1="554.491" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="692.378" y1="554.491" x2="642.585" y2="567.639" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="793.444" y1="766.277" x2="772.964" y2="813.309" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="882.359" y1="842.983" x2="772.964" y2="813.309" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1025.396" y1="819.267" x2="1049.496" y2="864.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1137.592" y1="790.664" x2="1049.496" y2="864.300" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1213.493" y1="668.306" x2="1263.530" y2="677.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1253.661" y1="560.982" x2="1263.530" y2="677.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1211.934" y1="423.876" x2="1249.776" y2="390.334" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1147.069" y1="330.282" x2="1249.776" y2="390.334" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.816" y1="271.320" x2="906.049" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="903.184" y1="271.320" x2="906.049" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1016.816" y1="271.320" x2="1013.951" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="906.049" y1="221.118" x2="1013.951" y2="221.118" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1249.776" y1="390.334" x2="1188.004" y2="301.611" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1147.069" y1="330.282" x2="1188.004" y2="301.611" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1263.530" y1="677.265" x2="1301.211" y2="575.274" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1253.661" y1="560.982" x2="1301.211" y2="575.274" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1049.496" y1="864.300" x2="1155.679" y2="836.533" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1137.592" y1="790.664" x2="1155.679" y2="836.533" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="772.964" y1="813.309" x2="857.708" y2="885.262" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="882.359" y1="842.983" x2="857.708" y2="885.262" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="642.585" y1="567.639" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="883.325" y1="596.007" x2="885.389" y2="556.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="642.585" y1="567.639" x2="632.310" y2="680.130" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="632.310" y1="680.130" x2="680.396" y2="673.393" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="752.883" y1="315.696" x2="653.215" y2="373.272" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="687.959" y1="406.613" x2="653.215" y2="373.272" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="752.883" y1="315.696" x2="712.938" y2="290.050" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="653.215" y1="373.272" x2="712.938" y2="290.050" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="642.585" y1="567.639" x2="598.165" y2="583.055" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="632.310" y1="680.130" x2="598.165" y2="583.055" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="772.964" y1="813.309" x2="757.808" y2="857.325" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="857.708" y1="885.262" x2="757.808" y2="857.325" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1049.496" y1="864.300" x2="1074.505" y2="902.988" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1155.679" y1="836.533" x2="1074.505" y2="902.988" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1263.530" y1="677.265" x2="1308.909" y2="681.410" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1301.211" y1="575.274" x2="1308.909" y2="681.410" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1249.776" y1="390.334" x2="1280.671" y2="357.543" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1188.004" y1="301.611" x2="1280.671" y2="357.543" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1013.951" y1="221.118" x2="912.747" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="906.049" y1="221.118" x2="912.747" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1013.951" y1="221.118" x2="1007.253" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="912.747" y1="177.103" x2="1007.253" y2="177.103" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1280.671" y1="357.543" x2="1226.325" y2="280.038" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1188.004" y1="301.611" x2="1226.325" y2="280.038" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1308.909" y1="681.410" x2="1341.288" y2="591.973" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1301.211" y1="575.274" x2="1341.288" y2="591.973" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1074.505" y1="902.988" x2="1167.025" y2="877.848" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1155.679" y1="836.533" x2="1167.025" y2="877.848" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="757.808" y1="857.325" x2="832.455" y2="919.147" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="857.708" y1="885.262" x2="832.455" y2="919.147" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="598.165" y1="583.055" x2="590.656" y2="681.021" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="632.310" y1="680.130" x2="590.656" y2="681.021" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="712.938" y1="290.050" x2="627.315" y2="341.417" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="653.215" y1="373.272" x2="627.315" y2="341.417" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="712.938" y1="290.050" x2="676.658" y2="273.057" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="627.315" y1="341.417" x2="676.658" y2="273.057" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="598.165" y1="583.055" x2="563.063" y2="601.016" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="590.656" y1="681.021" x2="563.063" y2="601.016" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="757.808" y1="857.325" x2="750.625" y2="895.443" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="832.455" y1="919.147" x2="750.625" y2="895.443" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1074.505" y1="902.988" x2="1099.827" y2="931.510" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1167.025" y1="877.848" x2="1099.827" y2="931.510" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1308.909" y1="681.410" x2="1346.307" y2="678.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1341.288" y1="591.973" x2="1346.307" y2="678.857" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1280.671" y1="357.543" x2="1301.129" y2="326.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1226.325" y1="280.038" x2="1301.129" y2="326.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1007.253" y1="177.103" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="912.747" y1="177.103" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1007.253" y1="177.103" x2="995.695" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="924.305" y1="142.841" x2="995.695" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1301.129" y1="326.923" x2="1259.898" y2="268.527" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1226.325" y1="280.038" x2="1259.898" y2="268.527" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1346.307" y1="678.857" x2="1370.306" y2="611.218" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1341.288" y1="591.973" x2="1370.306" y2="611.218" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1099.827" y1="931.510" x2="1169.360" y2="911.914" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1167.025" y1="877.848" x2="1169.360" y2="911.914" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="750.625" y1="895.443" x2="807.326" y2="941.258" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="832.455" y1="919.147" x2="807.326" y2="941.258" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="563.063" y1="601.016" x2="558.492" y2="674.604" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="590.656" y1="681.021" x2="558.492" y2="674.604" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="676.658" y1="273.057" x2="613.214" y2="312.551" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="627.315" y1="341.417" x2="613.214" y2="312.551" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="676.658" y1="273.057" x2="646.178" y2="267.101" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="613.214" y1="312.551" x2="646.178" y2="267.101" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="563.063" y1="601.016" x2="540.398" y2="621.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="558.492" y1="674.604" x2="540.398" y2="621.265" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="750.625" y1="895.443" x2="753.048" y2="925.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="807.326" y1="941.258" x2="753.048" y2="925.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1099.827" y1="931.510" x2="1124.394" y2="947.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1169.360" y1="911.914" x2="1124.394" y2="947.081" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1346.307" y1="678.857" x2="1372.922" y2="668.819" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1370.306" y1="611.218" x2="1372.922" y2="668.819" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1301.129" y1="326.923" x2="1308.905" y2="300.219" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1259.898" y1="268.527" x2="1308.905" y2="300.219" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="979.353" y1="121.106" x2="924.305" y2="142.841" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="924.305" y1="142.841" x2="940.647" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="995.695" y1="142.841" x2="979.353" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="940.647" y1="121.106" x2="979.353" y2="121.106" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1308.905" y1="300.219" x2="1286.483" y2="268.614" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1259.898" y1="268.527" x2="1286.483" y2="268.614" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1372.922" y1="668.819" x2="1385.760" y2="632.116" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1370.306" y1="611.218" x2="1385.760" y2="632.116" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1124.394" y1="947.081" x2="1161.961" y2="936.228" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1169.360" y1="911.914" x2="1161.961" y2="936.228" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="753.048" y1="925.081" x2="783.908" y2="949.587" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="807.326" y1="941.258" x2="783.908" y2="949.587" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="540.398" y1="621.265" x2="538.334" y2="661.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="558.492" y1="674.604" x2="538.334" y2="661.008" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="646.178" y1="267.101" x2="612.245" y2="288.776" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="613.214" y1="312.551" x2="612.245" y2="288.776" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="646.178" y1="267.101" x2="623.876" y2="272.780" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="612.245" y1="288.776" x2="623.876" y2="272.780" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="540.398" y1="621.265" x2="532.015" y2="642.208" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="538.334" y1="661.008" x2="532.015" y2="642.208" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="753.048" y1="925.081" x2="764.831" y2="943.813" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="783.908" y1="949.587" x2="764.831" y2="943.813" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1124.394" y1="947.081" x2="1146.080" y2="948.504" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1161.961" y1="936.228" x2="1146.080" y2="948.504" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1372.922" y1="668.819" x2="1386.541" y2="652.356" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1385.760" y1="632.116" x2="1386.541" y2="652.356" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1308.905" y1="300.219" x2="1303.589" y2="279.869" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1286.483" y1="268.614" x2="1303.589" y2="279.869" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="979.353" y1="121.106" x2="960.000" y2="113.660" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="940.647" y1="121.106" x2="960.000" y2="113.660" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="903.184" y1="323.640" x2="834.058" y2="380.006" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="794.221" y1="346.709" x2="903.184" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="794.221" y1="346.709" x2="903.184" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="903.184" y1="271.320" x2="752.883" y2="315.696" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="906.049" y1="221.118" x2="752.883" y2="315.696" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="906.049" y1="221.118" x2="712.938" y2="290.050" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="712.938" y1="290.050" x2="912.747" y2="177.103" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="912.747" y1="177.103" x2="676.658" y2="273.057" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="676.658" y1="273.057" x2="924.305" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="646.178" y1="267.101" x2="924.305" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="940.647" y1="121.106" x2="646.178" y2="267.101" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="940.647" y1="121.106" x2="623.876" y2="272.780" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="769.133" y1="470.923" x2="791.413" y2="537.142" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="743.326" y1="543.879" x2="769.133" y2="470.923" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="743.326" y1="543.879" x2="727.796" y2="439.910" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="727.796" y1="439.910" x2="692.378" y2="554.491" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="692.378" y1="554.491" x2="687.959" y2="406.613" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="687.959" y1="406.613" x2="642.585" y2="567.639" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="642.585" y1="567.639" x2="653.215" y2="373.272" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="653.215" y1="373.272" x2="598.165" y2="583.055" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="598.165" y1="583.055" x2="627.315" y2="341.417" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="563.063" y1="601.016" x2="627.315" y2="341.417" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="563.063" y1="601.016" x2="613.214" y2="312.551" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="540.398" y1="621.265" x2="613.214" y2="312.551" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="612.245" y1="288.776" x2="540.398" y2="621.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="612.245" y1="288.776" x2="532.015" y2="642.208" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="781.137" y1="649.633" x2="841.428" y2="677.368" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="816.777" y1="719.646" x2="781.137" y2="649.633" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="816.777" y1="719.646" x2="731.345" y2="662.781" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="731.345" y1="662.781" x2="793.444" y2="766.277" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="680.396" y1="673.393" x2="793.444" y2="766.277" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="680.396" y1="673.393" x2="772.964" y2="813.309" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="772.964" y1="813.309" x2="632.310" y2="680.130" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="632.310" y1="680.130" x2="757.808" y2="857.325" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="757.808" y1="857.325" x2="590.656" y2="681.021" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="750.625" y1="895.443" x2="590.656" y2="681.021" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="750.625" y1="895.443" x2="558.492" y2="674.604" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="753.048" y1="925.081" x2="558.492" y2="674.604" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="538.334" y1="661.008" x2="753.048" y2="925.081" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="538.334" y1="661.008" x2="764.831" y2="943.813" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1003.272" y1="771.965" x2="926.172" y2="749.320" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1003.272" y1="771.965" x2="905.691" y2="796.353" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="905.691" y1="796.353" x2="1025.396" y2="819.267" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="882.359" y1="842.983" x2="1025.396" y2="819.267" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="882.359" y1="842.983" x2="1049.496" y2="864.300" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="857.708" y1="885.262" x2="1049.496" y2="864.300" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="857.708" y1="885.262" x2="1074.505" y2="902.988" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1074.505" y1="902.988" x2="832.455" y2="919.147" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="832.455" y1="919.147" x2="1099.827" y2="931.510" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1099.827" y1="931.510" x2="807.326" y2="941.258" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1124.394" y1="947.081" x2="807.326" y2="941.258" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="783.908" y1="949.587" x2="1124.394" y2="947.081" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="783.908" y1="949.587" x2="1146.080" y2="948.504" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1162.616" y1="656.290" x2="1091.369" y2="698.330" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1162.616" y1="656.290" x2="1115.469" y2="743.362" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1115.469" y1="743.362" x2="1213.493" y2="668.306" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1137.592" y1="790.664" x2="1213.493" y2="668.306" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1137.592" y1="790.664" x2="1263.530" y2="677.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1155.679" y1="836.533" x2="1263.530" y2="677.265" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1155.679" y1="836.533" x2="1308.909" y2="681.410" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1308.909" y1="681.410" x2="1167.025" y2="877.848" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1167.025" y1="877.848" x2="1346.307" y2="678.857" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1346.307" y1="678.857" x2="1169.360" y2="911.914" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1372.922" y1="668.819" x2="1169.360" y2="911.914" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1161.961" y1="936.228" x2="1372.922" y2="668.819" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1161.961" y1="936.228" x2="1386.541" y2="652.356" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1129.979" y1="485.008" x2="1107.369" y2="535.862" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1129.979" y1="485.008" x2="1152.748" y2="540.007" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1152.748" y1="540.007" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1202.785" y1="548.966" x2="1170.914" y2="456.336" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1202.785" y1="548.966" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1253.661" y1="560.982" x2="1211.934" y2="423.876" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1253.661" y1="560.982" x2="1249.776" y2="390.334" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1301.211" y1="575.274" x2="1249.776" y2="390.334" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1301.211" y1="575.274" x2="1280.671" y2="357.543" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1280.671" y1="357.543" x2="1341.288" y2="591.973" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1341.288" y1="591.973" x2="1301.129" y2="326.923" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1301.129" y1="326.923" x2="1370.306" y2="611.218" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1308.905" y1="300.219" x2="1370.306" y2="611.218" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1385.760" y1="632.116" x2="1308.905" y2="300.219" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1013.951" y1="373.842" x2="1037.312" y2="429.076" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1013.951" y1="373.842" x2="1068.207" y2="396.285" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1068.207" y1="396.285" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1106.050" y1="362.743" x2="1016.816" y2="323.640" stroke="#ffd56a" stroke-width="2.000" stroke-linecap="round" opacity="0.880" filter="url(#edge-glow)"/>+<line x1="1106.050" y1="362.743" x2="1016.816" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1147.069" y1="330.282" x2="1016.816" y2="271.320" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1147.069" y1="330.282" x2="1013.951" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1188.004" y1="301.611" x2="1013.951" y2="221.118" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1188.004" y1="301.611" x2="1007.253" y2="177.103" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1007.253" y1="177.103" x2="1226.325" y2="280.038" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1226.325" y1="280.038" x2="995.695" y2="142.841" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="995.695" y1="142.841" x2="1259.898" y2="268.527" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="979.353" y1="121.106" x2="1259.898" y2="268.527" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<line x1="1286.483" y1="268.614" x2="979.353" y2="121.106" stroke="#dd78ff" stroke-width="0.750" stroke-linecap="round" opacity="0.060" filter="url(#edge-glow)"/>+<g filter="url(#hole-glow)"><path d="M 1013.951,373.842 L 906.049,373.842 L 903.184,323.640 L 1016.816,323.640 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>+<path d="M 834.058,380.006 L 769.133,470.923 L 727.796,439.910 L 794.221,346.709 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>+<path d="M 1162.616,656.290 L 1202.785,548.966 L 1213.493,668.306 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>+<path d="M 1170.914,456.336 L 1106.050,362.743 L 1147.069,330.282 L 1211.934,423.876 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>+<path d="M 1016.816,323.640 L 903.184,323.640 L 903.184,271.320 L 1016.816,271.320 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>+<path d="M 794.221,346.709 L 727.796,439.910 L 687.959,406.613 L 752.883,315.696 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>+<path d="M 1016.816,271.320 L 903.184,271.320 L 906.049,221.118 L 1013.951,221.118 Z" fill="none" stroke="url(#hole-stroke)" stroke-width="5.4" stroke-linejoin="round" opacity="0.98"/>+</g>+<circle cx="960.000" cy="552.000" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="996.876" cy="509.114" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1011.832" cy="529.361" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1010.016" cy="578.409" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1016.521" cy="554.092" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="974.617" cy="497.361" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="949.463" cy="496.430" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="993.605" cy="597.495" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="926.395" cy="506.505" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="970.537" cy="607.570" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="909.984" cy="525.591" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="945.383" cy="606.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="903.479" cy="549.908" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="908.168" cy="574.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="923.124" cy="594.886" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="960.000" cy="481.300" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1014.395" cy="506.750" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1029.736" cy="564.916" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="994.784" cy="614.125" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="934.305" cy="618.816" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="891.708" cy="575.064" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="898.140" cy="513.838" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="909.772" cy="497.843" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="885.389" cy="556.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="915.227" cy="613.042" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="978.903" cy="626.402" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1030.517" cy="585.156" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1031.501" cy="518.004" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="979.353" cy="473.854" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="940.647" cy="473.854" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1009.078" cy="486.399" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1043.355" cy="548.453" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1016.470" cy="615.548" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="946.088" cy="637.549" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="883.325" cy="596.007" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="875.839" cy="519.518" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="908.803" cy="474.067" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="865.231" cy="542.668" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="891.810" cy="621.371" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="971.504" cy="650.715" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1045.972" cy="606.054" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1058.086" cy="518.092" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="924.305" cy="452.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="995.695" cy="452.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1016.855" cy="459.695" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1069.970" cy="538.415" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1041.038" cy="631.119" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="948.510" cy="667.186" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="860.659" cy="616.256" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="845.359" cy="513.562" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="894.702" cy="445.202" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="833.067" cy="536.251" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="866.681" cy="643.482" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="973.839" cy="684.781" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1074.989" cy="625.299" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1091.658" cy="506.581" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="912.747" cy="417.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1007.253" cy="417.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1037.312" cy="429.076" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1107.369" cy="535.862" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1066.359" cy="659.641" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="941.328" cy="705.304" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="825.558" cy="634.217" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="809.079" cy="496.569" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="868.801" cy="413.347" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="791.413" cy="537.142" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="841.428" cy="677.368" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="985.185" cy="726.096" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1115.066" cy="641.998" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1129.979" cy="485.008" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="906.049" cy="373.842" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1013.951" cy="373.842" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1068.207" cy="396.285" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1152.748" cy="540.007" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1091.369" cy="698.330" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="926.172" cy="749.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="781.137" cy="649.633" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="769.133" cy="470.923" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="834.058" cy="380.006" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="743.326" cy="543.879" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="816.777" cy="719.646" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1003.272" cy="771.965" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1162.616" cy="656.290" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1170.914" cy="456.336" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="903.184" cy="323.640" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1016.816" cy="323.640" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1106.050" cy="362.743" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1202.785" cy="548.966" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1115.469" cy="743.362" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="905.691" cy="796.353" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="731.345" cy="662.781" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="727.796" cy="439.910" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="794.221" cy="346.709" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="692.378" cy="554.491" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="793.444" cy="766.277" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1025.396" cy="819.267" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1213.493" cy="668.306" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1211.934" cy="423.876" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="903.184" cy="271.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1016.816" cy="271.320" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1147.069" cy="330.282" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1253.661" cy="560.982" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1137.592" cy="790.664" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="882.359" cy="842.983" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="680.396" cy="673.393" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="687.959" cy="406.613" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="752.883" cy="315.696" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="642.585" cy="567.639" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="772.964" cy="813.309" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1049.496" cy="864.300" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1263.530" cy="677.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1249.776" cy="390.334" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="906.049" cy="221.118" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1013.951" cy="221.118" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1188.004" cy="301.611" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1301.211" cy="575.274" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1155.679" cy="836.533" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="857.708" cy="885.262" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="632.310" cy="680.130" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="653.215" cy="373.272" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="712.938" cy="290.050" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="598.165" cy="583.055" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="757.808" cy="857.325" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1074.505" cy="902.988" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1308.909" cy="681.410" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1280.671" cy="357.543" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="912.747" cy="177.103" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1007.253" cy="177.103" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1226.325" cy="280.038" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1341.288" cy="591.973" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1167.025" cy="877.848" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="832.455" cy="919.147" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="590.656" cy="681.021" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="627.315" cy="341.417" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="676.658" cy="273.057" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="563.063" cy="601.016" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="750.625" cy="895.443" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1099.827" cy="931.510" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1346.307" cy="678.857" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1301.129" cy="326.923" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="924.305" cy="142.841" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="995.695" cy="142.841" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1259.898" cy="268.527" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1370.306" cy="611.218" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1169.360" cy="911.914" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="807.326" cy="941.258" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="558.492" cy="674.604" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="613.214" cy="312.551" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="646.178" cy="267.101" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="540.398" cy="621.265" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="753.048" cy="925.081" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1124.394" cy="947.081" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1372.922" cy="668.819" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1308.905" cy="300.219" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="940.647" cy="121.106" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="979.353" cy="121.106" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1286.483" cy="268.614" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1385.760" cy="632.116" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1161.961" cy="936.228" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="783.908" cy="949.587" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="538.334" cy="661.008" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="612.245" cy="288.776" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="623.876" cy="272.780" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="532.015" cy="642.208" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="764.831" cy="943.813" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1146.080" cy="948.504" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1386.541" cy="652.356" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="1303.589" cy="279.869" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+<circle cx="960.000" cy="113.660" r="3.791" fill="#fff8d9" stroke="#ff9ad7" stroke-width="1.2" opacity="0.95"/>+</g>+<path d="M 504.232,637.038 L 511.333,619.556 L 515.575,612.082 L 529.764,595.098 L 550.582,579.300 L 577.273,565.013 L 608.822,552.264 L 644.022,540.909 L 681.558,530.816 L 720.080,522.024 L 758.277,514.831 L 781.582,511.624 L 764.940,500.588 L 733.646,477.535 L 702.754,452.899 L 673.315,427.519 L 646.527,402.016 L 623.634,376.841 L 605.785,352.389 L 601.543,344.915 L 589.641,321.648 L 584.226,300.190 L 585.414,281.358 L 592.914,266.105 L 597.156,258.631 L 610.357,247.925 L 628.453,242.576 L 637.139,242.576 L 659.264,243.081 L 684.596,249.507 L 712.407,261.467 L 742.044,278.184 L 772.869,298.625 L 804.163,321.679 L 835.056,346.315 L 864.495,371.694 L 880.355,386.794 L 878.544,366.177 L 877.056,327.337 L 877.056,287.823 L 878.544,248.983 L 881.781,212.139 L 887.190,178.544 L 895.179,149.343 L 905.949,125.532 L 910.191,118.058 L 923.591,100.445 L 939.056,89.632 L 955.657,85.986 L 964.343,85.986 L 980.944,89.632 L 996.409,100.445 L 1009.809,118.058 L 1014.051,125.532 L 1024.821,149.343 L 1032.810,178.544 L 1038.219,212.139 L 1041.456,248.983 L 1042.944,287.823 L 1042.944,327.337 L 1041.456,366.177 L 1039.645,386.794 L 1055.505,371.694 L 1084.944,346.315 L 1115.837,321.679 L 1147.131,298.625 L 1177.956,278.184 L 1207.593,261.467 L 1235.404,249.507 L 1260.736,243.081 L 1282.861,242.576 L 1291.547,242.576 L 1309.643,247.925 L 1322.844,258.631 L 1327.086,266.105 L 1334.586,281.358 L 1335.774,300.190 L 1330.359,321.648 L 1318.457,344.915 L 1314.215,352.389 L 1296.366,376.841 L 1273.473,402.016 L 1246.685,427.519 L 1217.246,452.899 L 1186.354,477.535 L 1155.060,500.588 L 1138.418,511.624 L 1161.723,514.831 L 1199.920,522.024 L 1238.442,530.816 L 1275.978,540.909 L 1311.178,552.264 L 1342.727,565.013 L 1369.418,579.300 L 1390.236,595.098 L 1404.425,612.082 L 1408.667,619.556 L 1415.768,637.038 L 1415.628,654.035 L 1408.380,669.409 L 1404.138,676.883 L 1390.155,689.553 L 1370.002,698.698 L 1344.390,703.900 L 1314.144,705.190 L 1305.458,705.190 L 1271.502,702.988 L 1234.861,697.946 L 1196.664,690.753 L 1158.141,681.961 L 1120.606,671.868 L 1102.029,665.875 L 1110.623,680.329 L 1114.865,687.803 L 1133.058,722.152 L 1150.202,757.752 L 1165.714,793.391 L 1178.783,827.992 L 1188.486,860.606 L 1193.958,890.381 L 1194.586,916.508 L 1190.155,938.190 L 1180.913,954.642 L 1176.671,962.116 L 1163.296,972.604 L 1146.757,976.523 L 1138.071,976.523 L 1119.446,973.490 L 1099.732,963.436 L 1079.696,946.655 L 1059.829,923.812 L 1040.380,895.892 L 1021.476,864.100 L 1017.234,856.626 L 999.042,822.278 L 981.898,786.678 L 966.386,751.039 L 960.000,734.132 L 953.614,751.039 L 938.102,786.678 L 920.958,822.278 L 902.766,856.626 L 898.524,864.100 L 879.620,895.892 L 860.171,923.812 L 840.304,946.655 L 820.268,963.436 L 800.554,973.490 L 781.929,976.523 L 773.243,976.523 L 756.704,972.604 L 743.329,962.116 L 739.087,954.642 L 729.845,938.190 L 725.414,916.508 L 726.042,890.381 L 731.514,860.606 L 741.217,827.992 L 754.286,793.391 L 769.798,757.752 L 786.942,722.152 L 805.135,687.803 L 809.377,680.329 L 817.971,665.875 L 799.394,671.868 L 761.859,681.961 L 723.336,690.753 L 685.139,697.946 L 648.498,702.988 L 614.542,705.190 L 605.856,705.190 L 575.610,703.900 L 549.998,698.698 L 529.845,689.553 L 515.862,676.883 L 511.620,669.409 L 504.372,654.035 Z" fill="none" stroke="url(#mask-stroke)" stroke-width="2.2" opacity="0.92" filter="url(#edge-glow)"/>+<g font-family="Inter, Avenir Next, Helvetica, Arial, sans-serif" fill="#f7f3ff">+<text x="72" y="82" font-size="24" font-weight="600" letter-spacing="8" fill="#9ff7ff">MOONLIGHT TRIANGULATION</text>+<text x="70" y="145" font-size="58" font-weight="780" letter-spacing="-1.5">PERSISTENCE ROSE</text>+<g transform="translate(1500 790)">+<text x="0" y="0" font-size="18" letter-spacing="2.4" fill="#91a8d8">TOPOLOGICAL HOLES AT THIS SCALE</text>+<text x="0" y="74" font-size="78" font-weight="720" fill="#fff5cd">β₁ = 7</text>+<text x="2" y="116" font-size="17" fill="#c9d3ff">7 independent one-dimensional holes</text>+<text x="2" y="144" font-size="15" fill="#9ba8d4">in the exact alpha complex</text>+<text x="2" y="176" font-size="14" fill="#7682aa">critical rank 538 · α² = 0.358</text>+<text x="2" y="200" font-size="12" fill="#596486">exact 94-digit / 94-digit rational</text>+</g>+</g>+</svg>
+ docs/social/moonlight-triangulation-persistence-rose.gif view

binary file changed (absent → 661966 bytes)

include/moonlight_triangulation.h view
@@ -89,6 +89,16 @@   ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE = 53,   ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH = 54,   ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE = 55,+  ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE = 56,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET = 57,+  ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP = 58,+  ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED = 59,+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE = 60,+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE = 61,+  ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE = 62,+  ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED = 63,+  ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED = 64,+  ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE = 65,   ML_OBSTRUCTION_NULL_POINTER = 100,   ML_OBSTRUCTION_COUNT_OVERFLOW = 101,   ML_OBSTRUCTION_BUFFER_TOO_SMALL = 102,
moonlight-triangulation.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.4 name:                moonlight-triangulation-version:             1.3.0.3+version:             1.4.0.1 synopsis:            Delaunay meshes and exact planar-region algebra. description:         Delaunay and constrained Delaunay triangulation as a lawful                      finite-set algebra, together with exact rational planar@@ -40,6 +40,10 @@ extra-doc-files:   README.md   CHANGELOG.md+  docs/README.md+  docs/moonlight-triangulation-alpha-eclipse.svg+  docs/moonlight-triangulation-persistence-rose.svg+  docs/social/moonlight-triangulation-persistence-rose.gif   bench/delaunay-compare/results/moonlight-delaunay-compare-small.svg extra-source-files:   weeder.toml@@ -73,7 +77,7 @@ source-repository this   type:     git   location: https://github.com/PaleRoses/moonlight.git-  tag:      moonlight-triangulation-1.3.0.3+  tag:      moonlight-triangulation-1.4.0.1   subdir:   moonlight-triangulation  flag warnings-as-errors@@ -212,8 +216,8 @@     base >= 4.22 && < 5     , containers >= 0.6 && < 0.9     , moonlight-algebra:abstract >= 0.1 && < 0.2-    , moonlight-homology >= 0.1 && < 0.2-    , moonlight-homology:cell-complex >= 0.1 && < 0.2+    , moonlight-homology >= 0.1.0.2 && < 0.2+    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2     , moonlight-triangulation:dcel     , vector >= 0.13 && < 0.14 @@ -381,7 +385,7 @@     moonlight_triangulation.h   build-depends:     base >= 4.19 && < 5-    , moonlight-triangulation:ffi >= 1.3 && < 1.4+    , moonlight-triangulation:ffi >= 1.4 && < 1.5   ghc-options: -threaded   if os(windows)     options: standalone@@ -499,8 +503,8 @@   build-depends:     base >= 4.22 && < 5     , containers >= 0.6 && < 0.9-    , moonlight-homology >= 0.1 && < 0.2-    , moonlight-homology:cell-complex >= 0.1 && < 0.2+    , moonlight-homology >= 0.1.0.2 && < 0.2+    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2     , moonlight-triangulation:core     , moonlight-triangulation:build     , moonlight-triangulation:cell-complex@@ -509,6 +513,34 @@     , tasty-hunit >= 0.10 && < 0.11     , vector >= 0.13 && < 0.14 +-- One pure, source-grounded projection of the alpha, dual, interpolation,+-- planar-algebra, and persistence owners. The test shares the internal exhibit+-- module; neither component creates a public visualization API.+test-suite moonlight-triangulation-persistence-rose-test+  import: shared-properties+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs:+    test/persistence-rose+    app/persistence-rose+  other-modules:+    Moonlight.Triangulation.Exhibit.PersistenceRose+  if impl(ghc < 9.14)+    buildable: False+  build-depends:+    base >= 4.22 && < 5+    , bytestring >= 0.12 && < 0.13+    , containers >= 0.6 && < 0.9+    , moonlight-homology >= 0.1.0.2 && < 0.2+    , moonlight-triangulation+    , moonlight-triangulation:cell-complex+    , moonlight-triangulation:dcel+    , moonlight-triangulation:dual+    , tasty >= 1.4 && < 1.6+    , tasty-hunit >= 0.10 && < 0.11+    , text >= 2.0 && < 2.2+    , vector >= 0.13 && < 0.14+ -- The single statement of the RTS environment every suite runs under: -N so -- that the concurrent tournament has more than one capability to be scheduled -- onto, -T so that the filtered-predicate test can read allocation counters.@@ -652,7 +684,9 @@ common triangulation-join-benchmark-slice   other-modules: Moonlight.Triangulation.JoinBench   build-depends:-    moonlight-triangulation:dcel+    containers >= 0.8 && < 0.9+    , moonlight-triangulation:core+    , moonlight-triangulation:dcel     , moonlight-triangulation:build  common triangulation-region-benchmark-slice@@ -723,13 +757,36 @@     , containers >= 0.6 && < 0.9     , moonlight-category >= 1.1.0.0 && < 1.2     , moonlight-category:simplicial >= 1.1.0.0 && < 1.2-    , moonlight-homology:cell-category >= 0.1 && < 0.2-    , moonlight-homology:cell-complex >= 0.1 && < 0.2+    , moonlight-homology:cell-category >= 0.1.0.2 && < 0.2+    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2     , moonlight-triangulation:build     , moonlight-triangulation:cell-complex     , moonlight-triangulation:dcel     , vector >= 0.13 && < 0.14 +executable moonlight-triangulation-persistence-rose+  import: shared-properties+  main-is: Main.hs+  hs-source-dirs: app/persistence-rose+  other-modules:+    Moonlight.Triangulation.Exhibit.PersistenceRose+  if impl(ghc < 9.14)+    buildable: False+  build-depends:+    base >= 4.22 && < 5+    , bytestring >= 0.12 && < 0.13+    , containers >= 0.6 && < 0.9+    , directory >= 1.3 && < 1.4+    , filepath >= 1.4 && < 1.6+    , moonlight-homology >= 0.1.0.2 && < 0.2+    , moonlight-triangulation+    , moonlight-triangulation:cell-complex+    , moonlight-triangulation:dcel+    , moonlight-triangulation:dual+    , text >= 2.0 && < 2.2+    , transformers >= 0.5 && < 0.7+    , vector >= 0.13 && < 0.14+ -- One exact Delaunay alpha filtration versus independently restricting and -- reducing the same chain complex at every critical radius. Construction of -- the shared Delaunay geometry is outside all timed lanes.@@ -749,7 +806,7 @@     base >= 4.22 && < 5     , containers >= 0.6 && < 0.9     , deepseq >= 1.5 && < 1.6-    , moonlight-homology >= 0.1 && < 0.2+    , moonlight-homology >= 0.1.0.2 && < 0.2     , moonlight-triangulation:build     , moonlight-triangulation:cell-complex     , moonlight-triangulation:dcel
src-build/Moonlight/Triangulation/BulkLoad.hs view
@@ -90,6 +90,7 @@     , triConstraint = emptyPaged     , triConstraintCount = 0     , triConstraintEdges = IntSet.empty+    , triSeamFrontier = Nothing     , triElementDefaults = defaults     } 
src-build/Moonlight/Triangulation/Cdt.hs view
@@ -10,7 +10,7 @@   , ConstraintBatchStats (..)   , ConstraintBatchResult (..)   , ConstrainedExtensionResult (..)-  , ConstrainedSeamSource (..)+  , ConstrainedSeamSide (..)   , ConstrainedSeamFaceEvidence   , constrainedSeamSourceFace   , constrainedSeamTargetFace@@ -22,13 +22,15 @@   , constrainedSeamConstraintRecovery   , ConstrainedSeamResult   , constrainedSeamResultTriangulation-  , constrainedSeamLeftFaceEvidence+  , constrainedSeamLeftFaceCount   , constrainedSeamRightFaceEvidence-  , constrainedSeamNewFaces-  , constrainedSeamLeftConstraintEvidence+  , constrainedSeamJoinFaces+  , constrainedSeamLeftConstraintCount   , constrainedSeamRightConstraintEvidence   , constrainedSeamConstraintStats   , constrainedSeamBuildStats+  , constrainedSeamPublicationStats+  , constrainedSeamCachedFrontierPointReads   , ConstraintSplitBatchResult   , CdtBuildResult (..)   , constrainedDelaunay@@ -99,10 +101,10 @@   , ConstraintBatchResult (..)   , ConstraintBatchStats (..)   , ConstrainedExtensionResult (..)+  , ConstrainedSeamSide (..)   , ConstrainedSeamConstraintEvidence (..)   , ConstrainedSeamFaceEvidence (..)   , ConstrainedSeamResult (..)-  , ConstrainedSeamSource (..)   , ConstraintConflict (..)   , ConstraintOutcome (..)   , ConstraintRecoveryResult (..)
src-build/Moonlight/Triangulation/Internal/Cdt/Types.hs view
@@ -11,7 +11,7 @@   , CanonicalSegment (..)   , ConstraintConflict (..)   , ConstrainedUnionError (..)-  , ConstrainedSeamSource (..)+  , ConstrainedSeamSide (..)   , ConstrainedSeamFaceEvidence (..)   , ConstrainedSeamConstraintEvidence (..)   , ConstrainedSeamResult (..)@@ -45,6 +45,7 @@   , VertexId   ) import Moonlight.Triangulation.Internal.Growable (GrowableWord32)+import Moonlight.Triangulation.Internal.Paged (PublicationStats) import Moonlight.Triangulation.Internal.Representation (BuildResult, Triangulation) import Moonlight.Triangulation.Internal.Types import Moonlight.Triangulation.IntersectionIterator (Intersection)@@ -118,29 +119,18 @@   | ConstraintUnionConstructionFailed !(CdtError)   | ConstraintUnionSiteMissing !(Point)   | ConstraintUnionNotSeparated-  | ConstraintUnionSourceFaceNotTriangular-      !ConstrainedSeamSource-      !FaceId-      {-# UNPACK #-} !Int-  | ConstraintUnionTargetFaceNotTriangular-      !FaceId-      {-# UNPACK #-} !Int-  | ConstraintUnionTargetFaceAmbiguous !FaceId !FaceId-  | ConstraintUnionSourceFaceNotPreserved-      !ConstrainedSeamSource-      !FaceId-  | ConstraintUnionSourceConstraintNotPreserved-      !ConstrainedSeamSource-      !(CanonicalSegment)+  -- | Local publication would otherwise enumerate a dense resident payload+  -- plane. Compact geometry-only payloads before requesting a seam.+  | ConstraintUnionRequiresGeometryOnlyPublication   deriving stock (Eq, Show, Generic)   deriving anyclass (NFData) --- | Operand identity in a separated constrained seam. This belongs in the--- obstruction and receipt vocabulary rather than being encoded as a Boolean;--- callers must handle both source sections explicitly.-data ConstrainedSeamSource-  = ConstrainedSeamLeftSource-  | ConstrainedSeamRightSource+-- | Source-local side presented to a separated seam's face-preservation+-- selector. The resident side keeps its handles; incoming face handles are+-- translated only inside the join interpreter.+data ConstrainedSeamSide+  = SeamResident+  | SeamIncoming   deriving stock (Eq, Ord, Show, Generic)   deriving anyclass (NFData) @@ -175,23 +165,24 @@   deriving anyclass (NFData)  -- | Atomic publication of a source-preserving constrained seam and the proof--- needed to transport solved face-indexed data. The receipt is derived only--- after canonical publication and exact source restriction checks succeed.+-- needed to transport solved face-indexed data. The receipt is derived during+-- the ordered append/seam interpretation; canonical numbering remains an+-- explicit observation rather than hidden work in this local operation. data ConstrainedSeamResult vertex = ConstrainedSeamResult   { constrainedSeamResultTriangulation       :: !(Triangulation 'Constrained vertex () () ())     -- ^ Published constrained union.-  , constrainedSeamLeftFaceEvidence-      :: !(V.Vector (ConstrainedSeamFaceEvidence))-    -- ^ Transport evidence for every active left-source face.+  , constrainedSeamLeftFaceCount :: {-# UNPACK #-} !Int+    -- ^ Stable size of the resident source face block. In selective mode this+    -- is a handle-partition witness, not proof that every resident face was+    -- selected.   , constrainedSeamRightFaceEvidence       :: !(V.Vector (ConstrainedSeamFaceEvidence))-    -- ^ Transport evidence for every active right-source face.-  , constrainedSeamNewFaces :: !(V.Vector FaceId)-    -- ^ Faces created by the seam rather than transported from an operand.-  , constrainedSeamLeftConstraintEvidence-      :: !(V.Vector (ConstrainedSeamConstraintEvidence))-    -- ^ Preservation evidence for left-source constraints.+    -- ^ Exact transport evidence only for selected incoming source faces.+  , constrainedSeamJoinFaces :: !(V.Vector FaceId)+    -- ^ Exact final J component, including rewritten exterior source filler.+  , constrainedSeamLeftConstraintCount :: {-# UNPACK #-} !Int+    -- ^ Stable number of left-source constraints.   , constrainedSeamRightConstraintEvidence       :: !(V.Vector (ConstrainedSeamConstraintEvidence))     -- ^ Preservation evidence for right-source constraints.@@ -199,6 +190,10 @@     -- ^ Constraint recovery work performed by the seam.   , constrainedSeamBuildStats :: !BuildStats     -- ^ Topology work performed by the seam.+  , constrainedSeamPublicationStats :: !PublicationStats+    -- ^ Page publication work measured by the transaction owner.+  , constrainedSeamCachedFrontierPointReads :: {-# UNPACK #-} !Int+    -- ^ Frontier point reads performed while interpreting the cached seam.   }   deriving stock (Generic)   deriving anyclass (NFData)
src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs view
@@ -19,20 +19,18 @@   , crossingIsRepresented   ) where -import Control.Monad (foldM) import Control.Monad.ST (ST) import qualified Data.Bifunctor as Bifunctor import Data.Either (isRight)-import Data.List (sort) import qualified Data.List.NonEmpty as NonEmpty import qualified Data.Map.Strict as Map import qualified Data.Set as Set import qualified Data.Vector as V import qualified Moonlight.Triangulation.Dcel as Dcel import Moonlight.Triangulation.Handles.HandleDefs-import Moonlight.Triangulation.IntersectionIterator (foldCorridorBetweenPoints) import Moonlight.Triangulation.Internal.Canonical (canonicalize) import Moonlight.Triangulation.Insertion (insertPointCombining)+import Moonlight.Triangulation.Refinement (validateRefinementParameters) import Moonlight.Triangulation.Internal.Cdt.Batch   ( finalizeConstraintBatch   , interpretConstraintRequests@@ -41,12 +39,18 @@ import Moonlight.Triangulation.Internal.Cdt.Build (fromDelaunay) import Moonlight.Triangulation.Internal.Cdt.Combinators (foldWhileM) import Moonlight.Triangulation.Internal.Cdt.Query (constraintEdges)+import Moonlight.Triangulation.IntersectionIterator (foldCorridorBetweenPoints) import Moonlight.Triangulation.Internal.Cdt.Types import Moonlight.Triangulation.Internal.Join.Rebuild (rebuildCanonicalSiteSet) import Moonlight.Triangulation.Internal.Join.Seam   ( executeConstrainedSeam   , planSeam   , seamExecutionBuildStats+  , seamExecutionPublicationStats+  , seamExecutionCachedFrontierPointReads+  , seamExecutionLeftFaceCount+  , seamExecutionJoinFaces+  , seamExecutionRightFaceEvidence   , seamExecutionTriangulation   ) import Moonlight.Triangulation.Internal.Join.SiteSet@@ -57,6 +61,7 @@   , siteSetSize   , siteSetUnionWith   )+import Moonlight.Triangulation.Internal.BoxedPaged (boxedFill) import Moonlight.Triangulation.Internal.Paged (TransactionShape (DenseTransaction, LocalTransaction)) import Moonlight.Triangulation.Internal.Representation import Moonlight.Triangulation.Internal.Mutable (MutableDcel)@@ -146,199 +151,115 @@ unionConstrained = unionConstrainedWith (\_ _ -> ()) {-# INLINE unionConstrained #-} --- | Join two strictly separated constrained triangulations by copying both--- source meshes and zippering only their common tangent corridor. The source--- constraint planes are present during legalization, so contour edges are--- immutable barriers rather than edges recovery tries to resurrect after a--- solved face has already disappeared.+-- | Join two strictly separated constrained triangulations with the caller's+-- left mesh resident. The right source is appended and only their common+-- tangent corridor and its selected-face-free exterior cavity are legalized.+-- The selector sees source-local handles on an explicit resident/incoming+-- side; selected face handles and constraint contours are immutable barriers.+-- Passing a selector that is always true retains the preserve-all law and its+-- typed incompatibility when the seam requires a source-face rewrite. The+-- result carries the exact final J component plus transport evidence only for+-- selected incoming faces.+-- The resident operand must have gone through 'geometryOnlyPublication'; a+-- dense boxed payload section would make local freeze enumerate the resident+-- world and is refused as a typed obstruction. ----- Strict x-separation proves that the two site sets have no coincident point,--- so annotations are copied from their source mesh and never combined.+-- The published constraint section is exactly the source constraints union+-- the synthetic lower and upper seam bridge paths. Those paths are certified+-- perimeter constraints for the joined world and are subdivided locally when+-- an edge-length law requests it; the operation does not perform a global+-- constraint scan or canonicalization.+--+-- Strict separation in one admitted chart (x, or the orientation-preserving+-- y chart used by the seam planner) proves that the two site sets have no+-- coincident point, so annotations are copied from their source mesh and+-- never combined. joinSeparatedConstrained-  :: Triangulation 'Constrained annotation () () ()+  :: (ConstrainedSeamSide -> FaceId -> Bool)+  -> RefinementParameters   -> Triangulation 'Constrained annotation () () ()+  -> Triangulation 'Constrained annotation () () ()   -> Either       (ConstrainedUnionError)       (ConstrainedSeamResult annotation)-joinSeparatedConstrained left right = do+joinSeparatedConstrained sourceFacePreserved parameters left right = do+  vertexDefault <- ensureDefaultedPayloads left right+  Bifunctor.first+    (ConstraintUnionConstructionFailed . CdtBuildError)+    (validateRefinementParameters parameters)   seamPlan <- maybe (Left ConstraintUnionNotSeparated) Right (planSeam left right)   seamExecution <-     Bifunctor.first       (ConstraintUnionConstructionFailed . CdtBuildError)-      (executeConstrainedSeam seamPlan left right)-  let copied = seamExecutionTriangulation seamExecution-      leftSegments = constraintSegments left-      rightSegments = constraintSegments right-      copiedSegments = Set.fromList (V.toList (constraintSegments copied))-      missingSegments =-        V.filter (`Set.notMember` copiedSegments) (leftSegments V.++ rightSegments)-  requests <- traverse (segmentRequest copied) (V.toList missingSegments)-  recovered <--    Bifunctor.first ConstraintUnionConstructionFailed-      (recoverConstraints copied (V.fromList requests))-  case firstRejected (constraintBatchOutcomes recovered) of-    Just blocking ->-      Left-        ( ConstraintUnionConstructionFailed-            (ConstraintIntersection blocking)-        )-    Nothing -> do-      published <--        Bifunctor.first-          (ConstraintUnionConstructionFailed . CdtBuildError)-          (canonicalize (constraintBatchTriangulation recovered))-      targetFaces <- targetFaceIndex published-      leftFaces <--        sourceFaceEvidence-          ConstrainedSeamLeftSource+      ( executeConstrainedSeam+          sourceFacePreserved+          parameters+          vertexDefault+          seamPlan           left-          targetFaces-      rightFaces <--        sourceFaceEvidence-          ConstrainedSeamRightSource           right-          targetFaces-      let preservedTargets =-            Set.fromList-              ( fmap constrainedSeamTargetFace (V.toList leftFaces)-                  <> fmap constrainedSeamTargetFace (V.toList rightFaces)-              )-          newFaces =-            V.fromList-              ( filter-                  (`Set.notMember` preservedTargets)-                  (innerFaceIds published)-              )-          recoveredOutcomes =-            Map.fromList-              (V.toList (V.zip missingSegments (constraintBatchOutcomes recovered)))-          finalSegments = Set.fromList (V.toList (constraintSegments published))-      leftConstraintEvidence <--        sourceConstraintEvidence-          ConstrainedSeamLeftSource-          finalSegments-          recoveredOutcomes-          leftSegments-      rightConstraintEvidence <--        sourceConstraintEvidence-          ConstrainedSeamRightSource-          finalSegments-          recoveredOutcomes-          rightSegments-      pure-        ConstrainedSeamResult-          { constrainedSeamResultTriangulation = published-          , constrainedSeamLeftFaceEvidence = leftFaces-          , constrainedSeamRightFaceEvidence = rightFaces-          , constrainedSeamNewFaces = newFaces-          , constrainedSeamLeftConstraintEvidence = leftConstraintEvidence-          , constrainedSeamRightConstraintEvidence = rightConstraintEvidence-          , constrainedSeamConstraintStats = constraintBatchStats recovered-          , constrainedSeamBuildStats = seamExecutionBuildStats seamExecution-          }--data CanonicalFaceKey = CanonicalFaceKey-  !(Point)-  !(Point)-  !(Point)-  deriving stock (Eq, Ord)--targetFaceIndex-  :: Triangulation mode vertex directed undirected face-  -> Either-      (ConstrainedUnionError)-      (Map.Map CanonicalFaceKey FaceId)-targetFaceIndex triangulation =-  foldM insertTarget Map.empty (innerFaceIds triangulation)- where-  insertTarget-    :: Map.Map CanonicalFaceKey FaceId-    -> FaceId-    -> Either (ConstrainedUnionError) (Map.Map CanonicalFaceKey FaceId)-  insertTarget index face = do-    key <- targetFaceKey triangulation face-    case Map.lookup key index of-      Just existing -> Left (ConstraintUnionTargetFaceAmbiguous existing face)-      Nothing -> Right (Map.insert key face index)+      )+  let published = seamExecutionTriangulation seamExecution+      rightSegments = constraintSegments right+  rightConstraintEvidence <- sourceConstraintEvidence rightSegments Map.empty+  pure+    ConstrainedSeamResult+      { constrainedSeamResultTriangulation = published+      , constrainedSeamLeftFaceCount = seamExecutionLeftFaceCount seamExecution+      , constrainedSeamRightFaceEvidence = seamExecutionRightFaceEvidence seamExecution+      , constrainedSeamJoinFaces = seamExecutionJoinFaces seamExecution+      , constrainedSeamLeftConstraintCount = Dcel.numConstraints left+      , constrainedSeamRightConstraintEvidence = rightConstraintEvidence+      , constrainedSeamConstraintStats = ConstraintBatchStats 0 0 0 0 0 0+      , constrainedSeamBuildStats = seamExecutionBuildStats seamExecution+      , constrainedSeamPublicationStats = seamExecutionPublicationStats seamExecution+      , constrainedSeamCachedFrontierPointReads = seamExecutionCachedFrontierPointReads seamExecution+      } -sourceFaceEvidence-  :: ConstrainedSeamSource-  -> Triangulation mode vertex directed undirected face-  -> Map.Map CanonicalFaceKey FaceId-  -> Either-      (ConstrainedUnionError)-      (V.Vector (ConstrainedSeamFaceEvidence))-sourceFaceEvidence source triangulation targetFaces =-  V.fromList <$> traverse evidenceFor (innerFaceIds triangulation)- where-  evidenceFor sourceFace = do-    key@(CanonicalFaceKey first second third) <--      sourceFaceKey source triangulation sourceFace-    targetFace <--      maybe-        (Left (ConstraintUnionSourceFaceNotPreserved source sourceFace))-        Right-        (Map.lookup key targetFaces)-    pure-      ConstrainedSeamFaceEvidence-        { constrainedSeamSourceFace = sourceFace-        , constrainedSeamTargetFace = targetFace-        , constrainedSeamFaceFirstPoint = first-        , constrainedSeamFaceSecondPoint = second-        , constrainedSeamFaceThirdPoint = third-        }+ensureDefaultedPayloads+  :: Triangulation 'Constrained annotation () () ()+  -> Triangulation 'Constrained annotation () () ()+  -> Either ConstrainedUnionError annotation+ensureDefaultedPayloads left right =+  case+      ( triSeamFrontier left+      , triSeamFrontier right+      , boxedFill (triVertexData left)+      , boxedFill (triDirectedData left)+      , boxedFill (triUndirectedData left)+      , boxedFill (triFaceData left)+      , boxedFill (triVertexData right)+      , boxedFill (triDirectedData right)+      , boxedFill (triUndirectedData right)+      , boxedFill (triFaceData right)+      ) of+    ( Just _+      , Just _+      , Just vertexDefault+      , Just _+      , Just _+      , Just _+      , Just _+      , Just _+      , Just _+      , Just _+      ) -> Right vertexDefault+    _ -> Left ConstraintUnionRequiresGeometryOnlyPublication  sourceConstraintEvidence-  :: ConstrainedSeamSource-  -> Set.Set (CanonicalSegment)+  :: V.Vector (CanonicalSegment)   -> Map.Map (CanonicalSegment) ConstraintOutcome-  -> V.Vector (CanonicalSegment)-  -> Either-      (ConstrainedUnionError)-      (V.Vector (ConstrainedSeamConstraintEvidence))-sourceConstraintEvidence source finalSegments recovered =+  -> Either (ConstrainedUnionError) (V.Vector (ConstrainedSeamConstraintEvidence))+sourceConstraintEvidence segments recovered =   traverse     (\segment ->-       if Set.member segment finalSegments-         then-           Right-             ConstrainedSeamConstraintEvidence-               { constrainedSeamConstraintSegment = segment-               , constrainedSeamConstraintRecovery = Map.lookup segment recovered-               }-         else Left (ConstraintUnionSourceConstraintNotPreserved source segment)+       Right+         ConstrainedSeamConstraintEvidence+           { constrainedSeamConstraintSegment = segment+           , constrainedSeamConstraintRecovery = Map.lookup segment recovered+           }     )--sourceFaceKey-  :: ConstrainedSeamSource-  -> Triangulation mode vertex directed undirected face-  -> FaceId-  -> Either (ConstrainedUnionError) CanonicalFaceKey-sourceFaceKey source triangulation face =-  case sort (fmap (Dcel.vertexPoint triangulation) (Dcel.faceVertices triangulation face)) of-    [first, second, third] -> Right (CanonicalFaceKey first second third)-    points ->-      Left-        ( ConstraintUnionSourceFaceNotTriangular-            source-            face-            (length points)-        )--targetFaceKey-  :: Triangulation mode vertex directed undirected face-  -> FaceId-  -> Either (ConstrainedUnionError) CanonicalFaceKey-targetFaceKey triangulation face =-  case sort (fmap (Dcel.vertexPoint triangulation) (Dcel.faceVertices triangulation face)) of-    [first, second, third] -> Right (CanonicalFaceKey first second third)-    points -> Left (ConstraintUnionTargetFaceNotTriangular face (length points))--innerFaceIds-  :: Triangulation mode vertex directed undirected face-  -> [FaceId]-innerFaceIds triangulation =-  fmap (FaceId . fromIntegral) [1 .. Dcel.numFaces triangulation - 1]+    segments  -- | Extend one already-resident constrained triangulation with one new -- constrained section. This is intentionally asymmetric: the base mesh is
src-build/Moonlight/Triangulation/Internal/Join/Seam.hs view
@@ -1,547 +1,1380 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}---- | Linear seam construction for separated Delaunay triangulations. Admission--- returns an opaque proof carrying the exact source order and tangents; the--- executor therefore has no untyped precondition and owns no fallback.-module Moonlight.Triangulation.Internal.Join.Seam-  ( SeamPlan-  , planSeam-  , executeSeam-  , ConstrainedSeamExecution-  , seamExecutionTriangulation-  , seamExecutionBuildStats-  , executeConstrainedSeam-  ) where--import Control.Monad.ST (ST, runST)-import Data.Bits (xor)-import Data.Foldable (traverse_)-import qualified Data.Vector.Unboxed as U-import Moonlight.Triangulation.Dcel-  ( adjacentEdge-  , faceDirectedEdges-  , numDirectedEdges-  , numFaces-  , numInnerFaces-  , numVertices-  , outerFace-  , vertexOutEdge-  , vertexData-  )-import Moonlight.Triangulation.Handles.HandleDefs-  ( DirectedEdgeId (..)-  , FaceId (..)-  , UndirectedEdgeId (..)-  , VertexId (..)-  )-import Moonlight.Triangulation.Internal.DcelOperations.Hull (closeOuterTurn)-import Moonlight.Triangulation.Internal.DcelOperations.Legalize (legalizeEdges)-import Moonlight.Triangulation.Internal.Cdt.Query (constraintEdges)-import Moonlight.Triangulation.Internal.Mutable-import Moonlight.Triangulation.Internal.OperationState-  ( OperationState-  , freezeBuildStats-  , newOperationState-  )-import Moonlight.Triangulation.Internal.Paged (pagedUnsafeIndex)-import Moonlight.Triangulation.Internal.Representation (Triangulation (..))-import Moonlight.Triangulation.Internal.Types-  ( BuildError-  , BuildStats-  , ConstraintMode (Constrained, Unconstrained)-  , unitElementDefaults-  )-import Moonlight.Triangulation.Scalar (inCircleCoordinates, orient2dCoordinates)---- | Proof that a particular pair can be copied and stitched by the seam--- kernel. Constructors stay private so an unseparated pair cannot be handed to--- the executor by accident.-data SeamPlan-  = SeamLeftBeforeRight !SeamTangents-  | SeamRightBeforeLeft !SeamTangents--planSeam-  :: Triangulation mode vertex () () ()-  -> Triangulation mode' vertex () () ()-  -> Maybe SeamPlan-planSeam left right =-  case separatedOrder left right of-    Just LeftBeforeRight ->-      Just (SeamLeftBeforeRight (seamTangents left right))-    Just RightBeforeLeft ->-      Just (SeamRightBeforeLeft (seamTangents right left))-    Nothing -> Nothing---- | Execute a proved seam schedule. Numbering follows the schedule;--- 'Moonlight.Triangulation.Dcel.canonicalize' remains the explicit--- construction-independent observation.-executeSeam-  :: forall vertex-   . SeamPlan-  -> Triangulation 'Unconstrained vertex () () ()-  -> Triangulation 'Unconstrained vertex () () ()-  -> Either BuildError (Triangulation 'Unconstrained vertex () () ())-executeSeam plan left right =-  case plan of-    SeamLeftBeforeRight tangents ->-      fmap fst (mergeSeparated [] left right tangents)-    SeamRightBeforeLeft tangents ->-      fmap fst (mergeSeparated [] right left tangents)---- | Result of the constrained seam kernel. Constraint flags are copied before--- legalization, so source contour edges are immutable barriers while the--- zipper constructs only the missing corridor.-data ConstrainedSeamExecution vertex = ConstrainedSeamExecution-  { seamExecutionTriangulation-      :: !(Triangulation 'Constrained vertex () () ())-  , seamExecutionBuildStats :: !BuildStats-  }---- | Execute a proved seam while transporting both source constraint planes.--- This is distinct from promoting the unconstrained result afterward: source--- hull constraints must already be visible to seam legalization or the--- legalization schedule could erase a solved source face before recovery had--- a chance to mark it.-executeConstrainedSeam-  :: forall vertex-   . SeamPlan-  -> Triangulation 'Constrained vertex () () ()-  -> Triangulation 'Constrained vertex () () ()-  -> Either BuildError (ConstrainedSeamExecution vertex)-executeConstrainedSeam plan left right =-  fmap-    (uncurry ConstrainedSeamExecution)-    (case plan of-      SeamLeftBeforeRight tangents ->-        mergeSeparated-          [ (0, left)-          , (numDirectedEdges left, right)-          ]-          left-          right-          tangents-      SeamRightBeforeLeft tangents ->-        mergeSeparated-          [ (0, right)-          , (numDirectedEdges right, left)-          ]-          right-          left-          tangents-    )--data SeparatedOrder-  = LeftBeforeRight-  | RightBeforeLeft--separatedOrder-  :: Triangulation mode vertex directed undirected face-  -> Triangulation mode' vertex' directed' undirected' face'-  -> Maybe SeparatedOrder-separatedOrder left right-  | numInnerFaces left <= 0 || numInnerFaces right <= 0 = Nothing-  | otherwise = do-      (leftMinimum, leftMaximum) <- xBounds left-      (rightMinimum, rightMaximum) <- xBounds right-      if leftMaximum < rightMinimum-        then Just LeftBeforeRight-        else-          if rightMaximum < leftMinimum-            then Just RightBeforeLeft-            else Nothing--xBounds-  :: Triangulation mode vertex directed undirected face-  -> Maybe (Double, Double)-xBounds triangulation-  | total <= 0 = Nothing-  | otherwise = Just (go 1 first first)- where-  !total = numVertices triangulation-  !coordinates = triPointX triangulation-  !first = coordinates `pagedUnsafeIndex` 0-  go !index !minimumX !maximumX-    | index >= total = (minimumX, maximumX)-    | otherwise =-        let !x = coordinates `pagedUnsafeIndex` index-         in go (index + 1) (min minimumX x) (max maximumX x)---- The opposite-sign branch cannot overflow in its sum. The same-sign branch--- cannot overflow in its difference.-data SeamTangents = SeamTangents-  {-# UNPACK #-} !Int-  {-# UNPACK #-} !Int-  {-# UNPACK #-} !Int-  {-# UNPACK #-} !Int--seamTangents-  :: Triangulation mode vertex directed undirected face-  -> Triangulation mode' vertex' directed' undirected' face'-  -> SeamTangents-seamTangents left right =-  let (lowerLeft, lowerRight) = lowerTangent left right leftHull rightHull-      (upperLeft, upperRight) = upperTangent left right leftHull rightHull-   in SeamTangents lowerLeft lowerRight upperLeft upperRight- where-  !leftHull = outerEdges left-  !rightHull = outerEdges right--lowerTangent-  :: Triangulation mode vertex directed undirected face-  -> Triangulation mode' vertex' directed' undirected' face'-  -> U.Vector Int-  -> U.Vector Int-  -> (Int, Int)--- The walk carries both endpoints' coordinates: a step replaces exactly one--- endpoint, and the replacement is the neighbour whose coordinates the step's--- own test already read.-lowerTangent left right leftHull rightHull =-  walk leftStart leftStartX leftStartY rightStart rightStartX rightStartY- where-  !leftStart = extremeHullIndex preferRightmost left leftHull-  !rightStart = extremeHullIndex preferLeftmost right rightHull-  (!leftStartX, !leftStartY) = hullPoint left leftHull leftStart-  (!rightStartX, !rightStartY) = hullPoint right rightHull rightStart--  walk !leftIndex !leftX !leftY !rightIndex !rightX !rightY-    | leftBelow = walk nextLeft nextLeftX nextLeftY rightIndex rightX rightY-    | rightBelow = walk leftIndex leftX leftY previousRight previousRightX previousRightY-    | otherwise = (leftHull `U.unsafeIndex` leftIndex, rightHull `U.unsafeIndex` rightIndex)-   where-    !nextLeft = nextIndex (U.length leftHull) leftIndex-    (!nextLeftX, !nextLeftY) = hullPoint left leftHull nextLeft-    !leftBelow =-      orient2dCoordinates leftX leftY rightX rightY nextLeftX nextLeftY == LT-    previousRight = previousIndex (U.length rightHull) rightIndex-    (previousRightX, previousRightY) = hullPoint right rightHull previousRight-    rightBelow =-      orient2dCoordinates leftX leftY rightX rightY previousRightX previousRightY == LT--upperTangent-  :: Triangulation mode vertex directed undirected face-  -> Triangulation mode' vertex' directed' undirected' face'-  -> U.Vector Int-  -> U.Vector Int-  -> (Int, Int)-upperTangent left right leftHull rightHull =-  walk leftStart leftStartX leftStartY rightStart rightStartX rightStartY- where-  !leftStart = extremeHullIndex preferRightmostUpper left leftHull-  !rightStart = extremeHullIndex preferLeftmostUpper right rightHull-  (!leftStartX, !leftStartY) = hullPoint left leftHull leftStart-  (!rightStartX, !rightStartY) = hullPoint right rightHull rightStart--  walk !leftIndex !leftX !leftY !rightIndex !rightX !rightY-    | leftAbove = walk previousLeft previousLeftX previousLeftY rightIndex rightX rightY-    | rightAbove = walk leftIndex leftX leftY nextRight nextRightX nextRightY-    | otherwise = (leftHull `U.unsafeIndex` leftIndex, rightHull `U.unsafeIndex` rightIndex)-   where-    !previousLeft = previousIndex (U.length leftHull) leftIndex-    (!previousLeftX, !previousLeftY) = hullPoint left leftHull previousLeft-    !leftAbove =-      orient2dCoordinates leftX leftY rightX rightY previousLeftX previousLeftY == GT-    nextRight = nextIndex (U.length rightHull) rightIndex-    (nextRightX, nextRightY) = hullPoint right rightHull nextRight-    rightAbove =-      orient2dCoordinates leftX leftY rightX rightY nextRightX nextRightY == GT--type ExtremePreference = Double -> Double -> Double -> Double -> Bool--preferRightmost :: ExtremePreference-preferRightmost bestX bestY candidateX candidateY =-  candidateX > bestX || (candidateX == bestX && candidateY < bestY)-{-# INLINE preferRightmost #-}--preferLeftmost :: ExtremePreference-preferLeftmost bestX bestY candidateX candidateY =-  candidateX < bestX || (candidateX == bestX && candidateY < bestY)-{-# INLINE preferLeftmost #-}--preferRightmostUpper :: ExtremePreference-preferRightmostUpper bestX bestY candidateX candidateY =-  candidateX > bestX || (candidateX == bestX && candidateY > bestY)-{-# INLINE preferRightmostUpper #-}--preferLeftmostUpper :: ExtremePreference-preferLeftmostUpper bestX bestY candidateX candidateY =-  candidateX < bestX || (candidateX == bestX && candidateY > bestY)-{-# INLINE preferLeftmostUpper #-}--extremeHullIndex-  :: ExtremePreference-  -> Triangulation mode vertex directed undirected face-  -> U.Vector Int-  -> Int--- The running best carries its own coordinates. Re-reading them per candidate--- read the same immutable slots @size@ times over instead of once.-extremeHullIndex prefer triangulation hull-  | size <= 0 = 0-  | otherwise =-      let (!firstX, !firstY) = hullPoint triangulation hull 0-       in go 1 0 firstX firstY- where-  !size = U.length hull-  go !index !best !bestX !bestY-    | index >= size = best-    | otherwise =-        let (!candidateX, !candidateY) = hullPoint triangulation hull index-         in if prefer bestX bestY candidateX candidateY-              then go (index + 1) index candidateX candidateY-              else go (index + 1) best bestX bestY--outerEdges-  :: Triangulation mode vertex directed undirected face-  -> U.Vector Int-outerEdges =-  U.fromList-    . fmap (\(DirectedEdgeId edge) -> fromIntegral edge)-    . (`faceDirectedEdges` outerFace)--hullPoint-  :: Triangulation mode vertex directed undirected face-  -> U.Vector Int-  -> Int-  -> (Double, Double)-hullPoint triangulation hull index =-  let !edge = hull `U.unsafeIndex` index-      !vertex = topologyAt triangulation (4 * edge)-   in ( triPointX triangulation `pagedUnsafeIndex` vertex-      , triPointY triangulation `pagedUnsafeIndex` vertex-      )-{-# INLINE hullPoint #-}--nextIndex :: Int -> Int -> Int-nextIndex size index-  | index + 1 == size = 0-  | otherwise = index + 1-{-# INLINE nextIndex #-}--previousIndex :: Int -> Int -> Int-previousIndex size index-  | index == 0 = size - 1-  | otherwise = index - 1-{-# INLINE previousIndex #-}--mergeSeparated-  :: forall outputMode leftMode rightMode vertex-   . [(Int, Triangulation 'Constrained vertex () () ())]-  -> Triangulation leftMode vertex () () ()-  -> Triangulation rightMode vertex () () ()-  -> SeamTangents-  -> Either-      BuildError-      (Triangulation outputMode vertex () () (), BuildStats)-mergeSeparated constraintSections left right (SeamTangents lowerLeft lowerRight upperLeft upperRight) = runST $ do-  mutable <- newMutableDcel unitElementDefaults (planarDcelCapacity totalVertices)-  pointCapacityOutcome <- ensurePointCapacity mutable totalVertices-  cellCapacityOutcome <--    ensureCellCapacity-      mutable-      ((leftDirected + rightDirected) `quot` 2 + 1)-      (leftFaces + rightFaces - 2)-  case (pointCapacityOutcome, cellCapacityOutcome) of-    (Left obstruction, _) -> pure (Left obstruction)-    (_, Left obstruction) -> pure (Left obstruction)-    (Right (), Right ()) -> do-      appendSourceVertices mutable left-      appendSourceVertices mutable right-      _ <- addEdgeBlock mutable ((leftDirected + rightDirected) `quot` 2)-      _ <- addFaceBlock mutable (leftFaces + rightFaces - 2)-      copySource mutable left 0 0 0-      copySource mutable right leftVertices leftDirected (leftFaces - 1)-      traverse_ (uncurry (protectSourceEdges mutable)) constraintSections-      base <- spliceLowerTangent mutable leftDirected lowerLeft lowerRight-      operation <- newOperationState (halfEdgeCapacity mutable)-      stitched <--        stitchSeam-          mutable-          operation-          base-          (topologyAt left (4 * upperLeft))-          (leftVertices + topologyAt right (4 * upperRight))-          []-      case stitched of-        Left obstruction -> pure (Left obstruction)-        Right () -> do-          traverse_ (uncurry (restoreSourceConstraints mutable)) constraintSections-          statistics <- freezeBuildStats operation-          fmap (\triangulation -> (triangulation, statistics))-            <$> freezeTriangulation mutable- where-  !leftVertices = numVertices left-  !rightVertices = numVertices right-  !totalVertices = leftVertices + rightVertices-  !leftDirected = numDirectedEdges left-  !rightDirected = numDirectedEdges right-  !leftFaces = numFaces left-  !rightFaces = numFaces right--stitchSeam-  :: MutableDcel s vertex directed undirected face-  -> OperationState s-  -> Int-  -> Int-  -> Int-  -> [Int]-  -> ST s (Either BuildError ())-stitchSeam mutable operation base upperLeft upperRight seeds = do-  leftVertex <- readOrigin mutable base-  rightVertex <- readOrigin mutable (base `xor` 1)-  if leftVertex == upperLeft && rightVertex == upperRight-    then legalizeEdges mutable operation seeds >> pure (Right ())-    else do-      leftEdge <- readPrevious mutable base-      rightEdge <- readNext mutable base-      nextLeft <- readOrigin mutable leftEdge-      nextRight <- readOrigin mutable (rightEdge `xor` 1)-      leftTurn <- vertexOrientation mutable nextLeft leftVertex rightVertex-      rightTurn <- vertexOrientation mutable leftVertex rightVertex nextRight-      chooseRight <--        if rightVertex == upperRight-          then pure False-          else-            if leftVertex == upperLeft-              then pure True-              else-                case (leftTurn == GT, rightTurn == GT) of-                  (True, True) ->-                    (== GT) <$> vertexInCircle mutable nextLeft leftVertex rightVertex nextRight-                  (False, True) -> pure True-                  _ -> pure False-      if chooseRight-        then do-          closed <- closeOuterTurn mutable base-          case closed of-            Left obstruction -> pure (Left obstruction)-            Right nextBase ->-              stitchSeam mutable operation nextBase upperLeft upperRight (base : rightEdge : seeds)-        else do-          closed <- closeOuterTurn mutable leftEdge-          case closed of-            Left obstruction -> pure (Left obstruction)-            Right nextBase ->-              stitchSeam mutable operation nextBase upperLeft upperRight (leftEdge : base : seeds)--vertexOrientation-  :: MutableDcel s vertex directed undirected face-  -> Int-  -> Int-  -> Int-  -> ST s Ordering-vertexOrientation mutable a b c = do-  ax <- readPointX mutable a-  ay <- readPointY mutable a-  bx <- readPointX mutable b-  by <- readPointY mutable b-  cx <- readPointX mutable c-  cy <- readPointY mutable c-  pure $! orient2dCoordinates ax ay bx by cx cy-{-# INLINE vertexOrientation #-}--vertexInCircle-  :: MutableDcel s vertex directed undirected face-  -> Int-  -> Int-  -> Int-  -> Int-  -> ST s Ordering-vertexInCircle mutable a b c d = do-  ax <- readPointX mutable a-  ay <- readPointY mutable a-  bx <- readPointX mutable b-  by <- readPointY mutable b-  cx <- readPointX mutable c-  cy <- readPointY mutable c-  dx <- readPointX mutable d-  dy <- readPointY mutable d-  pure $! inCircleCoordinates ax ay bx by cx cy dx dy-{-# INLINE vertexInCircle #-}--appendSourceVertices-  :: MutableDcel s vertex () () ()-  -> Triangulation mode vertex () () ()-  -> ST s ()-appendSourceVertices mutable source =-  forRange 0 (numVertices source) $ \vertex -> do-    _ <--      appendVertexCoordinates-        mutable-        (triPointX source `pagedUnsafeIndex` vertex)-        (triPointY source `pagedUnsafeIndex` vertex)-        (vertexData source (VertexId (fromIntegral vertex)))-    pure ()--copySource-  :: MutableDcel s vertex () () ()-  -> Triangulation mode vertex () () ()-  -> Int-  -> Int-  -> Int-  -> ST s ()-copySource mutable source vertexOffset edgeOffset faceOffset = do-  forRange 0 (numDirectedEdges source) $ \edge -> do-    let !target = edgeOffset + edge-        !sourceBase = 4 * edge-        !sourceFace = topologyAt source (sourceBase + 3)-        !targetFace = if sourceFace == 0 then 0 else faceOffset + sourceFace-    writeOrigin mutable target (vertexOffset + topologyAt source sourceBase)-    writeNext mutable target (edgeOffset + topologyAt source (sourceBase + 1))-    writePrevious mutable target (edgeOffset + topologyAt source (sourceBase + 2))-    writeFace mutable target targetFace-  forRange 0 (numVertices source) $ \vertex ->-    case vertexOutEdge source (VertexId (fromIntegral vertex)) of-      Nothing -> markConnected mutable (vertexOffset + vertex) (-1)-      Just (DirectedEdgeId edge) ->-        markConnected mutable (vertexOffset + vertex) (edgeOffset + fromIntegral edge)-  forRange 1 (numFaces source) $ \face ->-    case adjacentEdge source (FaceId (fromIntegral face)) of-      Nothing -> writeFaceEdge mutable (faceOffset + face) (-1)-      Just (DirectedEdgeId edge) ->-        writeFaceEdge mutable (faceOffset + face) (edgeOffset + fromIntegral edge)--copySourceConstraints-  :: MutableDcel s vertex () () ()-  -> Int-  -> Triangulation 'Constrained vertex () () ()-  -> ST s ()-copySourceConstraints mutable directedEdgeOffset source =-  traverse_-    (\(UndirectedEdgeId edge) ->-       ()-         <$ setConstraint-           mutable-           (directedEdgeOffset + 2 * fromIntegral edge)-    )-    (constraintEdges source)---- Source faces are immutable sections of a separated join.  Protect every--- copied source edge while the seam is legalized, then restore the original--- constraint plane before publication.  The temporary flags prevent the--- corridor legalization from escaping the overlap and flipping a solved--- source face.-protectSourceEdges-  :: MutableDcel s vertex () () ()-  -> Int-  -> Triangulation 'Constrained vertex () () ()-  -> ST s ()-protectSourceEdges mutable directedEdgeOffset source =-  forRange 0 (numDirectedEdges source `quot` 2) $ \edge -> do-    _ <- setConstraint mutable (directedEdgeOffset + 2 * edge)-    pure ()--restoreSourceConstraints-  :: MutableDcel s vertex () () ()-  -> Int-  -> Triangulation 'Constrained vertex () () ()-  -> ST s ()-restoreSourceConstraints mutable directedEdgeOffset source = do-  forRange 0 (numDirectedEdges source `quot` 2) $ \edge -> do-    _ <- clearConstraint mutable (directedEdgeOffset + 2 * edge)-    pure ()-  copySourceConstraints mutable directedEdgeOffset source+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Linear seam construction for separated Delaunay triangulations. Admission+-- returns an opaque proof carrying the exact source order and tangents; the+-- executor therefore has no untyped precondition and owns no fallback.+module Moonlight.Triangulation.Internal.Join.Seam+  ( SeamPlan+  , planSeam+  , executeSeam+  , ConstrainedSeamExecution+  , seamExecutionTriangulation+  , seamExecutionBuildStats+  , seamExecutionPublicationStats+  , seamExecutionCachedFrontierPointReads+  , seamExecutionLeftFaceCount+  , seamExecutionRightFaceEvidence+  , seamExecutionJoinFaces+  , executeConstrainedSeam+  ) where++import Control.Applicative ((<|>))+import Control.Monad.ST (ST, runST)+import Data.Bits (finiteBitSize, xor)+import Data.Foldable (traverse_)+import qualified Data.IntSet as IntSet+import qualified Data.List as List+import qualified Data.Sequence as Seq+import qualified Data.Vector as V+import Moonlight.Triangulation.Dcel+  ( adjacentEdge+  , faceDirectedEdges+  , faceVertices+  , incidentFace+  , isConstraintEdge+  , numDirectedEdges+  , numFaces+  , numInnerFaces+  , numVertices+  , origin+  , previous+  , vertexOutEdge+  , vertexData+  )+import Moonlight.Triangulation.FloodFillIterator (floodFillFaces)+import Moonlight.Triangulation.Handles.HandleDefs+  ( DirectedEdgeId (..)+  , FaceId (..)+  , UndirectedEdgeId (..)+  , VertexId (..)+  , asUndirected+  , directedPair+  )+import Moonlight.Triangulation.Internal.DcelOperations.Hull (closeOuterTurn)+import Moonlight.Triangulation.Internal.DcelOperations.Legalize+  ( legalizeEdges+  , legalizeEdgesPinned+  )+import Moonlight.Triangulation.Internal.DcelOperations.Subdivide+  ( splitBoundaryEdgeWithLegalization+  )+import Moonlight.Triangulation.Internal.DcelOperations.FlipRule (illegalDiagonal)+import Moonlight.Triangulation.Internal.Cdt.Combinators (bindMutable)+import Moonlight.Triangulation.Internal.Cdt.Query (constraintEdges)+import Moonlight.Triangulation.Internal.Mutable+import Moonlight.Triangulation.Internal.OperationState+  ( Counter (CounterSteinerPoints)+  , OperationState+  , addCounter+  , freezeBuildStats+  , newOperationState+  )+import Moonlight.Triangulation.Internal.Paged+  ( PublicationStats+  , TransactionShape (LocalTransaction)+  , pagedUnsafeIndex+  )+import Moonlight.Triangulation.Internal.Representation+  ( SeamFrontierIndex (..)+  , Triangulation (..)+  , seamQuarterTurn+  , summarizeSeamChart+  )+import Moonlight.Triangulation.Internal.Cdt.Types+  ( ConstrainedSeamFaceEvidence (..)+  , ConstrainedSeamSide (..)+  )+import Moonlight.Triangulation.Internal.Types+  ( BuildError (..)+  , BuildStats+  , ConstraintMode (Constrained, Unconstrained)+  , InvariantViolation (..)+  , Point (..)+  , QueryPoint (queryPointValue)+  , RefinementParameters (..)+  , unitElementDefaults+  )+import Moonlight.Triangulation.Scalar (inCircleCoordinates, orient2dCoordinates)+import Moonlight.Triangulation.Math (distance, midpoint, validatePoint)+import Moonlight.Triangulation.Internal.Transaction (runTransactionWithPublication)++-- | Proof that a particular pair can be copied and stitched by the seam+-- kernel. Constructors stay private so an unseparated pair cannot be handed to+-- the executor by accident.+data SeamPlan+  = SeamLeftBeforeRight !SeamTangents+  | SeamRightBeforeLeft !SeamTangents++-- | Coordinate chart used by the separated seam.  The second chart is the+-- orientation-preserving quarter turn @(u,v) = (y,-x)@; exact predicates and+-- the zipper therefore remain unchanged while north/south admission reuses+-- the same tangent interpreter.+data SeamAxis+  = SeamAxisX+  | SeamAxisY+  deriving stock (Eq, Show)++planSeam+  :: Triangulation mode vertex () () ()+  -> Triangulation mode' vertex () () ()+  -> Maybe SeamPlan+planSeam left right = do+  leftFrontier <- triSeamFrontier left+  rightFrontier <- triSeamFrontier right+  planAxis SeamAxisX leftFrontier rightFrontier+    <|> planAxis SeamAxisY leftFrontier rightFrontier+ where+  planAxis axis leftFrontier rightFrontier =+    case separatedOrder axis left right leftFrontier rightFrontier of+      Just LeftBeforeRight ->+        SeamLeftBeforeRight+          <$> seamTangents axis left leftFrontier right rightFrontier+      Just RightBeforeLeft ->+        SeamRightBeforeLeft+          <$> seamTangents axis right rightFrontier left leftFrontier+      Nothing -> Nothing++-- | Execute a proved seam schedule. Numbering follows the schedule;+-- 'Moonlight.Triangulation.Dcel.canonicalize' remains the explicit+-- construction-independent observation.+executeSeam+  :: forall vertex+   . SeamPlan+  -> Triangulation 'Unconstrained vertex () () ()+  -> Triangulation 'Unconstrained vertex () () ()+  -> Either BuildError (Triangulation 'Unconstrained vertex () () ())+executeSeam plan left right =+  case plan of+    SeamLeftBeforeRight tangents ->+      fmap fst (mergeSeparated left right tangents)+    SeamRightBeforeLeft tangents ->+      fmap fst (mergeSeparated right left tangents)++-- | Result of the constrained seam kernel. Constraint flags are copied before+-- legalization, so source contour edges are immutable barriers while the+-- zipper constructs only the missing corridor.+data ConstrainedSeamExecution vertex = ConstrainedSeamExecution+  { seamExecutionTriangulation+      :: !(Triangulation 'Constrained vertex () () ())+  , seamExecutionBuildStats :: !BuildStats+  , seamExecutionPublicationStats :: !PublicationStats+  , seamExecutionCachedFrontierPointReads :: {-# UNPACK #-} !Int+  , seamExecutionLeftFaceCount :: {-# UNPACK #-} !Int+  , seamExecutionRightFaceEvidence :: !(V.Vector ConstrainedSeamFaceEvidence)+  , seamExecutionJoinFaces :: !(V.Vector FaceId)+  }++-- | Execute a proved seam while transporting both source constraint planes.+-- This is distinct from promoting the unconstrained result afterward: source+-- hull constraints must already be visible to seam legalization or the+-- legalization schedule could erase a solved source face before recovery had+-- a chance to mark it.+executeConstrainedSeam+  :: forall vertex+   . (ConstrainedSeamSide -> FaceId -> Bool)+  -> RefinementParameters+  -> vertex+  -> SeamPlan+  -> Triangulation 'Constrained vertex () () ()+  -> Triangulation 'Constrained vertex () () ()+  -> Either BuildError (ConstrainedSeamExecution vertex)+executeConstrainedSeam sourceFacePreserved parameters vertexDefault plan left right = do+  let (order, tangents) =+        case plan of+          SeamLeftBeforeRight selectedTangents ->+            (GeometricBaseLeft, selectedTangents)+          SeamRightBeforeLeft selectedTangents ->+            (GeometricBaseRight, selectedTangents)+  (triangulation, statistics, publicationStats, joinFaces) <-+    executeResidentConstrained+      targetFaceProtected+      parameters+      vertexDefault+      order+      left+      right+      tangents+  rightEvidence <-+    seamFaceEvidenceForSource+      right+      triangulation+      incomingFaceOffset+      rightEvidenceFaces+  pure+    ConstrainedSeamExecution+      { seamExecutionTriangulation = triangulation+      , seamExecutionBuildStats = statistics+      , seamExecutionPublicationStats = publicationStats+      , seamExecutionCachedFrontierPointReads =+          seamFrontierPointReads tangents+            + case refineMaxEdgeLength parameters of+              Nothing -> 0+              Just _ -> 4+      , seamExecutionLeftFaceCount = numInnerFaces left+      , seamExecutionRightFaceEvidence = rightEvidence+      , seamExecutionJoinFaces = joinFaces+      }+ where+  !residentFaceLimit = numFaces left+  !incomingFaceOffset = residentFaceLimit - 1+  !incomingFaceLimit = incomingFaceOffset + numFaces right++  targetFaceProtected rawFace+    | rawFace <= 0 = False+    | rawFace < residentFaceLimit =+        sourceFacePreserved SeamResident (FaceId (fromIntegral rawFace))+    | rawFace < incomingFaceLimit =+        sourceFacePreserved+          SeamIncoming+          (FaceId (fromIntegral (rawFace - incomingFaceOffset)))+    | otherwise = False++  rightEvidenceFaces =+    filter+      (sourceFacePreserved SeamIncoming)+      (fmap (FaceId . fromIntegral) [1 .. numFaces right - 1])++-- | Descend exactly the unprotected face component touched by the zipper.+-- This is the authoritative J section: it contains both newly allocated seam+-- faces and any source exterior filler lawfully retriangulated into them.+exactJoinFaces+  :: Triangulation 'Constrained vertex () () ()+  -> (Int -> Bool)+  -> V.Vector FaceId+  -> V.Vector FaceId+exactJoinFaces triangulation targetFaceProtected seeds =+  V.fromList+    ( floodFillFaces+        triangulation+        (V.toList seeds)+        canCross+    )+ where+  canCross edge =+    not (isConstraintEdge triangulation edge)+      && not+        ( any+            targetFaceIsProtected+            (incidentInnerFaces triangulation edge)+        )++  targetFaceIsProtected (FaceId raw) =+    targetFaceProtected (fromIntegral raw)++-- | Certify only the J/selected-face overlap after local descent. A selected+-- source face is immutable; therefore an unconstrained overlap edge that still+-- requires a Delaunay flip is a genuine incompatibility, never permission to+-- rewrite the solved section.+certifyJoinBoundary+  :: Triangulation 'Constrained vertex () () ()+  -> (Int -> Bool)+  -> V.Vector FaceId+  -> Either BuildError ()+certifyJoinBoundary triangulation targetFaceProtected joinFaces =+  traverse_ certifyPair (IntSet.toAscList candidatePairs)+ where+  joinFaceSet =+    IntSet.fromList+      [ fromIntegral raw+      | FaceId raw <- V.toList joinFaces+      ]++  candidatePairs =+    List.foldl'+      (\pairs faceHandle ->+         List.foldl'+           (\acc edge ->+              let UndirectedEdgeId raw = asUndirected edge+               in IntSet.insert (fromIntegral raw) acc+           )+           pairs+           (faceDirectedEdges triangulation faceHandle)+      )+      IntSet.empty+      (V.toList joinFaces)++  certifyPair rawPair =+    let edge = UndirectedEdgeId (fromIntegral rawPair)+     in if not (selectedJoinOverlap edge)+          || isConstraintEdge triangulation edge+          || not (immutableEdgeRequiresFlip triangulation edge)+          then Right ()+          else Left (SeamSourceEdgeRequiresFlip edge)++  selectedJoinOverlap edge =+    case directedPair edge of+      (forward, backward) ->+        let forwardFace = incidentFace triangulation forward+            backwardFace = incidentFace triangulation backward+         in (joinFace forwardFace && protectedFace backwardFace)+              || (joinFace backwardFace && protectedFace forwardFace)++  joinFace (FaceId raw) = IntSet.member (fromIntegral raw) joinFaceSet+  protectedFace (FaceId raw) = targetFaceProtected (fromIntegral raw)++incidentInnerFaces+  :: Triangulation mode vertex directed undirected face+  -> UndirectedEdgeId+  -> [FaceId]+incidentInnerFaces triangulation edge =+  filter (/= FaceId 0)+    [ incidentFace triangulation forward+    , incidentFace triangulation backward+    ]+ where+  (forward, backward) = directedPair edge++immutableEdgeRequiresFlip+  :: Triangulation mode vertex directed undirected face+  -> UndirectedEdgeId+  -> Bool+immutableEdgeRequiresFlip triangulation edge =+  leftFace /= FaceId 0+    && rightFace /= FaceId 0+    && orient2dCoordinates cx cy dx dy bx by == GT+    && orient2dCoordinates dx dy cx cy ax ay == GT+    && illegalDiagonal ax ay bx by cx cy dx dy+ where+  (forward, backward) = directedPair edge+  leftFace = incidentFace triangulation forward+  rightFace = incidentFace triangulation backward+  forwardPrevious = previous triangulation forward+  backwardPrevious = previous triangulation backward+  Point ax ay = vertexPointAt triangulation (origin triangulation forward)+  Point bx by = vertexPointAt triangulation (origin triangulation backward)+  Point cx cy = vertexPointAt triangulation (origin triangulation forwardPrevious)+  Point dx dy = vertexPointAt triangulation (origin triangulation backwardPrevious)++vertexPointAt+  :: Triangulation mode vertex directed undirected face+  -> VertexId+  -> Point+vertexPointAt triangulation (VertexId raw) =+  Point+    (triPointX triangulation `pagedUnsafeIndex` fromIntegral raw)+    (triPointY triangulation `pagedUnsafeIndex` fromIntegral raw)++data GeometricBaseOrder+  = GeometricBaseLeft+  | GeometricBaseRight+  deriving stock (Eq, Show)++-- | Finite local work required to turn the two synthetic perimeter bridges+-- into edge-bounded constrained paths.  The witness is derived while the+-- tangent endpoints still identify J exactly; after publication those edges+-- are ordinary constraints and must not be rediscovered by a global scan.+data SeamBridgeSubdivision = SeamBridgeSubdivision+  { seamLowerBridgeDepth :: {-# UNPACK #-} !Int+  , seamUpperBridgeDepth :: {-# UNPACK #-} !Int+  , seamBridgeAddedVertices :: {-# UNPACK #-} !Int+  }++planSeamBridgeSubdivision+  :: RefinementParameters+  -> Point+  -> Point+  -> Point+  -> Point+  -> Either BuildError SeamBridgeSubdivision+planSeamBridgeSubdivision parameters lowerLeft lowerRight upperLeft upperRight =+  case refineMaxEdgeLength parameters of+    Nothing -> Right (SeamBridgeSubdivision 0 0 0)+    Just maximumLength -> do+      let !lowerDepth = requiredBridgeSubdivisionDepth maximumLength lowerLeft lowerRight+          !upperDepth = requiredBridgeSubdivisionDepth maximumLength upperLeft upperRight+          !required =+            subdivisionVertexCount lowerDepth+              + subdivisionVertexCount upperDepth+      if required > toInteger (maxBound :: Int)+        then Left (CapacityExceeded maxBound)+        else do+          let !requiredVertices = fromInteger required+          available <-+            case (requiredVertices, refineMaxAdditionalVertices parameters) of+              (0, Nothing) -> Right 0+              (_, Nothing) -> Left RefinementDomainRequiresFiniteVertexBudget+              (_, Just budget) -> Right budget+          if requiredVertices <= available+            then+              Right+                SeamBridgeSubdivision+                  { seamLowerBridgeDepth = lowerDepth+                  , seamUpperBridgeDepth = upperDepth+                  , seamBridgeAddedVertices = requiredVertices+                  }+            else+              Left+                ( RefinementSeamBridgeBudgetExceeded+                    requiredVertices+                    available+                )+ where+  subdivisionVertexCount :: Int -> Integer+  subdivisionVertexCount depth = (2 :: Integer) ^ depth - 1++requiredBridgeSubdivisionDepth :: Double -> Point -> Point -> Int+requiredBridgeSubdivisionDepth maximumLength from to =+  descend 0 (distance from to)+ where+  descend !depth !currentLength+    | currentLength <= maximumLength = depth+    | depth >= finiteBitSize (0 :: Int) = depth+    | otherwise = descend (depth + 1) (0.5 * currentLength)++vertexPointForOuterEdge+  :: Triangulation mode vertex directed undirected face+  -> Int+  -> Point+vertexPointForOuterEdge triangulation edge =+  vertexPointAt+    triangulation+    (VertexId (fromIntegral (topologyAt triangulation (4 * edge))))++subdivideSeamBridge+  :: forall s vertex directed undirected face+   . (Int -> Bool)+  -> vertex+  -> Int+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Int+  -> ST s (Either BuildError (Seq.Seq Int))+subdivideSeamBridge targetFaceProtected vertexDefault = descend+ where+  descend+    :: Int+    -> MutableDcel s vertex directed undirected face+    -> OperationState s+    -> Int+    -> ST s (Either BuildError (Seq.Seq Int))+  descend depth mutable operation outerEdge+    | depth <= 0 = pure (Right (Seq.singleton outerEdge))+    | otherwise = do+        fromVertex <- readOrigin mutable outerEdge+        toVertex <- readOrigin mutable (outerEdge `xor` 1)+        fromPoint <- pointAt mutable fromVertex+        toPoint <- pointAt mutable toVertex+        case validatePoint Nothing (midpoint fromPoint toPoint) of+          Left obstruction -> pure (Left obstruction)+          Right admitted ->+            let !splitPoint = queryPointValue admitted+             in if splitPoint == fromPoint || splitPoint == toPoint+                  then+                    pure+                      ( Left+                          ( RefinementSeamBridgeMidpointCollapsed+                              (UndirectedEdgeId (fromIntegral (outerEdge `quot` 2)))+                          )+                      )+                  else do+                    vertex <- appendVertex mutable splitPoint vertexDefault+                    splitBoundaryEdgeWithLegalization+                      (\target targetOperation _ firstCandidate secondCandidate ->+                         legalizeEdgesPinned+                           targetFaceProtected+                           target+                           targetOperation+                           [firstCandidate, secondCandidate]+                      )+                      mutable+                      operation+                      outerEdge+                      vertex+                      `bindMutable` \(firstOuter, secondOuter) -> do+                        addCounter operation CounterSteinerPoints 1+                        descend (depth - 1) mutable operation firstOuter+                          `bindMutable` \firstChain ->+                            fmap (fmap (firstChain Seq.><))+                              (descend (depth - 1) mutable operation secondOuter)++executeResidentConstrained+  :: forall vertex+   . (Int -> Bool)+  -> RefinementParameters+  -> vertex+  -> GeometricBaseOrder+  -> Triangulation 'Constrained vertex () () ()+  -> Triangulation 'Constrained vertex () () ()+  -> SeamTangents+  -> Either BuildError+      ( Triangulation 'Constrained vertex () () ()+      , BuildStats+      , PublicationStats+      , V.Vector FaceId+      )+executeResidentConstrained targetFaceProtected parameters vertexDefault order base extension tangents = do+  let !baseDirected = numDirectedEdges base+      !extensionDirected = numDirectedEdges extension+      !baseVertices = numVertices base+      (leftSource, rightSource, leftVertexOffset, rightVertexOffset, leftEdgeOffset, rightEdgeOffset) =+        case order of+          GeometricBaseLeft ->+            (base, extension, 0, baseVertices, 0, baseDirected)+          GeometricBaseRight ->+            (extension, base, baseVertices, 0, baseDirected, 0)+      SeamTangents+        { seamLowerLeft = SeamTangent _ lowerLeft+        , seamLowerRight = SeamTangent _ lowerRight+        , seamUpperLeft = SeamTangent _ upperLeft+        , seamUpperRight = SeamTangent _ upperRight+        } = tangents+  leftFrontier <- maybe (Left SeamFrontierUnavailable) Right (triSeamFrontier leftSource)+  rightFrontier <- maybe (Left SeamFrontierUnavailable) Right (triSeamFrontier rightSource)+  bridgePlan <-+    planSeamBridgeSubdivision+      parameters+      (vertexPointForOuterEdge leftSource lowerLeft)+      (vertexPointForOuterEdge rightSource lowerRight)+      (vertexPointForOuterEdge leftSource upperLeft)+      (vertexPointForOuterEdge rightSource upperRight)+  let !extensionVertices = numVertices extension+      !bridgeVertices = seamBridgeAddedVertices bridgePlan+  transactionAdditional <-+    if bridgeVertices > maxBound - extensionVertices+      then Left (CapacityExceeded maxBound)+      else Right (extensionVertices + bridgeVertices)+  ((newFaces, progress, upperBridgeChain, lowerBridgeChain), triangulation, statistics, publicationStats) <-+    runTransactionWithPublication+      id+      LocalTransaction+      base+      transactionAdditional+      (\mutable operation -> do+         appendSourceVertices mutable extension+         _ <- addEdgeBlock mutable (extensionDirected `quot` 2)+         _ <- addFaceBlock mutable (numFaces extension - 1)+         copySource+           mutable+           extension+           baseVertices+           baseDirected+           (numFaces base - 1)+         copySourceConstraints mutable baseDirected extension+         seamBase <-+           spliceLowerTangent+             mutable+             rightEdgeOffset+             (leftEdgeOffset + lowerLeft)+             lowerRight+         -- The two synthetic hull bridges are the certified perimeter of the+         -- published world.  Once a later extension consumes either bridge it+         -- becomes Γ, so fixing it now preserves the prior solve without a+         -- retrospective source rewrite.+         _ <- setConstraint mutable seamBase+         stitchSeamPinned+           targetFaceProtected+           mutable+           operation+           (Seq.length (seamFrontierEdges leftFrontier))+           (seamTangentIndex (seamLowerLeft tangents))+           (Seq.length (seamFrontierEdges rightFrontier))+           (seamTangentIndex (seamLowerRight tangents))+           seamBase+           (leftVertexOffset + topologyAt leftSource (4 * upperLeft))+           (rightVertexOffset + topologyAt rightSource (4 * upperRight))+           []+           []+           `bindMutable` \(upperBridge, newFaces, progress) -> do+             _ <- setConstraint mutable upperBridge+             subdivideSeamBridge+               targetFaceProtected+               vertexDefault+               (seamLowerBridgeDepth bridgePlan)+               mutable+               operation+               (seamBase `xor` 1)+               `bindMutable` \lowerBridgeChain ->+                 subdivideSeamBridge+                   targetFaceProtected+                   vertexDefault+                   (seamUpperBridgeDepth bridgePlan)+                   mutable+                   operation+                   upperBridge+                   `bindMutable` \upperBridgeChain ->+                     case Seq.lookup 0 upperBridgeChain of+                       Nothing -> pure (Left SeamFrontierUnavailable)+                       Just frontierStart -> do+                         writeFaceEdge mutable 0 frontierStart+                         pure+                           ( Right+                               (newFaces, progress, upperBridgeChain, lowerBridgeChain)+                           )+      )+  let newFaceSeeds = V.fromList (fmap (FaceId . fromIntegral) newFaces)+      joinFaces = exactJoinFaces triangulation targetFaceProtected newFaceSeeds+  certifyJoinBoundary triangulation targetFaceProtected joinFaces+  frontier <-+    combineSeamFrontier+      triangulation+      leftFrontier+      rightFrontier+      leftEdgeOffset+      rightEdgeOffset+      upperBridgeChain+      lowerBridgeChain+      progress+  let published :: Triangulation 'Constrained vertex () () ()+      published = triangulation{triSeamFrontier = Just frontier}+  pure+    ( published+    , statistics+    , publicationStats+    , joinFaces+    )++seamFaceEvidenceForSource+  :: Triangulation 'Constrained vertex () () ()+  -> Triangulation 'Constrained vertex () () ()+  -> Int+  -> [FaceId]+  -> Either BuildError (V.Vector ConstrainedSeamFaceEvidence)+seamFaceEvidenceForSource source target targetOffset sourceFaces =+  V.fromList <$> traverse evidenceFor sourceFaces+ where+  evidenceFor sourceFace = do+    sourcePoints <- facePoints source sourceFace+    let targetFace = FaceId (unFaceId sourceFace + fromIntegral targetOffset)+    targetPoints <- facePoints target targetFace+    if sourcePoints == targetPoints+      then+        case sourcePoints of+          [first, second, third] ->+            Right+              ConstrainedSeamFaceEvidence+                { constrainedSeamSourceFace = sourceFace+                , constrainedSeamTargetFace = targetFace+                , constrainedSeamFaceFirstPoint = first+                , constrainedSeamFaceSecondPoint = second+                , constrainedSeamFaceThirdPoint = third+                }+          points -> Left (faceCardinalityFailure source sourceFace points)+      else Left (SeamProtectedSourceFaceChanged sourceFace)++  facePoints triangulation faceHandle =+    case List.sort (fmap (vertexPointAt triangulation) (faceVertices triangulation faceHandle)) of+      [first, second, third] -> Right [first, second, third]+      points -> Left (faceCardinalityFailure triangulation faceHandle points)++  faceCardinalityFailure triangulation faceHandle _points =+    case faceDirectedEdges triangulation faceHandle of+      edge : _ -> RefinementInputTopologyInvalid (InnerFaceNotTriangularAtEdge edge)+      [] -> RefinementInputTopologyInvalid (FaceMissingAdjacentEdge faceHandle)++data SeparatedOrder+  = LeftBeforeRight+  | RightBeforeLeft++separatedOrder+  :: SeamAxis+  -> Triangulation mode vertex directed undirected face+  -> Triangulation mode' vertex' directed' undirected' face'+  -> SeamFrontierIndex+  -> SeamFrontierIndex+  -> Maybe SeparatedOrder+separatedOrder axis left right leftFrontier rightFrontier+  | numInnerFaces left <= 0 || numInnerFaces right <= 0 = Nothing+  | chartMaximum axis leftFrontier < chartMinimum axis rightFrontier =+      Just LeftBeforeRight+  | chartMaximum axis rightFrontier < chartMinimum axis leftFrontier =+      Just RightBeforeLeft+  | otherwise = Nothing++chartMinimum :: SeamAxis -> SeamFrontierIndex -> Double+chartMinimum axis frontier =+  case axis of+    SeamAxisX -> seamFrontierXMinimum frontier+    SeamAxisY -> seamFrontierYMinimum frontier++chartMaximum :: SeamAxis -> SeamFrontierIndex -> Double+chartMaximum axis frontier =+  case axis of+    SeamAxisX -> seamFrontierXMaximum frontier+    SeamAxisY -> seamFrontierYMaximum frontier++-- The opposite-sign branch cannot overflow in its sum. The same-sign branch+-- cannot overflow in its difference.+data SeamTangent = SeamTangent+  { seamTangentIndex :: {-# UNPACK #-} !Int+  , seamTangentEdge :: {-# UNPACK #-} !Int+  }++data SeamTangents = SeamTangents+  { seamLowerLeft :: !SeamTangent+  , seamLowerRight :: !SeamTangent+  , seamUpperLeft :: !SeamTangent+  , seamUpperRight :: !SeamTangent+  , seamFrontierPointReads :: {-# UNPACK #-} !Int+  }++seamTangents+  :: SeamAxis+  -> Triangulation mode vertex directed undirected face+  -> SeamFrontierIndex+  -> Triangulation mode' vertex' directed' undirected' face'+  -> SeamFrontierIndex+  -> Maybe SeamTangents+seamTangents axis left leftFrontier right rightFrontier = do+  (lowerLeft, lowerRight, lowerReads) <- lowerTangent axis left leftFrontier right rightFrontier+  (upperLeft, upperRight, upperReads) <- upperTangent axis left leftFrontier right rightFrontier+  pure+    SeamTangents+      { seamLowerLeft = lowerLeft+      , seamLowerRight = lowerRight+      , seamUpperLeft = upperLeft+      , seamUpperRight = upperRight+      , seamFrontierPointReads = lowerReads + upperReads+      }++lowerTangent+  :: SeamAxis+  -> Triangulation mode vertex directed undirected face+  -> SeamFrontierIndex+  -> Triangulation mode' vertex' directed' undirected' face'+  -> SeamFrontierIndex+  -> Maybe (SeamTangent, SeamTangent, Int)+-- The walk carries both endpoints' coordinates: a step replaces exactly one+-- endpoint, and the replacement is the neighbour whose coordinates the step's+-- own test already read.+lowerTangent axis left leftFrontier right rightFrontier = do+  (leftStart, leftReads) <- frontierPointAtCount axis left leftFrontier leftStartIndex+  (rightStart, rightReads) <- frontierPointAtCount axis right rightFrontier rightStartIndex+  walk leftStartIndex leftStart rightStartIndex rightStart (leftReads + rightReads)+ where+  leftStartIndex :: Int+  rightStartIndex :: Int+  leftSize :: Int+  rightSize :: Int+  walk+    :: Int+    -> (Double, Double)+    -> Int+    -> (Double, Double)+    -> Int+    -> Maybe (SeamTangent, SeamTangent, Int)+  leftStartIndex = chartLowerRightmost axis leftFrontier+  rightStartIndex = chartLowerLeftmost axis rightFrontier+  leftSize = Seq.length (seamFrontierEdges leftFrontier)+  rightSize = Seq.length (seamFrontierEdges rightFrontier)++  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !reads = do+    nextLeftIndex <- pure (nextIndex leftSize leftIndex)+    previousRightIndex <- pure (previousIndex rightSize rightIndex)+    (nextLeftX, nextLeftY) <- frontierPointAt axis left leftFrontier nextLeftIndex+    (previousRightX, previousRightY) <- frontierPointAt axis right rightFrontier previousRightIndex+    let !nextReads = reads + 2+        !leftBelow =+          orient2dCoordinates leftX leftY rightX rightY nextLeftX nextLeftY == LT+        !rightBelow =+          orient2dCoordinates leftX leftY rightX rightY previousRightX previousRightY == LT+    if leftBelow+      then walk nextLeftIndex (nextLeftX, nextLeftY) rightIndex (rightX, rightY) nextReads+      else+        if rightBelow+          then walk leftIndex (leftX, leftY) previousRightIndex (previousRightX, previousRightY) nextReads+          else do+            leftEdge <- frontierEdgeAt leftFrontier leftIndex+            rightEdge <- frontierEdgeAt rightFrontier rightIndex+            pure+              ( SeamTangent leftIndex leftEdge+              , SeamTangent rightIndex rightEdge+              , nextReads+              )++upperTangent+  :: SeamAxis+  -> Triangulation mode vertex directed undirected face+  -> SeamFrontierIndex+  -> Triangulation mode' vertex' directed' undirected' face'+  -> SeamFrontierIndex+  -> Maybe (SeamTangent, SeamTangent, Int)+upperTangent axis left leftFrontier right rightFrontier = do+  (leftStart, leftReads) <- frontierPointAtCount axis left leftFrontier leftStartIndex+  (rightStart, rightReads) <- frontierPointAtCount axis right rightFrontier rightStartIndex+  walk leftStartIndex leftStart rightStartIndex rightStart (leftReads + rightReads)+ where+  leftStartIndex :: Int+  rightStartIndex :: Int+  leftSize :: Int+  rightSize :: Int+  walk+    :: Int+    -> (Double, Double)+    -> Int+    -> (Double, Double)+    -> Int+    -> Maybe (SeamTangent, SeamTangent, Int)+  leftStartIndex = chartUpperRightmost axis leftFrontier+  rightStartIndex = chartUpperLeftmost axis rightFrontier+  leftSize = Seq.length (seamFrontierEdges leftFrontier)+  rightSize = Seq.length (seamFrontierEdges rightFrontier)++  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !reads = do+    previousLeftIndex <- pure (previousIndex leftSize leftIndex)+    nextRightIndex <- pure (nextIndex rightSize rightIndex)+    (previousLeftX, previousLeftY) <- frontierPointAt axis left leftFrontier previousLeftIndex+    (nextRightX, nextRightY) <- frontierPointAt axis right rightFrontier nextRightIndex+    let !nextReads = reads + 2+        !leftAbove =+          orient2dCoordinates leftX leftY rightX rightY previousLeftX previousLeftY == GT+        !rightAbove =+          orient2dCoordinates leftX leftY rightX rightY nextRightX nextRightY == GT+    if leftAbove+      then walk previousLeftIndex (previousLeftX, previousLeftY) rightIndex (rightX, rightY) nextReads+      else+        if rightAbove+          then walk leftIndex (leftX, leftY) nextRightIndex (nextRightX, nextRightY) nextReads+          else do+            leftEdge <- frontierEdgeAt leftFrontier leftIndex+            rightEdge <- frontierEdgeAt rightFrontier rightIndex+            pure+              ( SeamTangent leftIndex leftEdge+              , SeamTangent rightIndex rightEdge+              , nextReads+              )++frontierEdgeAt :: SeamFrontierIndex -> Int -> Maybe Int+frontierEdgeAt frontier index = Seq.lookup index (seamFrontierEdges frontier)++chartLowerRightmost :: SeamAxis -> SeamFrontierIndex -> Int+chartLowerRightmost axis frontier =+  case axis of+    SeamAxisX -> seamFrontierLowerRightmost frontier+    SeamAxisY -> seamFrontierYLowerRightmost frontier++chartLowerLeftmost :: SeamAxis -> SeamFrontierIndex -> Int+chartLowerLeftmost axis frontier =+  case axis of+    SeamAxisX -> seamFrontierLowerLeftmost frontier+    SeamAxisY -> seamFrontierYLowerLeftmost frontier++chartUpperRightmost :: SeamAxis -> SeamFrontierIndex -> Int+chartUpperRightmost axis frontier =+  case axis of+    SeamAxisX -> seamFrontierUpperRightmost frontier+    SeamAxisY -> seamFrontierYUpperRightmost frontier++chartUpperLeftmost :: SeamAxis -> SeamFrontierIndex -> Int+chartUpperLeftmost axis frontier =+  case axis of+    SeamAxisX -> seamFrontierUpperLeftmost frontier+    SeamAxisY -> seamFrontierYUpperLeftmost frontier++frontierPointAt+  :: SeamAxis+  -> Triangulation mode vertex directed undirected face+  -> SeamFrontierIndex+  -> Int+  -> Maybe (Double, Double)+frontierPointAt axis triangulation frontier index = do+  edge <- frontierEdgeAt frontier index+  let !vertex = topologyAt triangulation (4 * edge)+  pure (chartPoint axis (physicalPoint triangulation vertex))+{-# INLINE frontierPointAt #-}++chartPoint :: SeamAxis -> (Double, Double) -> (Double, Double)+chartPoint axis (x, y) =+  case axis of+    SeamAxisX -> (x, y)+    SeamAxisY -> seamQuarterTurn (x, y)++physicalPoint+  :: Triangulation mode vertex directed undirected face+  -> Int+  -> (Double, Double)+physicalPoint triangulation vertex =+  ( triPointX triangulation `pagedUnsafeIndex` vertex+  , triPointY triangulation `pagedUnsafeIndex` vertex+  )++frontierPointAtCount+  :: SeamAxis+  -> Triangulation mode vertex directed undirected face+  -> SeamFrontierIndex+  -> Int+  -> Maybe ((Double, Double), Int)+frontierPointAtCount axis triangulation frontier index = do+  point <- frontierPointAt axis triangulation frontier index+  pure (point, 1)++nextIndex :: Int -> Int -> Int+nextIndex size index+  | index + 1 == size = 0+  | otherwise = index + 1+{-# INLINE nextIndex #-}++previousIndex :: Int -> Int -> Int+previousIndex size index+  | index == 0 = size - 1+  | otherwise = index - 1+{-# INLINE previousIndex #-}+++mergeSeparated+  :: forall outputMode leftMode rightMode vertex+   . Triangulation leftMode vertex () () ()+  -> Triangulation rightMode vertex () () ()+  -> SeamTangents+   -> Either+      BuildError+      (Triangulation outputMode vertex () () (), BuildStats)+mergeSeparated+  left+  right+  SeamTangents+    { seamLowerLeft = SeamTangent _ lowerLeft+    , seamLowerRight = SeamTangent _ lowerRight+    , seamUpperLeft = SeamTangent _ upperLeft+    , seamUpperRight = SeamTangent _ upperRight+    } = runST $ do+  mutable <- newMutableDcel unitElementDefaults (planarDcelCapacity totalVertices)+  pointCapacityOutcome <- ensurePointCapacity mutable totalVertices+  cellCapacityOutcome <-+    ensureCellCapacity+      mutable+      ((leftDirected + rightDirected) `quot` 2 + 1)+      (leftFaces + rightFaces - 2)+  case (pointCapacityOutcome, cellCapacityOutcome) of+    (Left obstruction, _) -> pure (Left obstruction)+    (_, Left obstruction) -> pure (Left obstruction)+    (Right (), Right ()) -> do+      appendSourceVertices mutable left+      appendSourceVertices mutable right+      _ <- addEdgeBlock mutable ((leftDirected + rightDirected) `quot` 2)+      _ <- addFaceBlock mutable (leftFaces + rightFaces - 2)+      copySource mutable left 0 0 0+      copySource mutable right leftVertices leftDirected (leftFaces - 1)+      base <- spliceLowerTangent mutable leftDirected lowerLeft lowerRight+      operation <- newOperationState (halfEdgeCapacity mutable)+      stitched <-+        stitchSeam+          mutable+          operation+          base+          (topologyAt left (4 * upperLeft))+          (leftVertices + topologyAt right (4 * upperRight))+          []+          []+      case stitched of+        Left obstruction -> pure (Left obstruction)+        Right _ -> do+          statistics <- freezeBuildStats operation+          fmap (\triangulation -> (triangulation, statistics))+            <$> freezeTriangulation mutable+ where+  !leftVertices = numVertices left+  !rightVertices = numVertices right+  !totalVertices = leftVertices + rightVertices+  !leftDirected = numDirectedEdges left+  !rightDirected = numDirectedEdges right+  !leftFaces = numFaces left+  !rightFaces = numFaces right++stitchSeam+  :: MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Int+  -> Int+  -> Int+  -> [Int]+  -> [Int]+  -> ST s (Either BuildError (Int, [Int]))+stitchSeam mutable operation base upperLeft upperRight seeds newFaces =+  fmap (fmap (\(finalBase, faces, _) -> (finalBase, faces)))+    ( stitchSeamWith+        (\target ops edges -> legalizeEdges target ops edges >> pure (Right ()))+        (SeamProgress 0 0 0 0 0 0)+        mutable+        operation+        base+        upperLeft+        upperRight+        seeds+        newFaces+    )++stitchSeamPinned+  :: (Int -> Bool)+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Int+  -> Int+  -> Int+  -> Int+  -> Int+  -> Int+  -> Int+  -> [Int]+  -> [Int]+  -> ST s (Either BuildError (Int, [Int], SeamProgress))+stitchSeamPinned targetFaceProtected mutable operation leftSize leftStart rightSize rightStart base upperLeft upperRight seeds newFaces =+  stitchSeamWith+    (\target ops edges -> legalizeEdgesPinned targetFaceProtected target ops edges >> pure (Right ()))+    (SeamProgress leftSize leftStart (previousIndex leftSize leftStart) rightSize rightStart rightStart)+    mutable+    operation+    base+    upperLeft+    upperRight+    seeds+    newFaces++data SeamProgress = SeamProgress+  { seamProgressLeftSize :: {-# UNPACK #-} !Int+  , seamProgressLeftStart :: {-# UNPACK #-} !Int+  , seamProgressLeftCursor :: {-# UNPACK #-} !Int+  , seamProgressRightSize :: {-# UNPACK #-} !Int+  , seamProgressRightStart :: {-# UNPACK #-} !Int+  , seamProgressRightCursor :: {-# UNPACK #-} !Int+  }++stitchSeamWith+  :: (MutableDcel s vertex directed undirected face -> OperationState s -> [Int] -> ST s (Either BuildError ()))+  -> SeamProgress+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Int+  -> Int+  -> Int+  -> [Int]+  -> [Int]+  -> ST s (Either BuildError (Int, [Int], SeamProgress))+stitchSeamWith legalize progress mutable operation base upperLeft upperRight seeds newFaces = do+  leftVertex <- readOrigin mutable base+  rightVertex <- readOrigin mutable (base `xor` 1)+  if leftVertex == upperLeft && rightVertex == upperRight+    then do+      legalized <- legalize mutable operation seeds+      pure (fmap (const (base, newFaces, progress)) legalized)+    else do+      leftEdge <- readPrevious mutable base+      rightEdge <- readNext mutable base+      nextLeft <- readOrigin mutable leftEdge+      nextRight <- readOrigin mutable (rightEdge `xor` 1)+      leftTurn <- vertexOrientation mutable nextLeft leftVertex rightVertex+      rightTurn <- vertexOrientation mutable leftVertex rightVertex nextRight+      chooseRight <-+        if rightVertex == upperRight+          then pure False+          else+            if leftVertex == upperLeft+              then pure True+              else+                case (leftTurn == GT, rightTurn == GT) of+                  (True, True) ->+                    (== GT) <$> vertexInCircle mutable nextLeft leftVertex rightVertex nextRight+                  (False, True) -> pure True+                  _ -> pure False+      if chooseRight+        then do+          closed <- closeOuterTurn mutable base+          case closed of+            Left obstruction -> pure (Left obstruction)+            Right nextBase ->+              do+                face <- readFace mutable (nextBase `xor` 1)+                stitchSeamWith+                  legalize+                  (advanceRight progress)+                  mutable+                  operation+                  nextBase+                  upperLeft+                  upperRight+                  (base : rightEdge : seeds)+                  (face : newFaces)+        else do+          closed <- closeOuterTurn mutable leftEdge+          case closed of+            Left obstruction -> pure (Left obstruction)+            Right nextBase ->+              do+                face <- readFace mutable (nextBase `xor` 1)+                stitchSeamWith+                  legalize+                  (advanceLeft progress)+                  mutable+                  operation+                  nextBase+                  upperLeft+                  upperRight+                  (leftEdge : base : seeds)+                  (face : newFaces)++advanceLeft :: SeamProgress -> SeamProgress+advanceLeft progress@SeamProgress{seamProgressLeftSize, seamProgressLeftCursor} =+  progress+    { seamProgressLeftCursor =+        previousIndex seamProgressLeftSize seamProgressLeftCursor+    }++advanceRight :: SeamProgress -> SeamProgress+advanceRight progress@SeamProgress{seamProgressRightSize, seamProgressRightCursor} =+  progress+    { seamProgressRightCursor =+        nextIndex seamProgressRightSize seamProgressRightCursor+    }++combineSeamFrontier+  :: Triangulation mode vertex directed undirected face+  -> SeamFrontierIndex+  -> SeamFrontierIndex+  -> Int+  -> Int+  -> Seq.Seq Int+  -> Seq.Seq Int+  -> SeamProgress+  -> Either BuildError SeamFrontierIndex+combineSeamFrontier triangulation leftFrontier rightFrontier leftEdgeOffset rightEdgeOffset upperBridge lowerBridge progress+  | seamProgressLeftSize progress /= Seq.length (seamFrontierEdges leftFrontier) = Left SeamFrontierUnavailable+  | seamProgressRightSize progress /= Seq.length (seamFrontierEdges rightFrontier) = Left SeamFrontierUnavailable+  | otherwise = do+  leftEdges <- translatedEdges leftFrontier leftEdgeOffset+  rightEdges <- translatedEdges rightFrontier rightEdgeOffset+  rightResidual <-+    cycleSlice+      rightEdges+      (seamProgressRightSize progress)+      (seamProgressRightCursor progress)+      (previousIndex+         (seamProgressRightSize progress)+         (seamProgressRightStart progress))+  leftResidual <-+    cycleSlice+      leftEdges+      (seamProgressLeftSize progress)+      (seamProgressLeftStart progress)+      (seamProgressLeftCursor progress)+  let !outputEdges =+        upperBridge+          Seq.>< rightResidual+          Seq.>< lowerBridge+          Seq.>< leftResidual+      !rightOffset = Seq.length upperBridge+      !lowerOffset = rightOffset + Seq.length rightResidual+      !leftOffset = lowerOffset + Seq.length lowerBridge+      !rightEnd = previousIndex (seamProgressRightSize progress) progressRightStart+      !leftEnd = seamProgressLeftCursor progress+      !candidatePositions =+        IntSet.toAscList+          ( IntSet.fromList+              ( residualBoundaryPositions 0 upperBridge+                  ++ residualBoundaryPositions rightOffset rightResidual+                  ++ residualBoundaryPositions lowerOffset lowerBridge+                  ++ residualBoundaryPositions leftOffset leftResidual+                  ++ sourceAnchorPositions+                    rightOffset+                    (seamProgressRightCursor progress)+                    rightEnd+                    (seamProgressRightSize progress)+                    rightFrontier+                  ++ sourceAnchorPositions+                    leftOffset+                    progressLeftStart+                    leftEnd+                    (seamProgressLeftSize progress)+                    leftFrontier+              )+          )+  (minimumX, maximumX, lowerRight, lowerLeft, upperRight, upperLeft) <-+    summarizeOutputChart id candidatePositions outputEdges+  (minimumY, maximumY, yLowerRight, yLowerLeft, yUpperRight, yUpperLeft) <-+    summarizeOutputChart seamQuarterTurn candidatePositions outputEdges+  pure+    SeamFrontierIndex+      { seamFrontierEdges = outputEdges+      , seamFrontierXMinimum = minimumX+      , seamFrontierXMaximum = maximumX+      , seamFrontierLowerRightmost = lowerRight+      , seamFrontierLowerLeftmost = lowerLeft+      , seamFrontierUpperRightmost = upperRight+      , seamFrontierUpperLeftmost = upperLeft+      , seamFrontierYMinimum = minimumY+      , seamFrontierYMaximum = maximumY+      , seamFrontierYLowerRightmost = yLowerRight+      , seamFrontierYLowerLeftmost = yLowerLeft+      , seamFrontierYUpperRightmost = yUpperRight+      , seamFrontierYUpperLeftmost = yUpperLeft+      }+ where+  progressRightStart = seamProgressRightStart progress+  progressLeftStart = seamProgressLeftStart progress++  translatedEdges+    :: SeamFrontierIndex+    -> Int+    -> Either BuildError (Seq.Seq Int)+  translatedEdges frontier offset+    | offset == 0 = Right (seamFrontierEdges frontier)+    | otherwise = Right (fmap (+ offset) (seamFrontierEdges frontier))++  cycleSlice+    :: Seq.Seq Int+    -> Int+    -> Int+    -> Int+    -> Either BuildError (Seq.Seq Int)+  cycleSlice edges size start end+    | size <= 0 || start < 0 || end < 0 || start >= size || end >= size = Left SeamFrontierUnavailable+    | start <= end = Right (Seq.take (end - start + 1) (Seq.drop start edges))+    | otherwise =+        Right+          ( Seq.drop start edges+              Seq.>< Seq.take (end + 1) edges+          )++  residualBoundaryPositions :: Int -> Seq.Seq Int -> [Int]+  residualBoundaryPositions offset residual =+    case Seq.length residual of+      0 -> []+      lengthOfResidual -> [offset, offset + lengthOfResidual - 1]++  sourceAnchorPositions+    :: Int+    -> Int+    -> Int+    -> Int+    -> SeamFrontierIndex+    -> [Int]+  sourceAnchorPositions offset start end size frontier =+    [ offset + position+    | anchor <- sourceAnchors frontier+    , Right position <- [intervalPosition size anchor start end]+    ]++  sourceAnchors :: SeamFrontierIndex -> [Int]+  sourceAnchors frontier =+    [ seamFrontierLowerRightmost frontier+    , seamFrontierLowerLeftmost frontier+    , seamFrontierUpperRightmost frontier+    , seamFrontierUpperLeftmost frontier+    , seamFrontierYLowerRightmost frontier+    , seamFrontierYLowerLeftmost frontier+    , seamFrontierYUpperRightmost frontier+    , seamFrontierYUpperLeftmost frontier+    ]++  summarizeOutputChart+    :: ((Double, Double) -> (Double, Double))+    -> [Int]+    -> Seq.Seq Int+    -> Either BuildError (Double, Double, Int, Int, Int, Int)+  summarizeOutputChart transform candidatePositions outputEdges = do+    candidates <-+      traverse+        (\index -> do+           edge <- maybe (Left SeamFrontierUnavailable) Right (Seq.lookup index outputEdges)+           let !vertex = topologyAt triangulation (4 * edge)+           pure (index, physicalPoint triangulation vertex))+        candidatePositions+    maybe (Left SeamFrontierUnavailable) Right+      (summarizeSeamChart transform candidates)++  intervalPosition :: Int -> Int -> Int -> Int -> Either BuildError Int+  intervalPosition size target start end+    | target < 0 || target >= size = Left SeamFrontierUnavailable+    | start <= end =+        if target >= start && target <= end+          then Right (target - start)+          else Left SeamFrontierUnavailable+    | target >= start = Right (target - start)+    | target <= end = Right (size - start + target)+    | otherwise = Left SeamFrontierUnavailable++vertexOrientation+  :: MutableDcel s vertex directed undirected face+  -> Int+  -> Int+  -> Int+  -> ST s Ordering+vertexOrientation mutable a b c = do+  ax <- readPointX mutable a+  ay <- readPointY mutable a+  bx <- readPointX mutable b+  by <- readPointY mutable b+  cx <- readPointX mutable c+  cy <- readPointY mutable c+  pure $! orient2dCoordinates ax ay bx by cx cy+{-# INLINE vertexOrientation #-}++vertexInCircle+  :: MutableDcel s vertex directed undirected face+  -> Int+  -> Int+  -> Int+  -> Int+  -> ST s Ordering+vertexInCircle mutable a b c d = do+  ax <- readPointX mutable a+  ay <- readPointY mutable a+  bx <- readPointX mutable b+  by <- readPointY mutable b+  cx <- readPointX mutable c+  cy <- readPointY mutable c+  dx <- readPointX mutable d+  dy <- readPointY mutable d+  pure $! inCircleCoordinates ax ay bx by cx cy dx dy+{-# INLINE vertexInCircle #-}++appendSourceVertices+  :: MutableDcel s vertex () () ()+  -> Triangulation mode vertex () () ()+  -> ST s ()+appendSourceVertices mutable source =+  forRange 0 (numVertices source) $ \vertex -> do+    _ <-+      appendVertexCoordinates+        mutable+        (triPointX source `pagedUnsafeIndex` vertex)+        (triPointY source `pagedUnsafeIndex` vertex)+        (vertexData source (VertexId (fromIntegral vertex)))+    pure ()++copySource+  :: MutableDcel s vertex () () ()+  -> Triangulation mode vertex () () ()+  -> Int+  -> Int+  -> Int+  -> ST s ()+copySource mutable source vertexOffset edgeOffset faceOffset = do+  forRange 0 (numDirectedEdges source) $ \edge -> do+    let !target = edgeOffset + edge+        !sourceBase = 4 * edge+        !sourceFace = topologyAt source (sourceBase + 3)+        !targetFace = if sourceFace == 0 then 0 else faceOffset + sourceFace+    writeOrigin mutable target (vertexOffset + topologyAt source sourceBase)+    writeNext mutable target (edgeOffset + topologyAt source (sourceBase + 1))+    writePrevious mutable target (edgeOffset + topologyAt source (sourceBase + 2))+    writeFace mutable target targetFace+  forRange 0 (numVertices source) $ \vertex ->+    case vertexOutEdge source (VertexId (fromIntegral vertex)) of+      Nothing -> markConnected mutable (vertexOffset + vertex) (-1)+      Just (DirectedEdgeId edge) ->+        markConnected mutable (vertexOffset + vertex) (edgeOffset + fromIntegral edge)+  forRange 1 (numFaces source) $ \face ->+    case adjacentEdge source (FaceId (fromIntegral face)) of+      Nothing -> writeFaceEdge mutable (faceOffset + face) (-1)+      Just (DirectedEdgeId edge) ->+        writeFaceEdge mutable (faceOffset + face) (edgeOffset + fromIntegral edge)++copySourceConstraints+  :: MutableDcel s vertex () () ()+  -> Int+  -> Triangulation 'Constrained vertex () () ()+  -> ST s ()+copySourceConstraints mutable directedEdgeOffset source =+  traverse_+    (\(UndirectedEdgeId edge) ->+       ()+         <$ setConstraint+           mutable+           (directedEdgeOffset + 2 * fromIntegral edge)+    )+    (constraintEdges source)  spliceLowerTangent   :: MutableDcel s vertex () () ()
src-build/Moonlight/Triangulation/Internal/Location.hs view
@@ -4,12 +4,14 @@ module Moonlight.Triangulation.Internal.Location   ( MutableLocation (..)   , locateMutable+  , locateMutableWithin   , locateLineMutable   , visibleOuterEdge   ) where  import Control.Monad.ST (ST) import Data.Bits ((.&.))+import qualified Data.IntSet as IntSet import Data.STRef (readSTRef, writeSTRef) import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex) import Moonlight.Triangulation.Internal.FaceProbe@@ -20,6 +22,10 @@   , addCounter   , maxCounter   )+import Moonlight.Triangulation.Handles.HandleDefs+  ( FaceId (..)+  , UndirectedEdgeId (..)+  ) import Moonlight.Triangulation.Math import Moonlight.Triangulation.Types @@ -38,6 +44,29 @@   -> Point   -> ST s (Either BuildError MutableLocation) locateMutable mutable operation hint rawQuery = do+  locateMutableWithBarrier Nothing mutable operation hint rawQuery++-- | Locate from a checked local face while refusing to walk across the exact+-- interface section. The ordinary locator remains unchanged for unrestricted+-- refinement; the local interpreter supplies only its compact barrier pairs.+locateMutableWithin+  :: IntSet.IntSet+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Maybe Int+  -> Point+  -> ST s (Either BuildError MutableLocation)+locateMutableWithin interfacePairs =+  locateMutableWithBarrier (Just interfacePairs)++locateMutableWithBarrier+  :: Maybe IntSet.IntSet+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Maybe Int+  -> Point+  -> ST s (Either BuildError MutableLocation)+locateMutableWithBarrier barrier mutable operation hint rawQuery = do   connected <- connectedCount mutable   faces <- faceCount mutable   let !query = canonicalPoint rawQuery@@ -46,17 +75,17 @@     _ | faces <= 1 -> Right <$> locateLineMutable mutable query     -- A hint is the caller vouching for adjacency, so the exact walk starts     -- there directly; only an unhinted locate buys the vertex descent.-    _ | Just face <- hint, face > 0 && face < faces -> walk mutable operation query face+    _ | Just face <- hint, face > 0 && face < faces -> walkWithBarrier barrier mutable operation query face     _ -> do       start <- chooseStartFace mutable hint       anchor <- readFaceEdge mutable start       if anchor < 0-        then walk mutable operation query start+        then walkWithBarrier barrier mutable operation query start         else do           origin <- readOrigin mutable anchor           nearest <- descendToNearest mutable operation query origin           face <- incidentInnerFace mutable nearest start-          walk mutable operation query face+          walkWithBarrier barrier mutable operation query face  -- | Greedy first-improvement descent through vertex neighbours: hop to the -- first neighbour strictly closer to the query until none improves. Distance@@ -208,6 +237,16 @@   -> Int   -> ST s (Either BuildError MutableLocation) walk mutable operation query initialFace = do+  walkWithBarrier Nothing mutable operation query initialFace++walkWithBarrier+  :: Maybe IntSet.IntSet+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Point+  -> Int+  -> ST s (Either BuildError MutableLocation)+walkWithBarrier barrier mutable operation query initialFace = do   faces <- faceCount mutable   halfEdges <- directedEdgeCount mutable   let !budget = max 8 (faces + halfEdges `quot` 2 + 4)@@ -238,11 +277,22 @@             pure (Right probe)           MutableOutsideHull edge -> do             adjacent <- readFace mutable edge-            if adjacent == 0-              then do-                maxCounter operation CounterLocationMaxWalk nextSteps-                pure (Right (MutableOutsideHull edge))-              else go (remaining - 1) nextSteps adjacent+            if adjacent > 0+                && maybe False (IntSet.member (edge `quot` 2)) barrier+              then+                pure+                  ( Left+                      ( RefinementDomainWouldCrossInterface+                          (UndirectedEdgeId (fromIntegral (edge `quot` 2)))+                          (FaceId (fromIntegral face))+                      )+                  )+              else+                if adjacent == 0+                  then do+                    maxCounter operation CounterLocationMaxWalk nextSteps+                    pure (Right (MutableOutsideHull edge))+                  else go (remaining - 1) nextSteps adjacent           MutableEmpty -> pure (Left (PointLocationFailed query))  probeFace :: MutableDcel s vertex directed undirected face -> Int -> Point -> ST s MutableLocation@@ -313,4 +363,3 @@           else do             edgeNext <- readNext mutable edge             if edgeNext == start then pure (maybe edge id fallback') else go (remaining - 1) start edgeNext fallback'-
src-build/Moonlight/Triangulation/Internal/Refinement.hs view
@@ -21,7 +21,8 @@ import Data.Foldable (traverse_) import qualified Data.IntSet as IntSet import Data.List (find)-import qualified Data.Map.Strict as Map+import qualified Data.List as List+import qualified Data.IntMap.Strict as IntMap import Data.Maybe (fromMaybe) import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef) import qualified Data.Vector.Unboxed.Mutable as MUV@@ -30,6 +31,7 @@ import Moonlight.Triangulation.Internal.DcelOperations.Legalize (legalizeEdges) import Moonlight.Triangulation.Internal.DcelOperations.Subdivide (insertOnEdge) import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)+import Moonlight.Triangulation.Internal.Dyadic (exactDiametralDot) import Moonlight.Triangulation.Internal.Growable   ( GrowableWord32   , clearGrowable@@ -42,6 +44,7 @@ import Moonlight.Triangulation.Internal.Location   ( MutableLocation (..)   , locateMutable+  , locateMutableWithin   ) import Moonlight.Triangulation.Internal.FaceQueue   ( FaceQueue@@ -96,6 +99,7 @@   , squaredDistanceWide   , triangleArea   , triangleRadiusEdgeRatioSquaredWithArea+  , triangleRadiusEdgeRatioSquaredWithAreaAndSides   , validateCoordinate   ) import Moonlight.Triangulation.Insertion (insertExistingVertexAtLocation)@@ -105,14 +109,121 @@   , RefinementParameters (..)   ) --- | The worklists refinement owns for the duration of one transaction. Every--- one is local to the cavity or segment it drains; nothing geometric is--- indexed globally.+-- | The worklists refinement owns for the duration of one transaction. Cavity+-- and segment queues are local to the current rewrite; the face-capacity mark+-- planes remain a bounded publication workspace until the compact arena is+-- replaced by a domain-indexed one.+data BoolMarks s+  = DenseBoolMarks !(MUV.MVector s Bool)+  | SparseBoolMarks !(STRef s IntSet.IntSet)++data EpochMarks s+  = DenseEpochMarks !(MUV.MVector s Word32)+  | SparseEpochMarks !(STRef s (IntMap.IntMap Word32))++data PairSlots s+  = DensePairSlots !(MUV.MVector s Word32)+  | SparsePairSlots !(STRef s (IntMap.IntMap Word32))++data SegmentOrigins s+  = DenseSegmentOrigins !(MUV.MVector s Word32)+  | SparseSegmentOrigins !(STRef s (IntMap.IntMap (Word32, Word32)))++newBoolMarks :: Maybe Int -> ST s (BoolMarks s)+newBoolMarks capacity =+  case capacity of+    Just size -> DenseBoolMarks <$> MUV.replicate (max 1 size) False+    Nothing -> SparseBoolMarks <$> newSTRef IntSet.empty++readBoolMark :: BoolMarks s -> Int -> ST s Bool+readBoolMark marks index =+  case marks of+    DenseBoolMarks values -> MUV.unsafeRead values index+    SparseBoolMarks values -> IntSet.member index <$> readSTRef values++writeBoolMark :: BoolMarks s -> Int -> Bool -> ST s ()+writeBoolMark marks index enabled =+  case marks of+    DenseBoolMarks values -> MUV.unsafeWrite values index enabled+    SparseBoolMarks values ->+      modifySTRef' values+        (if enabled then IntSet.insert index else IntSet.delete index)++newEpochMarks :: Maybe Int -> ST s (EpochMarks s)+newEpochMarks capacity =+  case capacity of+    Just size -> DenseEpochMarks <$> MUV.replicate (max 1 size) 0+    Nothing -> SparseEpochMarks <$> newSTRef IntMap.empty++readEpochMark :: EpochMarks s -> Int -> ST s Word32+readEpochMark marks index =+  case marks of+    DenseEpochMarks values -> MUV.unsafeRead values index+    SparseEpochMarks values -> fromMaybe 0 . IntMap.lookup index <$> readSTRef values++writeEpochMark :: EpochMarks s -> Int -> Word32 -> ST s ()+writeEpochMark marks index epoch =+  case marks of+    DenseEpochMarks values -> MUV.unsafeWrite values index epoch+    SparseEpochMarks values -> modifySTRef' values (IntMap.insert index epoch)++clearEpochMarks :: EpochMarks s -> ST s ()+clearEpochMarks marks =+  case marks of+    DenseEpochMarks values -> MUV.set values 0+    SparseEpochMarks values -> writeSTRef values IntMap.empty++newPairSlots :: Maybe Int -> ST s (PairSlots s)+newPairSlots capacity =+  case capacity of+    Just size -> DensePairSlots <$> MUV.replicate (max 1 size) noIndex+    Nothing -> SparsePairSlots <$> newSTRef IntMap.empty++readPairSlot :: PairSlots s -> Int -> ST s Word32+readPairSlot slots pair =+  case slots of+    DensePairSlots values -> MUV.unsafeRead values pair+    SparsePairSlots values -> fromMaybe noIndex . IntMap.lookup pair <$> readSTRef values++writePairSlot :: PairSlots s -> Int -> Word32 -> ST s ()+writePairSlot slots pair value =+  case slots of+    DensePairSlots values -> MUV.unsafeWrite values pair value+    SparsePairSlots values ->+      modifySTRef' values+        (if value == noIndex then IntMap.delete pair else IntMap.insert pair value)++newSegmentOrigins :: Maybe Int -> ST s (SegmentOrigins s)+newSegmentOrigins capacity =+  case capacity of+    Just size -> DenseSegmentOrigins <$> MUV.replicate (2 * max 1 size) noIndex+    Nothing -> SparseSegmentOrigins <$> newSTRef IntMap.empty++readSegmentOrigin :: SegmentOrigins s -> Int -> ST s (Word32, Word32)+readSegmentOrigin origins vertex = do+  case origins of+    DenseSegmentOrigins values ->+      (,)+        <$> MUV.unsafeRead values (2 * vertex)+        <*> MUV.unsafeRead values (2 * vertex + 1)+    SparseSegmentOrigins values -> do+      stored <- readSTRef values+      pure (fromMaybe (noIndex, noIndex) (IntMap.lookup vertex stored))++writeSegmentOrigin :: SegmentOrigins s -> Int -> Word32 -> Word32 -> ST s ()+writeSegmentOrigin origins vertex first second =+  case origins of+    DenseSegmentOrigins values -> do+      MUV.unsafeWrite values (2 * vertex) first+      MUV.unsafeWrite values (2 * vertex + 1) second+    SparseSegmentOrigins values ->+      modifySTRef' values (IntMap.insert vertex (first, second))+ data Workspace s = Workspace   { wsQueue :: !(FaceQueue s)-  , wsExcluded :: !(MUV.MVector s Bool)-  , wsPermitted :: !(Maybe (MUV.MVector s Bool))-  , wsVisited :: !(Maybe (MUV.MVector s Bool))+  , wsExcluded :: !(BoolMarks s)+  , wsPermitted :: !(Maybe (BoolMarks s))+  , wsVisited :: !(Maybe (BoolMarks s))   , wsInterfaceBoundaryReads :: !(STRef s Int)   , wsBoundaryCrossingAttempts :: !(STRef s Int)   , wsForcedSplits :: !(GrowableWord32 s)@@ -121,18 +232,18 @@   -- the pair is not queued. A re-push relocates the pair to the top and leaves   -- the older entry behind as a slot that no longer names it, so a pair is   -- tested once at its most recent offer instead of once per offer.-  , wsEncroachmentSlot :: !(MUV.MVector s Word32)+  , wsEncroachmentSlot :: !(PairSlots s)   -- | Steiner vertices on constraints carry the original segment's endpoint   -- pair, two words per vertex, 'noIndex' when the vertex is not on one.-  , wsSegmentOrigin :: !(MUV.MVector s Word32)+  , wsSegmentOrigin :: !(SegmentOrigins s)   -- | Cavity discovery state: an epoch stamp per face, edge pair and boundary   -- vertex, plus the three records a successful commit consumes. A cavity is   -- explored without mutating topology; the commit either fans the recorded   -- boundary or abandons the records entirely.   , wsCavityEpoch :: !(STRef s Word32)-  , wsCavityFaceMarks :: !(MUV.MVector s Word32)-  , wsCavityPairMarks :: !(MUV.MVector s Word32)-  , wsCavityVertexMarks :: !(MUV.MVector s Word32)+  , wsCavityFaceMarks :: !(EpochMarks s)+  , wsCavityPairMarks :: !(EpochMarks s)+  , wsCavityVertexMarks :: !(EpochMarks s)   , wsCavityFaces :: !(GrowableWord32 s)   , wsCavityInternal :: !(GrowableWord32 s)   , wsCavityBoundary :: !(GrowableWord32 s)@@ -147,9 +258,14 @@  data FaceHint   = FaceAcceptable+  | FaceMustSplitLongestEdge {-# UNPACK #-} !Int   | FaceMustRefine   | FaceShouldRefine +data EdgeSplitLaw+  = SplitFixedSegment+  | SplitQualityEdge+ -- | The initial support a refinement run is allowed to inspect. Subsequent -- cavity, star, and fixed-edge propagation remains exactly the ordinary -- refinement law; only the first offers differ. The constructor is internal@@ -165,9 +281,7 @@ data RefinementDomain = RefinementDomain   { refinementDomainPermittedFaces :: !IntSet.IntSet   , refinementDomainInterfacePairs :: !IntSet.IntSet-  , refinementDomainInputFaces :: !(Map.Map FaceId [Point])   , refinementDomainInputFaceCount :: {-# UNPACK #-} !Int-  , refinementDomainInputEdgeCount :: {-# UNPACK #-} !Int   }   deriving stock (Eq, Show) @@ -184,34 +298,44 @@   -> IntSet.IntSet   -> RefinementInitialSeed   -> Maybe RefinementDomain-  -> ST s (Either BuildError (Bool, Int, [Int], [Int], Int, Int))-refineMutable makeVertex mutable operation parameters originalVertexCount initialExcludedFaces initialSeed domain = do-  let !faceBound = 3 * pointCapacity mutable + 8-      !pairBound = mdHalfCapacity mutable `quot` 2+  -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))+refineMutable makeVertex mutable operation parameters budget initialExcludedFaces initialSeed domain = do+  let !faceBound =+        case domain of+          Nothing -> Just (3 * pointCapacity mutable + 8)+          Just _ -> Nothing+      !pairBound =+        case domain of+          Nothing -> Just (mdHalfCapacity mutable `quot` 2)+          Just _ -> Nothing+      !vertexBound =+        case domain of+          Nothing -> Just (pointCapacity mutable)+          Just _ -> Nothing   wsQueue <- newFaceQueue faceBound-  wsExcluded <- MUV.replicate faceBound False-  wsPermitted <- traverse (const (MUV.replicate faceBound False)) domain-  wsVisited <- traverse (const (MUV.replicate faceBound False)) domain+  wsExcluded <- newBoolMarks faceBound+  wsPermitted <- traverse (const (newBoolMarks Nothing)) domain+  wsVisited <- traverse (const (newBoolMarks Nothing)) domain   wsInterfaceBoundaryReads <- newSTRef 0   wsBoundaryCrossingAttempts <- newSTRef 0   traverse_     (\membership ->        traverse_-         (\face -> when (face < faceBound) (MUV.unsafeWrite membership face True))+         (\face -> writeBoolMark membership face True)          (maybe [] (IntSet.toAscList . refinementDomainPermittedFaces) domain)     )     wsPermitted   when (refineExcludeOuterFaces parameters) $-    forM_ (IntSet.toAscList initialExcludedFaces) $ \face ->-      when (face < faceBound) (MUV.unsafeWrite wsExcluded face True)+    traverse_ (\face -> writeBoolMark wsExcluded face True)+      (IntSet.toAscList initialExcludedFaces)   wsForcedSplits <- newGrowableWord32 16   wsEncroachment <- newGrowableWord32 64-  wsEncroachmentSlot <- MUV.replicate (max 1 pairBound) noIndex-  wsSegmentOrigin <- MUV.replicate (2 * pointCapacity mutable) noIndex+  wsEncroachmentSlot <- newPairSlots pairBound+  wsSegmentOrigin <- newSegmentOrigins vertexBound   wsCavityEpoch <- newSTRef 0-  wsCavityFaceMarks <- MUV.replicate faceBound 0-  wsCavityPairMarks <- MUV.replicate (max 1 pairBound) 0-  wsCavityVertexMarks <- MUV.replicate (pointCapacity mutable) 0+  wsCavityFaceMarks <- newEpochMarks faceBound+  wsCavityPairMarks <- newEpochMarks pairBound+  wsCavityVertexMarks <- newEpochMarks vertexBound   wsCavityFaces <- newGrowableWord32 16   wsCavityInternal <- newGrowableWord32 16   wsCavityBoundary <- newGrowableWord32 16@@ -222,13 +346,14 @@   seedInitialWork workspace initialSeed   loop workspace 0  where-  !limit = max 0 (fromMaybe (10 * originalVertexCount) (refineMaxAdditionalVertices parameters))+  !limit = max 0 budget    -- The three bounds every queued face is measured against. They are fixed for   -- the whole run, and the ratio bound is squared once here rather than once   -- per face the ratio test reaches.   !maximumArea = refineMaxArea parameters   !minimumArea = refineMinArea parameters+  !maximumSquaredEdgeLength = fmap (\bound -> bound * bound) (refineMaxEdgeLength parameters)   !squaredRatioBound = case refineMaxRadiusEdgeRatio parameters of     Nothing -> Nothing     Just bound -> Just $! squareBound bound@@ -249,7 +374,7 @@   pushEncroachment Workspace{wsEncroachment, wsEncroachmentSlot} pair = do     index <- growableLength wsEncroachment     pushGrowable wsEncroachment (packIndex pair)-    MUV.unsafeWrite wsEncroachmentSlot pair (packIndex index)+    writePairSlot wsEncroachmentSlot pair (packIndex index)    -- 'noIndex' when the offers are drained.   popEncroachment :: Workspace s -> ST s Word32@@ -263,10 +388,10 @@           let !index = size - 1           packed <- popGrowableOr noIndex wsEncroachment           let !pair = fromIntegral packed-          slot <- MUV.unsafeRead wsEncroachmentSlot pair+          slot <- readPairSlot wsEncroachmentSlot pair           if slot == packIndex index             then do-              MUV.unsafeWrite wsEncroachmentSlot pair noIndex+              writePairSlot wsEncroachmentSlot pair noIndex               pure packed             else go @@ -278,23 +403,23 @@     let !next = current + 1     if next == 0       then do-        MUV.set wsCavityFaceMarks 0-        MUV.set wsCavityPairMarks 0-        MUV.set wsCavityVertexMarks 0+        clearEpochMarks wsCavityFaceMarks+        clearEpochMarks wsCavityPairMarks+        clearEpochMarks wsCavityVertexMarks         writeSTRef wsCavityEpoch 1         pure 1       else do         writeSTRef wsCavityEpoch next         pure next -  loop :: Workspace s -> Int -> ST s (Either BuildError (Bool, Int, [Int], [Int], Int, Int))+  loop :: Workspace s -> Int -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))   loop workspace@Workspace{wsQueue, wsForcedSplits} !added     | added >= limit = finish workspace False added     | otherwise = do         forced <- popGrowableOr noIndex wsForcedSplits         if forced /= noIndex           then do-            splitOutcome <- resolveSplit workspace (fromIntegral forced)+            splitOutcome <- resolveSplit SplitFixedSegment workspace (fromIntegral forced)             case splitOutcome of               Left obstruction -> pure (Left obstruction)               Right split ->@@ -325,31 +450,24 @@                       Left failure -> pure (Left failure)                       Right gained -> loop workspace (added + gained) -  finish :: Workspace s -> Bool -> Int -> ST s (Either BuildError (Bool, Int, [Int], [Int], Int, Int))-  finish Workspace{wsExcluded, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts} complete added = do-    faces <- faceCount mutable-    let gather :: [Int] -> Int -> ST s [Int]-        gather !collected face-          | face < 0 = pure collected-          | otherwise = do-              flagged <- MUV.unsafeRead wsExcluded face-              gather (if flagged then face : collected else collected) (face - 1)-    excludedFaces <- gather [] (min (MUV.length wsExcluded) faces - 1)-    visitedFaces <--      case wsVisited of-        Nothing -> pure []-        Just visited -> do-          let gatherVisited :: [Int] -> Int -> ST s [Int]-              gatherVisited !collected face-                | face < 1 = pure collected-                | otherwise = do-                    flagged <- MUV.unsafeRead visited face-                    gatherVisited (if flagged then face : collected else collected) (face - 1)-          gatherVisited [] (min (MUV.length visited) faces - 1)+  finish :: Workspace s -> Bool -> Int -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))+  finish Workspace{wsExcluded, wsPermitted, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts} complete added = do+    excludedFaces <- markedIndices wsExcluded+    visitedFaces <- maybe (pure []) markedIndices wsVisited+    permittedFaces <- maybe (pure []) markedIndices wsPermitted     interfaceBoundaryReads <- readSTRef wsInterfaceBoundaryReads     boundaryCrossingAttempts <- readSTRef wsBoundaryCrossingAttempts-    pure (Right (complete, added, excludedFaces, visitedFaces, interfaceBoundaryReads, boundaryCrossingAttempts))+    pure (Right (complete, added, excludedFaces, visitedFaces, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts)) +  markedIndices :: BoolMarks s -> ST s [Int]+  markedIndices marks =+    case marks of+      DenseBoolMarks values -> do+        let indices = [0 .. MUV.length values - 1]+        filterM (MUV.unsafeRead values) indices+      SparseBoolMarks values -> IntSet.toAscList <$> readSTRef values++   offerAll :: FaceQueue s -> ST s ()   offerAll queue = do     faces <- faceCount mutable@@ -411,6 +529,12 @@     maybe False (IntSet.member pair . refinementDomainInterfacePairs) domain   {-# INLINE isInterfacePair #-} +  isHullPair :: Int -> ST s Bool+  isHullPair pair = do+    forwardFace <- readFace mutable (2 * pair)+    backwardFace <- readFace mutable (2 * pair + 1)+    pure (forwardFace == 0 || backwardFace == 0)+   interfaceCrossing :: Workspace s -> Int -> ST s BuildError   interfaceCrossing Workspace{wsPermitted, wsBoundaryCrossingAttempts} pair = do     modifySTRef' wsBoundaryCrossingAttempts (+ 1)@@ -427,38 +551,58 @@                 then Just forwardFace                 else if backwardPermitted then Just backwardFace else Nothing             )-    pure-      ( case joinFace of-          Just face ->-            RefinementDomainWouldCrossInterface-              (UndirectedEdgeId (fromIntegral pair))-              (FaceId (fromIntegral face))-          Nothing -> RefinementDomainTopologyChanged-      )+    let result =+          case joinFace of+            Just face ->+              RefinementDomainWouldCrossInterface+                (UndirectedEdgeId (fromIntegral pair))+                (FaceId (fromIntegral face))+            Nothing -> RefinementDomainTopologyChanged+    pure result    where-    dynamicPermitted :: MUV.MVector s Bool -> Int -> ST s Bool+    dynamicPermitted :: BoolMarks s -> Int -> ST s Bool     dynamicPermitted membership face =-      if face > 0 && face < MUV.length membership-        then MUV.unsafeRead membership face-        else pure False+      if face > 0 then readBoolMark membership face else pure False -  -- The area bound condemns outright ('FaceMustRefine'); the ratio bound only-  -- invites refinement, which the input-angle guard may still decline. The-  -- ratio is left for last: a face the area bound has already judged needs no-  -- second reason, and the ratio is the more expensive of the two questions.-  faceHint :: Point -> Point -> Point -> FaceHint-  faceHint p0 p1 p2-    | areaBad = FaceMustRefine-    | belowMinimum = FaceAcceptable-    | angleBad = FaceShouldRefine-    | otherwise = FaceAcceptable+  -- The edge bound is a hard geometric law and therefore precedes the minimum+  -- area short-circuit: a long, thin face must get a direct longest-edge split+  -- even when its area is below the configured minimum. The area bound then+  -- condemns outright ('FaceMustRefine'); the ratio bound only invites+  -- refinement, which the input-angle guard may still decline.+  faceHint :: Int -> Int -> Int -> Point -> Point -> Point -> FaceHint+  faceHint e0 e1 e2 p0 p1 p2 =+    case maximumSquaredEdgeLength of+      Just maximumSquaredLength ->+        let (!longestEdge, !longestSquaredLength, !side01, !side12, !side20) =+              faceEdgeMetrics e0 e1 e2 p0 p1 p2+         in if exceeds longestSquaredLength maximumSquaredLength+              then FaceMustSplitLongestEdge (longestEdge `quot` 2)+              else+                nonEdgeHint+                  ( ratioBad+                      ( triangleRadiusEdgeRatioSquaredWithAreaAndSides+                          area+                          side01+                          side12+                          side20+                      )+                  )+      Nothing ->+        nonEdgeHint+          (ratioBad (triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2))    where     !area = triangleArea p0 p1 p2     areaBad = maybe False (exceeds area) maximumArea     belowMinimum = maybe False (area <) minimumArea-    angleBad = case squaredRatioBound of+    ratioBad :: Double -> Bool+    ratioBad ratio = case squaredRatioBound of       Nothing -> False-      Just bound -> exceeds (triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2) bound+      Just bound -> exceeds ratio bound+    nonEdgeHint angleBad+      | areaBad = FaceMustRefine+      | belowMinimum = FaceAcceptable+      | angleBad = FaceShouldRefine+      | otherwise = FaceAcceptable    handleFace :: Workspace s -> Int -> ST s (Either BuildError Int)   handleFace workspace@Workspace{wsExcluded, wsPermitted, wsVisited} face = do@@ -466,13 +610,13 @@     permitted <-       maybe         (pure True)-        (\membership -> MUV.unsafeRead membership face)+        (\membership -> readBoolMark membership face)         wsPermitted     if not permitted       then pure (Left (RefinementDomainWouldRewriteProtectedFace (FaceId (fromIntegral face))))       else do-        traverse_ (\visited -> MUV.unsafeWrite visited face True) wsVisited-        skip <- MUV.unsafeRead wsExcluded face+        traverse_ (\visited -> writeBoolMark visited face True) wsVisited+        skip <- readBoolMark wsExcluded face         if skip           then pure (Right 0)           else do@@ -481,8 +625,9 @@             p0 <- edgeOriginPoint mutable e0             p1 <- edgeOriginPoint mutable e1             p2 <- edgeOriginPoint mutable e2-            case faceHint p0 p1 p2 of+            case faceHint e0 e1 e2 p0 p1 p2 of               FaceAcceptable -> pure (Right 0)+              FaceMustSplitLongestEdge pair -> qualitySplit workspace pair               FaceMustRefine -> attemptCandidate workspace face p0 p1 p2               FaceShouldRefine -> do                 blocked <- inputAngleBlocks workspace e0 e1 e2 p0 p1 p2@@ -490,6 +635,37 @@                   then pure (Right 0)                   else attemptCandidate workspace face p0 p1 p2 +  faceEdgeMetrics+    :: Int+    -> Int+    -> Int+    -> Point+    -> Point+    -> Point+    -> (Int, Double, Double, Double, Double)+  faceEdgeMetrics e0 e1 e2 p0 p1 p2+    | side01 >= side12 && side01 >= side20 = result e0 side01+    | side12 >= side20 = result e1 side12+    | otherwise = result e2 side20+   where+    !side01 = squaredDistanceWide p0 p1+    !side12 = squaredDistanceWide p1 p2+    !side20 = squaredDistanceWide p2 p0+    result :: Int -> Double -> (Int, Double, Double, Double, Double)+    result edge longest = (edge, longest, side01, side12, side20)++  -- An overlong edge on a local interface is a publish-time quality+  -- obstruction, not a permission to cross the frozen section. Let the+  -- completed-domain audit report that exact edge; a budget-limited run stays+  -- resumable and returns its ordinary incomplete result.+  qualitySplit :: Workspace s -> Int -> ST s (Either BuildError Int)+  qualitySplit workspace pair = do+    outcome <- resolveSplit SplitQualityEdge workspace pair+    case outcome of+      Left (RefinementDomainWouldCrossInterface _ _) -> pure (Right 0)+      Left failure -> pure (Left failure)+      Right split -> pure (Right (if split then 1 else 0))+   -- Two subsegments of one original segment meeting at a small input angle   -- cannot be refined apart: the angle is fixed by the input geometry. When   -- the shortest edge of a ratio-bound face joins two Steiner vertices whose@@ -514,12 +690,10 @@     !side12 = squaredDistanceWide p1 p2     !side20 = squaredDistanceWide p2 p0 -  sharedSegmentOrigin :: MUV.MVector s Word32 -> Int -> Int -> ST s Bool+  sharedSegmentOrigin :: SegmentOrigins s -> Int -> Int -> ST s Bool   sharedSegmentOrigin lineage a b = do-    a0 <- MUV.unsafeRead lineage (2 * a)-    a1 <- MUV.unsafeRead lineage (2 * a + 1)-    b0 <- MUV.unsafeRead lineage (2 * b)-    b1 <- MUV.unsafeRead lineage (2 * b + 1)+    (a0, a1) <- readSegmentOrigin lineage a+    (b0, b1) <- readSegmentOrigin lineage b     pure (a0 /= noIndex && b0 /= noIndex && (a0 == b0 || a0 == b1 || a1 == b0 || a1 == b1))    validCandidate :: Point -> Maybe (Point)@@ -532,8 +706,26 @@     case circumcenter p0 p1 p2 >>= validCandidate of       Nothing -> pure (Right 0)       Just point -> do-        located <- locateMutable mutable operation (Just face) point+        located <-+          case domain of+            Nothing -> locateMutable mutable operation (Just face) point+            Just refinementDomain ->+              locateMutableWithin+                (refinementDomainInterfacePairs refinementDomain)+                mutable+                operation+                (Just face)+                point         case located of+          -- The local locator has proved that the circumcenter lies beyond+          -- Γ without crossing it.  This is the same boundary-limited fixed+          -- point as a circumcenter outside a preserved hull: no admissible+          -- rewrite exists inside this section, so the face is discharged.+          -- Actual cavity demands involving Γ remain typed obstructions in+          -- 'exploreThenCommit'.+          Left (RefinementDomainWouldCrossInterface _ _) -> do+            modifySTRef' (wsInterfaceBoundaryReads workspace) (+ 1)+            pure (Right 0)           Left failure -> pure (Left failure)           Right site -> case site of             MutableOnVertex _ -> pure (Right 0)@@ -542,7 +734,12 @@               | refinePreserveConvexHull parameters -> pure (Right 0)               | otherwise -> attemptOutside workspace face point edge             MutableOnEdge edge -> do-              if isInterfacePair (edge `quot` 2)+              hull <- case domain of+                Nothing -> pure False+                Just _ -> isHullPair (edge `quot` 2)+              if hull+                then pure (Right 0)+                else if isInterfacePair (edge `quot` 2)                 then Left <$> interfaceCrossing workspace (edge `quot` 2)                 else do                   protected <- readConstraint mutable edge@@ -558,12 +755,12 @@               permittedSite <-                 maybe                   (pure True)-                  (\membership -> MUV.unsafeRead membership under)+                  (\membership -> readBoolMark membership under)                   (wsPermitted workspace)               if not permittedSite                 then pure (Left (RefinementDomainWouldRewriteProtectedFace (FaceId (fromIntegral under))))                 else do-                  excludedSite <- MUV.unsafeRead wsExcluded under+                  excludedSite <- readBoolMark wsExcluded under                   if excludedSite                     then pure (Right 0)                     else exploreThenCommit workspace face point site@@ -579,32 +776,106 @@   exploreThenCommit workspace@Workspace{wsQueue, wsForcedSplits} face point site = do     (visible, encroached) <- exploreCavity workspace point site     case find isInterfacePair encroached of-      Just pair -> Left <$> interfaceCrossing workspace pair-      Nothing ->-        if null encroached-          then commitCavity workspace point site visible-          else do-            pushable <- filterM splittablePair encroached-            forM_ pushable $ \pair -> pushGrowable wsForcedSplits (packIndex pair)-            unless (null pushable) $ pushFace wsQueue face-            pure (Right 0)+      Just pair+        | all isInterfacePair encroached -> pure (Right 0)+        | otherwise -> Left <$> interfaceCrossing workspace pair+      Nothing+        | null encroached -> commitCavity workspace point site visible+        | otherwise ->+            do+              pushable <- filterM splittablePair encroached+              traverse_ (\pair -> pushGrowable wsForcedSplits (packIndex pair)) pushable+              unless (null pushable) $ pushFace wsQueue face+              pure (Right 0)    splittablePair :: Int -> ST s Bool-  splittablePair pair =-    if isInterfacePair pair+  splittablePair pair = do+    hull <- case domain of+      Nothing -> pure False+      Just _ -> isHullPair pair+    if hull || isInterfacePair pair       then pure False       else if refineKeepConstraintEdges parameters-      then not <$> readConstraint mutable (2 * pair)-      else pure True+        then not <$> readConstraint mutable (2 * pair)+        else pure True    admitStar :: Workspace s -> Int -> ST s ()   admitStar Workspace{wsPermitted} vertex =+    case wsPermitted of+      Just membership ->+        forEachStarFace vertex $ \face -> rememberPermittedFace membership face+      Nothing -> pure ()++  rememberPermittedFace :: BoolMarks s -> Int -> ST s ()+  rememberPermittedFace membership face =+    if face <= 0+      then pure ()+      else do+        already <- readBoolMark membership face+        unless already $ do+          writeBoolMark membership face True++  retirePermittedFace :: Workspace s -> Int -> ST s ()+  retirePermittedFace Workspace{wsPermitted} face = do+    when (face >= 0) $ do+      traverse_+        (\membership -> writeBoolMark membership face False)+        wsPermitted++  retirePermittedFaces :: Workspace s -> GrowableWord32 s -> ST s ()+  retirePermittedFaces workspace faces = do+    count <- growableLength faces     traverse_-      (\membership ->-         forEachStarFace vertex (\face -> MUV.unsafeWrite membership face True)+      (\index -> do+         face <- fromIntegral <$> readGrowable faces index+         retirePermittedFace workspace face       )-      wsPermitted+      [0 .. count - 1] +  captureCavityFaces :: GrowableWord32 s -> ST s [(Int, Maybe [Point])]+  captureCavityFaces faces = do+    count <- growableLength faces+    traverse+      (\index -> do+         face <- fromIntegral <$> readGrowable faces index+         signature <- mutableFaceSignature face+         pure (face, signature)+      )+      [0 .. count - 1]++  reconcileCavityFaces :: Workspace s -> [(Int, Maybe [Point])] -> ST s ()+  reconcileCavityFaces workspace signatures =+    traverse_+      (\(face, before) -> do+         after <- mutableFaceSignature face+         when (after /= before) (retirePermittedFace workspace face)+      )+      signatures++  mutableFaceSignature :: Int -> ST s (Maybe [Point])+  mutableFaceSignature face = do+    faces <- faceCount mutable+    if face <= 0 || face >= faces+      then pure Nothing+      else do+        (e0, e1, e2) <- faceEdges mutable face+        points <- traverse (edgeOriginPoint mutable) [e0, e1, e2]+        pure (Just (List.sort points))++  captureIncidentFaceSignatures :: [Int] -> ST s [(Int, Maybe [Point])]+  captureIncidentFaceSignatures vertices = do+    faces <- newSTRef IntSet.empty+    traverse_+      (\vertex -> forEachStarFace vertex (modifySTRef' faces . IntSet.insert))+      vertices+    captured <- IntSet.toAscList <$> readSTRef faces+    traverse+      (\face -> do+         signature <- mutableFaceSignature face+         pure (face, signature)+      )+      captured+   -- The flood never mutates, so a popped edge can be retested idempotently:   -- an edge whose far face conflicts crosses, marks the face, and pushes that   -- face's other two edges; each directed edge has at most its two@@ -650,18 +921,18 @@    where     markFace :: Word32 -> Int -> ST s ()     markFace epoch face = do-      MUV.unsafeWrite wsCavityFaceMarks face epoch-      traverse_ (\visited -> MUV.unsafeWrite visited face True) wsVisited+      writeEpochMark wsCavityFaceMarks face epoch+      traverse_ (\visited -> writeBoolMark visited face True) wsVisited       pushGrowable wsCavityFaces (packIndex face)      isMarked :: Word32 -> Int -> ST s Bool-    isMarked epoch face = (== epoch) <$> MUV.unsafeRead wsCavityFaceMarks face+    isMarked epoch face = (== epoch) <$> readEpochMark wsCavityFaceMarks face      recordInternal :: Word32 -> Int -> ST s ()     recordInternal epoch pair = do-      seen <- MUV.unsafeRead wsCavityPairMarks pair+      seen <- readEpochMark wsCavityPairMarks pair       when (seen /= epoch) $ do-        MUV.unsafeWrite wsCavityPairMarks pair epoch+        writeEpochMark wsCavityPairMarks pair epoch         pushGrowable wsCavityInternal (packIndex pair)      -- The flood's stack traffic is transaction-local and owns no diagnostics;@@ -699,8 +970,14 @@                 from <- edgeOriginPoint mutable directed                 to <- edgeOriginPoint mutable (reverseIndex directed)                 let pair = directed `quot` 2-                    encroached = inDiametralCircle from to point-                when (isInterfacePair pair && not encroached) $+                hull <- case domain of+                  Nothing -> pure False+                  Just _ -> isHullPair pair+                let encroached =+                      if isInterfacePair pair || hull+                        then strictlyInDiametralCircle from to point+                        else inDiametralCircle from to point+                when (isInterfacePair pair) $                   modifySTRef' (wsInterfaceBoundaryReads workspace) (+ 1)                 if encroached                   then go (pair : acc) False@@ -734,6 +1011,10 @@                           pushGrowable wsCavityCocircular (packIndex directed)                         go acc (visible && orient2d from to point == GT) +  strictlyInDiametralCircle :: Point -> Point -> Point -> Bool+  strictlyInDiametralCircle (Point ax ay) (Point bx by) (Point px py) =+    exactDiametralDot ax ay bx by px py < 0+   -- The flood never mutates, so a popped edge can be retested idempotently:   -- an edge whose far face conflicts pushes that face's other two edges, and   -- each directed edge has at most its two face-neighbours as pushers.@@ -775,8 +1056,9 @@   -- commits through the ordinary split-and-legalize path instead; the   -- recorded cavity is simply abandoned.   commitFlipCandidate :: Workspace s -> Point -> MutableLocation -> ST s (Either BuildError Int)-  commitFlipCandidate workspace@Workspace{wsQueue, wsExcluded} point site = do+  commitFlipCandidate workspace@Workspace{wsQueue, wsExcluded, wsCavityFaces} point site = do     let !canonical = canonicalPoint point+    cavitySignatures <- captureCavityFaces wsCavityFaces     vertex <- appendVertex mutable canonical (makeVertex canonical)     splitSides <- case site of       MutableOnEdge edge@@ -785,7 +1067,7 @@             let !inner = if own == 0 then reverseIndex edge else edge             leftFace <- readFace mutable inner             rightFace <- readFace mutable (reverseIndex inner)-            leftExcluded <- MUV.unsafeRead wsExcluded leftFace+            leftExcluded <- readBoolMark wsExcluded leftFace             rightExcluded <- sideExcluded wsExcluded (reverseIndex inner)             faceBase <- faceCount mutable             pure (Just (leftFace, leftExcluded, rightFace, rightExcluded, faceBase))@@ -795,13 +1077,14 @@       Left failure -> pure (Left failure)       Right () -> do         addCounter operation CounterSteinerPoints 1+        reconcileCavityFaces workspace cavitySignatures         admitStar workspace vertex         case splitSides of           Just (leftFace, leftExcluded, rightFace, rightExcluded, faceBase) ->             inheritSplitExclusion wsExcluded leftFace leftExcluded rightFace rightExcluded faceBase           Nothing -> pure ()         forEachStarFace vertex $ \starFace -> do-          skip <- MUV.unsafeRead wsExcluded starFace+          skip <- readBoolMark wsExcluded starFace           unless skip $ pushFace wsQueue starFace         pure (Right 1) @@ -832,21 +1115,21 @@   chainBoundary :: Workspace s -> Int -> ST s Bool   chainBoundary Workspace{wsCavityEpoch, wsCavityFaceMarks, wsCavityVertexMarks, wsCavityBoundary, wsCavityChain} count = do     epoch <- readSTRef wsCavityEpoch-    budget <- directedEdgeCount mutable+    walkBudget <- directedEdgeCount mutable     start <- fromIntegral <$> readGrowable wsCavityBoundary 0     clearGrowable wsCavityChain     pushGrowable wsCavityChain (packIndex start)-    walk epoch (budget + 2) start start (count - 1)+    walk epoch (walkBudget + 2) start start (count - 1)    where     walk :: Word32 -> Int -> Int -> Int -> Int -> ST s Bool-    walk epoch budget start current remaining = do+    walk epoch walkBudget start current remaining = do       destination <- readOrigin mutable (reverseIndex current)-      seen <- MUV.unsafeRead wsCavityVertexMarks destination+      seen <- readEpochMark wsCavityVertexMarks destination       if seen == epoch         then pure False         else do-          MUV.unsafeWrite wsCavityVertexMarks destination epoch-          successor <- nextBoundaryEdge epoch budget current+          writeEpochMark wsCavityVertexMarks destination epoch+          successor <- nextBoundaryEdge epoch walkBudget current           if remaining == 0             then pure (successor == start)             else@@ -854,20 +1137,21 @@                 then pure False                 else do                   pushGrowable wsCavityChain (packIndex successor)-                  walk epoch budget start successor (remaining - 1)+                  walk epoch walkBudget start successor (remaining - 1)      nextBoundaryEdge :: Word32 -> Int -> Int -> ST s Int-    nextBoundaryEdge epoch budget edge = do+    nextBoundaryEdge epoch walkBudget edge = do       first <- readNext mutable edge-      rotate first budget+      rotate first walkBudget      where+      rotate :: Int -> Int -> ST s Int       rotate candidate !remaining         | remaining <= 0 = pure candidate         | otherwise = do             adjacent <- readFace mutable candidate-            markedHere <- (== epoch) <$> MUV.unsafeRead wsCavityFaceMarks adjacent+            markedHere <- (== epoch) <$> readEpochMark wsCavityFaceMarks adjacent             across <- readFace mutable (reverseIndex candidate)-            markedAcross <- (== epoch) <$> MUV.unsafeRead wsCavityFaceMarks across+            markedAcross <- (== epoch) <$> readEpochMark wsCavityFaceMarks across             if markedHere && not markedAcross               then pure candidate               else do@@ -881,8 +1165,10 @@       Left obstruction -> pure (Left obstruction)       Right () -> fanCommitWithCapacity    where+    fanCommitWithCapacity :: ST s (Either BuildError Int)     fanCommitWithCapacity = do       let !canonical = canonicalPoint point+      retirePermittedFaces workspace wsCavityFaces       vertex <- appendVertex mutable canonical (makeVertex canonical)       faceBase <- addFaceBlock mutable (boundaryCount - cavityFaceCount)       edgeBase <- addEdgeBlock mutable (boundaryCount - internalCount)@@ -922,12 +1208,13 @@       when (cocircular > 0) $ do         ties <- traverse (\index -> fromIntegral <$> readGrowable wsCavityCocircular index) [0 .. cocircular - 1]         legalizeEdges mutable operation ties+      admitStar workspace vertex       let offer :: Int -> ST s ()           offer !index             | index >= boundaryCount = pure ()             | otherwise = do                 face <- fanFace faceBase index-                skip <- MUV.unsafeRead wsExcluded face+                skip <- readBoolMark wsExcluded face                 unless skip $ pushFace wsQueue face                 offer (index + 1)       offer 0@@ -979,20 +1266,18 @@             when (refineExcludeOuterFaces parameters) $ do               seedCount <- newSTRef 0               forEachStarFace vertex $ \starFace -> do-                MUV.unsafeWrite wsExcluded starFace True+                writeBoolMark wsExcluded starFace True                 count <- readSTRef seedCount                 writeScratch operation count starFace                 writeSTRef seedCount (count + 1)               count <- readSTRef seedCount               propagateExcluded wsExcluded count             forEachStarFace vertex $ \starFace -> do-              skip <- MUV.unsafeRead wsExcluded starFace+              skip <- readBoolMark wsExcluded starFace               unless skip $ pushFace wsQueue starFace             forEachStarFace vertex $ \starFace -> do               (e0, e1, e2) <- faceEdges mutable starFace-              forM_ [e0, e1, e2] $ \directed -> do-                fixed <- isFixedEdge directed-                when fixed $ pushEncroachment workspace (directed `quot` 2)+              traverse_ (offerFixedEdge workspace) [e0, e1, e2]             pure (Right 1)    -- The maximal run of outer-cycle edges visible from the candidate, centred@@ -1005,8 +1290,8 @@     walk left right (halfEdges + 1) []    where     expand :: (Int -> ST s Int) -> Int -> Int -> ST s Int-    expand step !budget !current-      | budget <= 0 = pure current+    expand step !walkBudget !current+      | walkBudget <= 0 = pure current       | otherwise = do           candidate <- step current           if candidate == edge@@ -1015,20 +1300,20 @@               from <- edgeOriginPoint mutable candidate               to <- edgeOriginPoint mutable (reverseIndex candidate)               if orient2d from to point == GT-                then expand step (budget - 1) candidate+                then expand step (walkBudget - 1) candidate                 else pure current      walk :: Int -> Int -> Int -> [Int] -> ST s [Int]-    walk !current !end !budget !acc-      | budget <= 0 = pure acc+    walk !current !end !walkBudget !acc+      | walkBudget <= 0 = pure acc       | current == end = pure (current : acc)       | otherwise = do           following <- readNext mutable current-          walk following end (budget - 1) (current : acc)+          walk following end (walkBudget - 1) (current : acc)    -- New faces outside the hull sit at barrier depth zero; whatever they now   -- reach without crossing a constraint joined the outer region with them.-  propagateExcluded :: MUV.MVector s Bool -> Int -> ST s ()+  propagateExcluded :: BoolMarks s -> Int -> ST s ()   propagateExcluded excluded = drain    where     drain :: Int -> ST s ()@@ -1050,11 +1335,11 @@           if adjacent == 0             then pure count             else do-              already <- MUV.unsafeRead excluded adjacent+              already <- readBoolMark excluded adjacent               if already                 then pure count                 else do-                  MUV.unsafeWrite excluded adjacent True+                  writeBoolMark excluded adjacent True                   writeScratch operation count adjacent                   pure (count + 1) @@ -1073,16 +1358,16 @@       then         if isInterfacePair pair           then Left <$> interfaceCrossing workspace pair-          else resolveSplit workspace pair+          else resolveSplit SplitFixedSegment workspace pair       else pure (Right False)    where-    sideEncroaches :: MUV.MVector s Bool -> Int -> ST s Bool+    sideEncroaches :: BoolMarks s -> Int -> ST s Bool     sideEncroaches excluded side = do       adjacent <- readFace mutable side       if adjacent == 0         then pure False         else do-          skip <- MUV.unsafeRead excluded adjacent+          skip <- readBoolMark excluded adjacent           if skip             then pure False             else do@@ -1090,17 +1375,29 @@               query <- pointAt mutable opposite               from <- edgeOriginPoint mutable side               to <- edgeOriginPoint mutable (reverseIndex side)-              pure (inDiametralCircle from to query)+              hull <- case domain of+                Nothing -> pure False+                Just _ -> isHullPair pair+              pure+                ( if isInterfacePair pair || hull+                    then strictlyInDiametralCircle from to query+                    else inDiametralCircle from to query+                )    -- Split a fixed edge. A first split lands at the midpoint; a subsegment   -- split rounds its offset to the nearest power of two toward the original   -- endpoint, so segments meeting at a small input angle stop encroaching   -- each other instead of subdividing forever.-  resolveSplit :: Workspace s -> Int -> ST s (Either BuildError Bool)-  resolveSplit workspace@Workspace{wsExcluded, wsSegmentOrigin} pair = do+  resolveSplit :: EdgeSplitLaw -> Workspace s -> Int -> ST s (Either BuildError Bool)+  resolveSplit splitLaw workspace@Workspace{wsExcluded, wsSegmentOrigin} pair = do     let !directed = 2 * pair     protected <- readConstraint mutable directed-    if isInterfacePair pair+    hull <- case domain of+      Nothing -> pure False+      Just _ -> isHullPair pair+    if hull+      then pure (Right False)+      else if isInterfacePair pair       then Left <$> interfaceCrossing workspace pair       else if refineKeepConstraintEdges parameters && protected       then pure (Right False)@@ -1109,11 +1406,20 @@         v1 <- readOrigin mutable (reverseIndex directed)         from <- pointAt mutable v0         to <- pointAt mutable v1-        lineage0 <- MUV.unsafeRead wsSegmentOrigin (2 * v0)-        lineage1 <- MUV.unsafeRead wsSegmentOrigin (2 * v1)-        let !onSegment0 = lineage0 /= noIndex-            !onSegment1 = lineage1 /= noIndex+        segmentLineage <-+          case splitLaw of+            SplitQualityEdge -> pure Nothing+            SplitFixedSegment -> do+              (lineage0, second0) <- readSegmentOrigin wsSegmentOrigin v0+              (lineage1, second1) <- readSegmentOrigin wsSegmentOrigin v1+              pure (Just (lineage0, second0, lineage1, second1))+        let (!onSegment0, !onSegment1) =+              case segmentLineage of+                Nothing -> (False, False)+                Just (lineage0, _, lineage1, _) ->+                  (lineage0 /= noIndex, lineage1 /= noIndex)             !splitPoint = splitPosition onSegment0 onSegment1 from to+        incidentSignatures <- captureIncidentFaceSignatures [v0, v1]         valid <- validateSplitPosition directed splitPoint         if not valid           then pure (Right False)@@ -1127,26 +1433,28 @@             let !inner = if own == 0 then reverseIndex directed else directed             leftFace <- readFace mutable inner             rightFace <- readFace mutable (reverseIndex inner)-            leftExcluded <- MUV.unsafeRead wsExcluded leftFace+            leftExcluded <- readBoolMark wsExcluded leftFace             rightExcluded <- sideExcluded wsExcluded (reverseIndex inner)             faceBase <- faceCount mutable             let !canonical = canonicalPoint splitPoint+            retirePermittedFace workspace leftFace+            retirePermittedFace workspace rightFace             vertex <- appendVertex mutable canonical (makeVertex canonical)             inserted <- insertOnEdge @'ProbeOff mutable operation directed vertex             case inserted of               Left obstruction -> pure (Left obstruction)               Right () -> do+                reconcileCavityFaces workspace incidentSignatures                 admitStar workspace vertex-                if onSegment0-                  then do-                    second <- MUV.unsafeRead wsSegmentOrigin (2 * v0 + 1)-                    writeSegmentOrigin wsSegmentOrigin vertex lineage0 second-                  else-                    if onSegment1-                      then do-                        second <- MUV.unsafeRead wsSegmentOrigin (2 * v1 + 1)-                        writeSegmentOrigin wsSegmentOrigin vertex lineage1 second-                      else writeSegmentOrigin wsSegmentOrigin vertex (packIndex v0) (packIndex v1)+                case segmentLineage of+                  Nothing -> pure ()+                  Just (lineage0, second0, lineage1, second1) ->+                    if onSegment0+                      then writeSegmentOrigin wsSegmentOrigin vertex lineage0 second0+                      else+                        if onSegment1+                          then writeSegmentOrigin wsSegmentOrigin vertex lineage1 second1+                          else writeSegmentOrigin wsSegmentOrigin vertex (packIndex v0) (packIndex v1)                 when (refineExcludeOuterFaces parameters) $                   inheritSplitExclusion wsExcluded leftFace leftExcluded rightFace rightExcluded faceBase                 addCounter operation CounterSteinerPoints 1@@ -1200,16 +1508,16 @@                 && orient2d oppositePoint sideTo splitPoint == LT             ) -  sideExcluded :: MUV.MVector s Bool -> Int -> ST s Bool+  sideExcluded :: BoolMarks s -> Int -> ST s Bool   sideExcluded excluded side = do     adjacent <- readFace mutable side-    if adjacent == 0 then pure True else MUV.unsafeRead excluded adjacent+    if adjacent == 0 then pure True else readBoolMark excluded adjacent    -- A split keeps each side's classification. The faces a side holds   -- afterwards are the side face it kept and the face appended over it:   -- interior splits append two, boundary splits one, and both ids are   -- immune to the legalization that runs between the geometry and here.-  inheritSplitExclusion :: MUV.MVector s Bool -> Int -> Bool -> Int -> Bool -> Int -> ST s ()+  inheritSplitExclusion :: BoolMarks s -> Int -> Bool -> Int -> Bool -> Int -> ST s ()   inheritSplitExclusion excluded leftFace leftExcluded rightFace rightExcluded faceBase = do     writeSide leftExcluded leftFace     writeSide leftExcluded faceBase@@ -1219,21 +1527,14 @@    where     writeSide :: Bool -> Int -> ST s ()     writeSide status side = do-      when (side /= 0) $ MUV.unsafeWrite excluded side status--  writeSegmentOrigin :: MUV.MVector s Word32 -> Int -> Word32 -> Word32 -> ST s ()-  writeSegmentOrigin lineage vertex first second = do-    MUV.unsafeWrite lineage (2 * vertex) first-    MUV.unsafeWrite lineage (2 * vertex + 1) second+      when (side /= 0) $ writeBoolMark excluded side status    pushStarAfterSplit :: Workspace s -> Int -> ST s ()   pushStarAfterSplit workspace@Workspace{wsQueue} vertex =     forEachStarFace vertex $ \starFace -> do       pushFace wsQueue starFace       (e0, e1, e2) <- faceEdges mutable starFace-      forM_ [e0, e1, e2] $ \directed -> do-        fixed <- isFixedEdge directed-        when fixed $ pushEncroachment workspace (directed `quot` 2)+      traverse_ (offerFixedEdge workspace) [e0, e1, e2]    -- Every interior face incident to a vertex, once. One rotation direction   -- covers a full interior star; a hull star additionally needs the other
src-build/Moonlight/Triangulation/Internal/Transaction.hs view
@@ -10,6 +10,7 @@ -- forgeable. module Moonlight.Triangulation.Internal.Transaction   ( runTransaction+  , runTransactionWithPublication   , runUnmeasuredTransaction   ) where @@ -19,6 +20,7 @@ import Moonlight.Triangulation.Internal.Mutable   ( MutableDcel   , freezeTriangulation+  , freezeTriangulationWithStats   , halfEdgeCapacity   , thawTriangulation   , thawTriangulationDense@@ -28,7 +30,11 @@   , freezeBuildStats   , newOperationState   )-import Moonlight.Triangulation.Internal.Paged (TransactionShape (..))+import Moonlight.Triangulation.Internal.Paged+  ( PublicationStats+  , TransactionShape (..)+  , emptyPublicationStats+  ) import Moonlight.Triangulation.Internal.Representation (Triangulation) import Moonlight.Triangulation.Internal.Types (BuildError, BuildStats) @@ -47,6 +53,19 @@ runTransaction = runTransactionWithReceipt freezeBuildStats {-# INLINE runTransaction #-} +-- | The measured publication boundary.  The storage owner returns its page+-- counters alongside the immutable mesh; callers must not infer locality from+-- a final page count or from a benchmark timer.+runTransactionWithPublication+  :: (BuildError -> failure)+  -> TransactionShape+  -> Triangulation mode vertex directed undirected face+  -> Int+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))+  -> Either failure (result, Triangulation mode vertex directed undirected face, BuildStats, PublicationStats)+runTransactionWithPublication = runTransactionWithPublicationReceipt freezeBuildStats+{-# INLINE runTransactionWithPublication #-}+ -- | Publish a transaction whose caller observes no instrumentation. Avoiding -- the statistics fold matters for singleton constraint verbs: their public -- result has no statistics field, so reading every counter would be dead work.@@ -79,6 +98,33 @@   -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))   -> Either failure (result, Triangulation mode vertex directed undirected face, receipt) runTransactionWithReceipt freezeReceipt mapBuildFailure shape triangulation additional action = do+  fmap+    (\(value, frozen, receipt, _publicationStats) -> (value, frozen, receipt))+    (runTransactionCore freezeWithoutPublicationStats freezeReceipt mapBuildFailure shape triangulation additional action)+{-# INLINE runTransactionWithReceipt #-}++runTransactionWithPublicationReceipt+  :: (forall s. OperationState s -> ST s receipt)+  -> (BuildError -> failure)+  -> TransactionShape+  -> Triangulation mode vertex directed undirected face+  -> Int+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))+  -> Either failure (result, Triangulation mode vertex directed undirected face, receipt, PublicationStats)+runTransactionWithPublicationReceipt freezeReceipt mapBuildFailure shape triangulation additional action = do+  runTransactionCore freezeTriangulationWithStats freezeReceipt mapBuildFailure shape triangulation additional action+{-# INLINE runTransactionWithPublicationReceipt #-}++runTransactionCore+  :: (forall s. MutableDcel s vertex directed undirected face -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats)))+  -> (forall s. OperationState s -> ST s receipt)+  -> (BuildError -> failure)+  -> TransactionShape+  -> Triangulation mode vertex directed undirected face+  -> Int+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))+  -> Either failure (result, Triangulation mode vertex directed undirected face, receipt, PublicationStats)+runTransactionCore freezePublication freezeReceipt mapBuildFailure shape triangulation additional action = do   let !currentCapacity = numVertices triangulation       !requestedAdditional = max 0 additional       !capacity@@ -97,10 +143,16 @@     case outcome of       Left refusal -> pure (Left refusal)       Right value -> do-        frozenOutcome <- freezeTriangulation mutable+        frozenOutcome <- freezePublication mutable         case frozenOutcome of           Left obstruction -> pure (Left (mapBuildFailure obstruction))-          Right frozen -> do+          Right (frozen, publicationStats) -> do             receipt <- freezeReceipt operation-            pure (Right (value, frozen, receipt))-{-# INLINE runTransactionWithReceipt #-}+            pure (Right (value, frozen, receipt, publicationStats))+{-# INLINE runTransactionCore #-}++freezeWithoutPublicationStats+  :: MutableDcel s vertex directed undirected face+  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))+freezeWithoutPublicationStats mutable =+  fmap (fmap (\frozen -> (frozen, emptyPublicationStats))) (freezeTriangulation mutable)
src-build/Moonlight/Triangulation/Refinement.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE ScopedTypeVariables #-}  -- | Ruppert refinement: Steiner insertion composed after a built mesh, never a -- second kind of mesh. Parameters are reached through checked verbs rather than@@ -11,16 +12,17 @@   , radiusEdgeRatioForAngle   ) where +import Control.Monad (void) import Control.Monad.ST (ST) import Data.Foldable (traverse_) import qualified Data.IntSet as IntSet import Data.List (sort)-import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) import qualified Data.Set as Set import qualified Data.Vector as V import Moonlight.Triangulation.Dcel-  ( faceVertices+  ( faceDirectedEdges+  , faceVertices   , incidentFace   , isConstraintEdge   , numFaces@@ -32,21 +34,35 @@ import Moonlight.Triangulation.FloodFillIterator (facesAtEvenBarrierDepth) import Moonlight.Triangulation.Handles.HandleDefs   ( FaceId (..)+  , DirectedEdgeId   , UndirectedEdgeId (..)+  , asUndirected   , directedPair+  , reverseEdge   )+import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces) import Moonlight.Triangulation.Internal.Mutable-import Moonlight.Triangulation.Internal.Paged (TransactionShape (DenseTransaction))+import Moonlight.Triangulation.Internal.Paged+  ( PublicationStats+  , TransactionShape (DenseTransaction, LocalTransaction)+  ) import Moonlight.Triangulation.Internal.Refinement-import Moonlight.Triangulation.Internal.Transaction (runTransaction)+import Moonlight.Triangulation.Internal.Representation (Triangulation (..))+import Moonlight.Triangulation.Internal.Transaction (runTransactionWithPublication)+import Moonlight.Triangulation.Math (squaredDistanceWide) import Moonlight.Triangulation.Types-import Moonlight.Triangulation.Validation (validateTopology)+import Moonlight.Triangulation.Validation+  ( validateTopology+  , validateTopologyClosureWithStats+  )  data RefinementExecution mode vertex directed undirected face = RefinementExecution   { refinementExecutionResult :: !(RefinementResult mode vertex directed undirected face)   , refinementExecutionVisitedFaces :: ![Int]+  , refinementExecutionPermittedFaces :: ![Int]   , refinementExecutionInterfaceBoundaryReads :: {-# UNPACK #-} !Int   , refinementExecutionBoundaryCrossingAttempts :: {-# UNPACK #-} !Int+  , refinementExecutionPublicationStats :: !PublicationStats   }  -- | The radius-edge ratio admitting a minimum angle, in degrees.@@ -106,17 +122,11 @@             RefinementDomain               { refinementDomainPermittedFaces = permitted               , refinementDomainInterfacePairs = interface-              , refinementDomainInputFaces =-                  Map.fromAscList-                    [ (face, faceSignature triangulation face)-                    | raw <- [1 .. numFaces triangulation - 1]-                    , let face = FaceId (fromIntegral raw)-                    ]               , refinementDomainInputFaceCount = numFaces triangulation-              , refinementDomainInputEdgeCount = numUndirectedEdges triangulation               }  where   totalEdges = numUndirectedEdges triangulation+  validateInterfaceEdge :: UndirectedEdgeId -> Either BuildError Int   validateInterfaceEdge edge@(UndirectedEdgeId raw)     | toInteger raw >= toInteger totalEdges =         Left (RefinementDomainInterfaceEdgeNotActive edge totalEdges)@@ -125,23 +135,23 @@   expectedInterface permitted =     IntSet.fromList       [ pair-      | pair <- [0 .. totalEdges - 1]-      , let edge = UndirectedEdgeId (fromIntegral pair)-            (forward, backward) = directedPair edge-            FaceId forwardFace = incidentFace triangulation forward-            FaceId backwardFace = incidentFace triangulation backward-            forwardInner = forwardFace /= 0-            backwardInner = backwardFace /= 0-            forwardPermitted = IntSet.member (fromIntegral forwardFace) permitted-            backwardPermitted = IntSet.member (fromIntegral backwardFace) permitted-      , forwardInner && backwardInner && forwardPermitted /= backwardPermitted+      | rawFace <- IntSet.toAscList permitted+      , edge <- faceDirectedEdges triangulation (FaceId (fromIntegral rawFace))+      , let pair = fromIntegral (unUndirected (asUndirected edge))+      , let adjacent = incidentFace triangulation (reverseEdge edge)+      , adjacent /= FaceId 0+      , IntSet.notMember (fromIntegral (unFace adjacent)) permitted       ] +  unUndirected (UndirectedEdgeId raw) = raw+  unFace (FaceId raw) = raw+ -- | Refine exactly one checked local section. Interface edges are installed as -- transaction-local legalization barriers and removed before publication;--- every protected face signature is then compared with the input witness.+-- the receipt and collar proof inspect only the admitted local closure. refineWithinDomain-  :: (Point -> vertex)+  :: forall mode vertex directed undirected face.+     (Point -> vertex)   -> RefinementParameters   -> Set.Set FaceId   -> Set.Set UndirectedEdgeId@@ -158,21 +168,39 @@       parameters       triangulation   let result = refinementExecutionResult execution-      receipt =+  validationStats <- validateDomainClosure+    domain+    triangulation+    (refinedTriangulation result)+    (refinementExecutionPermittedFaces execution)+  let receipt =         buildRefinementReceipt           domain           triangulation           (refinedTriangulation result)           (refinementExecutionVisitedFaces execution)+          (refinementExecutionPermittedFaces execution)           (refinementExecutionInterfaceBoundaryReads execution)           (refinementExecutionBoundaryCrossingAttempts execution)-  validateProtectedFaces domain triangulation (refinedTriangulation result)+          (refinementExecutionPublicationStats execution)+          validationStats   case V.toList (refinementVisitedProtectedFaces receipt) of     protected : _ -> Left (RefinementDomainWouldRewriteProtectedFace protected)     [] ->-      Right+       let restoredResult :: RefinementResult mode vertex directed undirected face+           restoredResult =+            result+              { refinedTriangulation =+                  (refinedTriangulation result)+                    -- The checked local interpreter rejects every hull-pair+                    -- split, so the cached outer cycle remains an exact+                    -- handle witness and can be restored without a scan.+                    { triSeamFrontier = triSeamFrontier triangulation+                    }+              }+       in Right         RefinementDomainResult-          { refinementDomainResult = result+          { refinementDomainResult = restoredResult           , refinementDomainReceipt = receipt           } @@ -184,6 +212,8 @@       Left RefinementDomainRequiresConstraintPreservation   | refineExcludeOuterFaces parameters =       Left RefinementDomainForbidsOuterFaceExclusion+  | refineMaxAdditionalVertices parameters == Nothing =+      Left RefinementDomainRequiresFiniteVertexBudget   | otherwise = Right ()  refineWithInitialSeed@@ -195,13 +225,24 @@   -> Either BuildError (RefinementExecution mode vertex directed undirected face) refineWithInitialSeed initialSeed domain makeVertex parameters triangulation = do   validateRefinementParameters parameters-  case validateTopology triangulation of-    violation : _ -> Left (RefinementInputTopologyInvalid violation)-    [] -> pure ()+  case domain of+    Just _+      | refineMaxAdditionalVertices parameters == Nothing ->+          Left RefinementDomainRequiresFiniteVertexBudget+    _ -> Right ()+  case domain of+    Just _ -> pure ()+    Nothing ->+      case validateTopology triangulation of+        violation : _ -> Left (RefinementInputTopologyInvalid violation)+        [] -> pure ()   let originalCount = numVertices triangulation-      budget = max 0 (fromMaybe (10 * max 1 originalCount) (refineMaxAdditionalVertices parameters))+      budget =+        case domain of+          Just _ -> max 0 (fromMaybe 0 (refineMaxAdditionalVertices parameters))+          Nothing -> max 0 (fromMaybe (10 * max 1 originalCount) (refineMaxAdditionalVertices parameters))       initialExcludedFaces =-        if refineExcludeOuterFaces parameters+        if domain == Nothing && refineExcludeOuterFaces parameters           then             IntSet.fromList               [ fromIntegral raw@@ -214,8 +255,9 @@   ( outcome     , frozen     , stats+    , publicationStats     ) <--    runTransaction id DenseTransaction triangulation budget $ \mutable operation -> do+    runTransactionWithPublication id (transactionShape domain) triangulation budget $ \mutable operation -> do       installInterfaceBarriers mutable domain       refinement <-         refineMutable@@ -223,13 +265,25 @@           mutable           operation           parameters-          originalCount+          budget           initialExcludedFaces           initialSeed           domain       removeInterfaceBarriers mutable triangulation domain       pure refinement-  let (complete, added, excluded, visited, interfaceBoundaryReads, boundaryCrossingAttempts) = outcome+  let (complete, added, excluded, visited, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts) = outcome+      excludedFaceSet = IntSet.fromList excluded+      finalAuditFaces =+        case domain of+          Nothing ->+            [ face+            | face@(FaceId raw) <- innerFaces frozen+            , IntSet.notMember (fromIntegral raw) excludedFaceSet+            ]+          Just _ -> fmap (FaceId . fromIntegral) permittedFaces+  if complete+    then validateMaximumEdgeLength parameters frozen finalAuditFaces+    else Right ()   pure     RefinementExecution       { refinementExecutionResult =@@ -241,17 +295,22 @@             , refinementExcludedFaces = V.fromList (map (FaceId . fromIntegral) excluded)             }       , refinementExecutionVisitedFaces = visited+      , refinementExecutionPermittedFaces = permittedFaces       , refinementExecutionInterfaceBoundaryReads = interfaceBoundaryReads       , refinementExecutionBoundaryCrossingAttempts = boundaryCrossingAttempts+      , refinementExecutionPublicationStats = publicationStats       } +transactionShape :: Maybe RefinementDomain -> TransactionShape+transactionShape = maybe DenseTransaction (const LocalTransaction)+ installInterfaceBarriers   :: MutableDcel s vertex directed undirected face   -> Maybe RefinementDomain   -> ST s () installInterfaceBarriers mutable =   traverse_-    (\pair -> setConstraint mutable (2 * pair) >> pure ())+    (\pair -> void (setConstraint mutable (2 * pair)))     . maybe [] (IntSet.toAscList . refinementDomainInterfacePairs)  removeInterfaceBarriers@@ -265,105 +324,219 @@        let edge = UndirectedEdgeId (fromIntegral pair)         in if isConstraintEdge input edge              then pure ()-             else clearConstraint mutable (2 * pair) >> pure ()+             else void (clearConstraint mutable (2 * pair))     )     . maybe [] (IntSet.toAscList . refinementDomainInterfacePairs) -validateProtectedFaces-  :: RefinementDomain-  -> Triangulation mode vertex directed undirected face-  -> Triangulation mode vertex directed undirected face-  -> Either BuildError ()-validateProtectedFaces domain before after =-  traverse_ validateProtected protectedFaces- where-  protectedFaces =-    [ FaceId (fromIntegral face)-    | face <- [1 .. refinementDomainInputFaceCount domain - 1]-    , IntSet.notMember face (refinementDomainPermittedFaces domain)-    ]-  validateProtected face-    | Map.lookup face (refinementDomainInputFaces domain)-        == Just (faceSignature before face)-        && Map.lookup face (refinementDomainInputFaces domain)-          == Just (faceSignature after face) = Right ()-    | otherwise = Left (RefinementDomainProtectedFaceChanged face)--faceSignature-  :: Triangulation mode vertex directed undirected face-  -> FaceId-  -> [Point]-faceSignature triangulation =-  sort . fmap (vertexPoint triangulation) . faceVertices triangulation--type EdgeSignature = (Point, Point, [FaceId])--edgeSignature-  :: Triangulation mode vertex directed undirected face-  -> UndirectedEdgeId-  -> EdgeSignature-edgeSignature triangulation edge =-  (min fromPoint toPoint, max fromPoint toPoint, sort [forwardFace, backwardFace])- where-  (fromVertex, toVertex) = undirectedEndpoints triangulation edge-  fromPoint = vertexPoint triangulation fromVertex-  toPoint = vertexPoint triangulation toVertex-  (forward, backward) = directedPair edge-  forwardFace = incidentFace triangulation forward-  backwardFace = incidentFace triangulation backward- buildRefinementReceipt   :: RefinementDomain   -> Triangulation mode vertex directed undirected face   -> Triangulation mode vertex directed undirected face   -> [Int]+  -> [Int]   -> Int   -> Int+  -> PublicationStats+  -> ValidationClosureStats   -> RefinementReceipt-buildRefinementReceipt domain before after visited interfaceBoundaryReads boundaryCrossingAttempts =+buildRefinementReceipt domain before after visited permittedFaces interfaceBoundaryReads boundaryCrossingAttempts publicationStats validationStats =   RefinementReceipt     { refinementVisitedJoinFaces = V.fromList (fmap toFace visitedJoin)     , refinementVisitedProtectedFaces = V.fromList (fmap toFace visitedProtected)-    , refinementCreatedFaces =-        V.fromList-          [ targetFace-          | face <- [1 .. numFaces after - 1]-          , let targetFace = toFace face-          , face >= numFaces before-              || faceSignature before targetFace /= faceSignature after targetFace-          ]-    , refinementTouchedEdges = V.fromList touched-    , refinementRemovedEdges = V.fromList removed+    , refinementCreatedFaces = V.fromList (fmap toFace createdFaces)+    , refinementFinalPermittedFaces = V.fromList (fmap toFace permittedFaces)+    , refinementTouchedEdges = V.fromList (fmap toEdge touchedEdges)+    , refinementRemovedEdges = V.fromList (fmap toEdge removedEdges)     , refinementInterfaceBoundaryReads = interfaceBoundaryReads     , refinementAttemptedBoundaryCrossings = boundaryCrossingAttempts+    , refinementPublicationStats = publicationStats+    , refinementValidationClosureStats = validationStats     }  where   permitted = refinementDomainPermittedFaces domain   (visitedProtected, visitedJoin) =     foldr       (\face (protected, join) ->-         if IntSet.member face permitted || face >= refinementDomainInputFaceCount domain+         if IntSet.member face permitted            then (protected, face : join)-           else (face : protected, join)-      )-      ([], [])-      visited-  toFace = FaceId . fromIntegral-  beforeEdges = numUndirectedEdges before-  afterEdges = numUndirectedEdges after-  touched =+           else if face >= refinementDomainInputFaceCount domain+             then (protected, face : join)+           else (face : protected, join))+      ([], []) visited+  initialFaces = refinementDomainPermittedFaces domain+  finalFaces = IntSet.fromList permittedFaces+  faceClosure = IntSet.toAscList (IntSet.union initialFaces finalFaces)+  createdFaces =+    [ face+    | face <- faceClosure+    , localFaceSignature before face /= localFaceSignature after face+    ]+  initialEdges = localDomainEdges before initialFaces+  finalEdges = localDomainEdges after finalFaces+  edgeClosure = IntSet.toAscList (IntSet.union initialEdges finalEdges)+  touchedEdges =     [ edge-    | raw <- [0 .. afterEdges - 1]-    , let edge = UndirectedEdgeId (fromIntegral raw)-    , raw >= beforeEdges || edgeSignature before edge /= edgeSignature after edge+    | edge <- edgeClosure+    , localEdgeSignature before edge /= localEdgeSignature after edge     ]-  removed =+  removedEdges =     [ edge-    | raw <- [0 .. beforeEdges - 1]-    , let edge = UndirectedEdgeId (fromIntegral raw)-    , raw >= afterEdges || edgeSignature before edge /= edgeSignature after edge+    | edge <- IntSet.toAscList initialEdges+    , localEdgeSignature before edge /= localEdgeSignature after edge     ]+  toFace = FaceId . fromIntegral+  toEdge = UndirectedEdgeId . fromIntegral +localFaceSignature+  :: Triangulation mode vertex directed undirected face+  -> Int+  -> Maybe [Point]+localFaceSignature triangulation raw+  | raw <= 0 || raw >= numFaces triangulation = Nothing+  | otherwise =+      Just+        ( sort+            (fmap (vertexPoint triangulation) (faceVertices triangulation (FaceId (fromIntegral raw))))+        )++localDomainEdges+  :: Triangulation mode vertex directed undirected face+  -> IntSet.IntSet+  -> IntSet.IntSet+localDomainEdges triangulation faces =+  IntSet.fromList+    [ fromIntegral (unUndirected (asUndirected edge))+    | rawFace <- IntSet.toAscList faces+    , edge <- faceDirectedEdges triangulation (FaceId (fromIntegral rawFace))+    ]+ where+  unUndirected (UndirectedEdgeId raw) = raw++localEdgeSignature+  :: Triangulation mode vertex directed undirected face+  -> Int+  -> Maybe (Point, Point, [FaceId])+localEdgeSignature triangulation raw+  | raw < 0 || raw >= numUndirectedEdges triangulation = Nothing+  | otherwise =+      let edge = UndirectedEdgeId (fromIntegral raw)+          (fromVertex, toVertex) = undirectedEndpoints triangulation edge+          fromPoint = vertexPoint triangulation fromVertex+          toPoint = vertexPoint triangulation toVertex+          (forward, backward) = directedPair edge+       in Just+            ( min fromPoint toPoint+            , max fromPoint toPoint+            , sort+                [ incidentFace triangulation forward+                , incidentFace triangulation backward+                ]+            )++validateDomainClosure+  :: RefinementDomain+  -> Triangulation mode vertex directed undirected face+  -> Triangulation mode vertex directed undirected face+  -> [Int]+  -> Either BuildError ValidationClosureStats+validateDomainClosure domain before after finalPermittedFaces = do+  let (closureStats, closureViolations) =+        validateTopologyClosureWithStats+          ( IntSet.union+              (refinementDomainPermittedFaces domain)+              (IntSet.fromList finalPermittedFaces)+          )+          (refinementDomainInterfacePairs domain)+          after+  case closureViolations of+    violation : _ -> Left (RefinementInputTopologyInvalid violation)+    [] -> pure ()+  traverse_ validateProtectedFace (IntSet.toAscList protectedFaces)+  traverse_ validateInterfaceEdge (IntSet.toAscList (refinementDomainInterfacePairs domain))+  pure closureStats+ where+  permitted = refinementDomainPermittedFaces domain+  protectedFaces =+    IntSet.fromList+      [ rawFace+      | rawPair <- IntSet.toAscList (refinementDomainInterfacePairs domain)+      , let edge = UndirectedEdgeId (fromIntegral rawPair)+      , let (forward, backward) = directedPair edge+      , rawFace <-+          [ fromIntegral (unFace (incidentFace before forward))+          , fromIntegral (unFace (incidentFace before backward))+          ]+      , rawFace > 0+      , IntSet.notMember rawFace permitted+      ]++  validateProtectedFace rawFace =+    if localFaceSignature before rawFace == localFaceSignature after rawFace+      then Right ()+      else Left (RefinementDomainProtectedFaceChanged (FaceId (fromIntegral rawFace)))++  validateInterfaceEdge rawPair =+    if interfaceEdgePreserved rawPair+      then Right ()+      else Left RefinementDomainTopologyChanged++  interfaceEdgePreserved rawPair =+    case (localEdgeSignature before rawPair, localEdgeSignature after rawPair) of+      ( Just (beforeFrom, beforeTo, beforeFaces)+        , Just (afterFrom, afterTo, afterFaces)+        ) ->+          beforeFrom == afterFrom+            && beforeTo == afterTo+            && all (`elem` afterFaces) (filter protected beforeFaces)+      _ -> False++  protected face =+    let raw = fromIntegral (unFace face)+     in raw > 0 && IntSet.notMember raw permitted++  unFace (FaceId raw) = raw++-- | A drained worklist is publishable only when its final active face section+-- satisfies the optional edge-length law. Global refinement checks every+-- active inner face; local refinement checks only its exact dynamic permitted+-- witness, never the retired handles from the input cover.+validateMaximumEdgeLength+  :: RefinementParameters+  -> Triangulation mode vertex directed undirected face+  -> [FaceId]+  -> Either BuildError ()+validateMaximumEdgeLength parameters triangulation finalPermittedFaces =+  case refineMaxEdgeLength parameters of+    Nothing -> Right ()+    Just maximumLength ->+      traverse_+        (validateFace maximumLength (maximumLength * maximumLength))+        finalPermittedFaces+ where+  validateFace :: Double -> Double -> FaceId -> Either BuildError ()+  validateFace maximumLength maximumSquaredLength face =+    traverse_ (validateEdge maximumLength maximumSquaredLength face)+      (faceDirectedEdges triangulation face)++  validateEdge :: Double -> Double -> FaceId -> DirectedEdgeId -> Either BuildError ()+  validateEdge maximumLength maximumSquaredLength face edge =+    let undirected = asUndirected edge+        (fromVertex, toVertex) = undirectedEndpoints triangulation undirected+        actualSquaredLength =+          squaredDistanceWide+            (vertexPoint triangulation fromVertex)+            (vertexPoint triangulation toVertex)+     in if actualSquaredLength > maximumSquaredLength+          then+            Left+              ( RefinementOversizedEdge+                  face+                  undirected+                  (sqrt actualSquaredLength)+                  maximumLength+              )+          else Right ()+ -- | Refuse outer or absent faces rather than silently treating an invalid -- topology witness as an empty repair. Face zero is the outer face and has no -- active refinement equation.@@ -376,6 +549,7 @@  where   totalFaces = numFaces triangulation +  validateFace :: FaceId -> Either BuildError Int   validateFace face@(FaceId raw)     | raw == 0 || toInteger raw >= toInteger totalFaces =         Left (RefinementSeedFaceNotActive face totalFaces)@@ -383,7 +557,7 @@  -- | Refuse parameters no mesh can satisfy. validateRefinementParameters :: RefinementParameters -> Either BuildError ()-validateRefinementParameters RefinementParameters{refineMaxAdditionalVertices, refineMinArea, refineMaxArea, refineMaxRadiusEdgeRatio} = do+validateRefinementParameters RefinementParameters{refineMaxAdditionalVertices, refineMinArea, refineMaxArea, refineMaxRadiusEdgeRatio, refineMaxEdgeLength} = do   case refineMaxAdditionalVertices of     Just value       | value < 0 ->@@ -404,6 +578,11 @@     RefinementMaximumRadiusEdgeRatioNotPositive     (> 0)     refineMaxRadiusEdgeRatio+  validateOptionalRefinementParameter+    RefinementMaximumEdgeLengthNotFinite+    RefinementMaximumEdgeLengthNotPositive+    (> 0)+    refineMaxEdgeLength   case (refineMinArea, refineMaxArea) of     (Just minimumArea, Just maximumArea)       | minimumArea > maximumArea ->
src-core/Moonlight/Triangulation/Internal/BoxedPaged.hs view
@@ -10,11 +10,14 @@   , BoxedStorageError (..)   , emptyBoxedPaged   , boxedFill+  , boxedDefaulted+  , boxedMaterializedPageCount   , boxedPagedLength   , boxedUnsafeIndex   , boxedFromVector   , boxedToVector   , thawBoxedPaged+  , readMutableBoxedPublicationStats   , readBoxedPaged   , writeBoxedPaged   , resetBoxedRange@@ -24,14 +27,22 @@   ) where  import Control.DeepSeq (NFData)+import Control.Monad (when) import Control.Monad.ST (ST) import Data.Bits (shiftL, shiftR, (.&.))+import Data.Foldable (traverse_) import qualified Data.IntMap.Strict as IntMap import qualified Data.List as List import Data.STRef import qualified Data.Vector as V import qualified Data.Vector.Mutable as MV import Moonlight.Triangulation.Internal.PageDirectory+import Moonlight.Triangulation.Internal.Paged+  ( PublicationStats (..)+  , TransactionShape (..)+  , addPublicationStats+  , emptyPublicationStats+  )  import GHC.Generics (Generic) @@ -68,6 +79,25 @@ boxedFill BoxedPaged{boxedStorage = DenseBoxedPages _} = Nothing boxedFill BoxedPaged{boxedStorage = DefaultedBoxedPages fill _} = Just fill +-- | Number of materialized payload pages.  Defaulted stores report only their+-- sparse directory; an absent page is the fill and was never copied.+boxedMaterializedPageCount :: BoxedPaged a -> Int+boxedMaterializedPageCount BoxedPaged{boxedStorage} = case boxedStorage of+  DenseBoxedPages pages -> V.length pages+  DefaultedBoxedPages _ pages -> directorySize pages+{-# INLINE boxedMaterializedPageCount #-}++-- | A length-bearing payload section whose every slot is its default.  The+-- section deliberately owns no materialized pages; a geometry-only+-- publication can therefore carry an arbitrarily large unit payload plane+-- without forcing a dense boxed vector at its next freeze.+boxedDefaulted :: a -> Int -> BoxedPaged a+boxedDefaulted fill length' =+  BoxedPaged+    { boxedLength = max 0 length'+    , boxedStorage = DefaultedBoxedPages fill emptyDirectory+    }+ -- Structural equality would distinguish a store that materialized a page of -- defaults from one that did not, and the two are the same store. Equality is -- therefore what can be observed: the length, the fill every unwritten slot@@ -89,7 +119,7 @@ data MutableBoxedPaged s a = MutableBoxedPaged   { mutableBoxedBase :: !(BoxedPaged a)   , mutableBoxedDirty :: !(STRef s (IntMap.IntMap (MV.MVector s a)))-  , mutableBoxedFill :: !(Maybe a)+  , mutableBoxedStats :: !(Maybe (STRef s PublicationStats))   , -- | The thaw found no materialized page. Together with an empty dirty set     -- this says every slot in the store reports the fill, which is the state a     -- payload component nobody writes stays in for the whole transaction —@@ -147,25 +177,29 @@  boxedFromVector :: Maybe a -> V.Vector a -> BoxedPaged a boxedFromVector fill values =+  let !chunks =+        List.unfoldr+          (\offset ->+             if offset >= V.length values+               then Nothing+               else+                 let !page = pageOf offset+                     !count = min pageSize (V.length values - offset)+                     !chunk = V.slice offset count values+                  in Just ((page, chunk), offset + count))+          0+   in   BoxedPaged     { boxedLength = V.length values     , boxedStorage =         case fill of-          Nothing -> DenseBoxedPages (V.fromList (map snd (go 0)))-          Just value -> DefaultedBoxedPages value (directoryFromAscList (go 0))+          Nothing -> DenseBoxedPages (V.fromList (map snd chunks))+          Just value -> DefaultedBoxedPages value (directoryFromAscList chunks)     }- where-  go !offset-    | offset >= V.length values = []-    | otherwise =-        let !page = pageOf offset-            !count = min pageSize (V.length values - offset)-            !chunk = V.slice offset count values-         in (page, chunk) : go (offset + count)  -- Resolved a page at a time. Asking the directory per element paid a descent -- for every slot of a run the descent had already found.-boxedToVector :: BoxedPaged a -> V.Vector a+boxedToVector :: forall a. BoxedPaged a -> V.Vector a boxedToVector BoxedPaged{boxedLength, boxedStorage}   | boxedLength <= 0 = V.empty   | otherwise = case boxedStorage of@@ -173,6 +207,7 @@       DefaultedBoxedPages fill pages ->         V.concat (map (pageRun fill pages) [0 .. pageOf (boxedLength - 1)])  where+  pageRun :: a -> PageDirectory (V.Vector a) -> Int -> V.Vector a   pageRun fill pages page =     let !base = page `shiftL` pageBits         !width = min pageSize (boxedLength - base)@@ -186,14 +221,18 @@ -- argument obliged every call site to name the right one, and 'boxedUpdate' — -- which passed the value being written — named the wrong one. That was harmless -- only while pages were never absent.-thawBoxedPaged :: BoxedPaged a -> ST s (MutableBoxedPaged s a)-thawBoxedPaged base = do+thawBoxedPaged :: TransactionShape -> BoxedPaged a -> ST s (MutableBoxedPaged s a)+thawBoxedPaged shape base = do   mutableBoxedDirty <- newSTRef IntMap.empty+  mutableBoxedStats <-+    case shape of+      DenseTransaction -> pure Nothing+      LocalTransaction -> Just <$> newSTRef emptyPublicationStats   pure     MutableBoxedPaged       { mutableBoxedBase = base       , mutableBoxedDirty-      , mutableBoxedFill = boxedFill base+      , mutableBoxedStats       , mutableBoxedPristineBase = case boxedStorage base of           DenseBoxedPages pages -> V.null pages           DefaultedBoxedPages _ pages -> directorySize pages == 0@@ -240,35 +279,39 @@ resetBoxedRange :: MutableBoxedPaged s a -> a -> Int -> Int -> ST s () resetBoxedRange paged@MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty} fill start count   | count <= 0 = pure ()-  | otherwise = go start+  | otherwise = traverse_ resetChunk chunkStarts  where   !end = start + count-  go !index-    | index >= end = pure ()-    | otherwise = do-        dirty <- readSTRef mutableBoxedDirty-        let !page = pageOf index-            !offset = offsetOf index-            !width = min (pageSize - offset) (end - index)-            !written =-              IntMap.member page dirty-                || case boxedStorage mutableBoxedBase of-                  DenseBoxedPages pages ->-                    page < V.length pages-                      && offset < V.length (V.unsafeIndex pages page)-                  DefaultedBoxedPages _ pages ->-                    case lookupDirectory page pages of-                      Just values -> offset < V.length values-                      Nothing -> False-        if written-          then do-            values <- ensureMutableBoxedPage paged page-            MV.set (MV.slice offset width values) fill-            go (index + width)-          else go (index + width)+  chunkStarts =+    List.unfoldr+      (\index ->+         if index >= end+           then Nothing+           else+             let !width = min (pageSize - offsetOf index) (end - index)+              in Just ((index, width), index + width))+      start -ensureMutableBoxedPage :: MutableBoxedPaged s a -> Int -> ST s (MV.MVector s a)-ensureMutableBoxedPage MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty, mutableBoxedFill} page = do+  resetChunk (index, width) = do+    dirty <- readSTRef mutableBoxedDirty+    let !page = pageOf index+        !offset = offsetOf index+        !written =+          IntMap.member page dirty+            || case boxedStorage mutableBoxedBase of+              DenseBoxedPages pages ->+                page < V.length pages+                  && offset < V.length (V.unsafeIndex pages page)+              DefaultedBoxedPages _ pages ->+                case lookupDirectory page pages of+                  Just values -> offset < V.length values+                  Nothing -> False+    when written $ do+      values <- ensureMutableBoxedPage paged page+      MV.set (MV.slice offset width values) fill++ensureMutableBoxedPage :: forall s a. MutableBoxedPaged s a -> Int -> ST s (MV.MVector s a)+ensureMutableBoxedPage MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty, mutableBoxedStats} page = do   dirty <- readSTRef mutableBoxedDirty   case IntMap.lookup page dirty of     Just values -> pure values@@ -278,32 +321,66 @@       -- per store. A full page is one thaw; a short tail page fills only the       -- slots the base does not reach. This is the same shape the unboxed       -- store already uses.-      values <- case boxedStorage mutableBoxedBase of+      (values, copiedCells) <- case boxedStorage mutableBoxedBase of         DenseBoxedPages pages           | page < V.length pages -> copyExisting (V.unsafeIndex pages page)-          | otherwise -> MV.new pageSize+          | otherwise -> do+              vector <- MV.new pageSize+              pure (vector, 0)         DefaultedBoxedPages fill pages ->           case lookupDirectory page pages of             Just old -> copyExisting old-            Nothing -> MV.replicate pageSize fill+            Nothing -> do+              vector <- MV.replicate pageSize fill+              pure (vector, 0)       writeSTRef mutableBoxedDirty $! IntMap.insert page values dirty+      let !basePageCount = boxedPageCount mutableBoxedBase+          !basePage = page < basePageCount+      recordPublicationStats+        mutableBoxedStats+        (emptyPublicationStats+             { publicationBoxedBasePageOpens = if basePage then 1 else 0+             , publicationBoxedDirtyBasePages = if basePage then 1 else 0+             , publicationBoxedDirtyAppendedPages = if basePage then 0 else 1+             , publicationBoxedCopiedCells = copiedCells+             })       pure values  where+  boxedPageCount :: BoxedPaged a -> Int+  boxedPageCount base+    | boxedLength base <= 0 = 0+    | otherwise = pageOf (boxedLength base - 1) + 1++  copyExisting :: V.Vector a -> ST s (MV.MVector s a, Int)   copyExisting old-    | V.length old == pageSize = V.thaw old+    | V.length old == pageSize = (, V.length old) <$> V.thaw old     | otherwise = do-        vector <- case mutableBoxedFill of-          Just fill -> MV.replicate pageSize fill-          Nothing -> MV.new pageSize+        vector <- case boxedStorage mutableBoxedBase of+          DenseBoxedPages _ -> MV.new pageSize+          DefaultedBoxedPages fill _ -> MV.replicate pageSize fill         V.copy (MV.slice 0 (V.length old) vector) old-        pure vector+        pure (vector, V.length old) -freezeBoxedPaged :: Int -> MutableBoxedPaged s a -> ST s (Either BoxedStorageError (BoxedPaged a))-freezeBoxedPaged length' MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty}+recordPublicationStats :: Maybe (STRef s PublicationStats) -> PublicationStats -> ST s ()+recordPublicationStats Nothing _ = pure ()+recordPublicationStats (Just statsRef) delta = modifySTRef' statsRef (addPublicationStats delta)+{-# INLINE recordPublicationStats #-}++freezeBoxedPaged :: forall s a. Int -> MutableBoxedPaged s a -> ST s (Either BoxedStorageError (BoxedPaged a))+freezeBoxedPaged length' MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty, mutableBoxedStats}   | length' < 0 = pure (Left (BoxedFreezeLengthNegative length'))   | otherwise = do       dirty <- readSTRef mutableBoxedDirty       frozenDirty <- traverse V.unsafeFreeze dirty+      case boxedStorage mutableBoxedBase of+        DenseBoxedPages _ ->+          recordPublicationStats+            mutableBoxedStats+            (emptyPublicationStats+                 { publicationBoxedBasePageEnumerations = pageCount+                 , publicationBoxedBasePageFreezes = pageCount+                 })+        DefaultedBoxedPages _ _ -> pure ()       pure $ case boxedStorage mutableBoxedBase of         DenseBoxedPages basePages -> do           pages <-@@ -333,10 +410,15 @@                 BoxedPaged                   { boxedLength = length'                   , boxedStorage = DefaultedBoxedPages fill trimmed-                  }+                }  where-  !pageCount = if length' <= 0 then 0 else pageOf (length' + pageSize - 1)+  !pageCount = if length' <= 0 then 0 else pageOf (length' - 1) + 1 +  densePage+    :: IntMap.IntMap (V.Vector a)+    -> V.Vector (V.Vector a)+    -> Int+    -> Either BoxedStorageError (V.Vector a)   densePage frozenDirty basePages page =     let !width = min pageSize (length' - page * pageSize)      in case IntMap.lookup page frozenDirty of@@ -344,6 +426,10 @@           Nothing -> case basePages V.!? page of             Just values -> Right (V.take width values)             Nothing -> Left (BoxedFreezeDensePageMissing page pageCount)++readMutableBoxedPublicationStats :: MutableBoxedPaged s a -> ST s PublicationStats+readMutableBoxedPublicationStats MutableBoxedPaged{mutableBoxedStats} =+  maybe (pure emptyPublicationStats) readSTRef mutableBoxedStats  boxedUpdate :: Int -> a -> BoxedPaged a -> BoxedPaged a boxedUpdate index value source@BoxedPaged{boxedStorage} =
src-core/Moonlight/Triangulation/Internal/FaceQueue.hs view
@@ -11,9 +11,17 @@   ) where  import Control.Monad.ST (ST)-import qualified Data.Vector.Unboxed.Mutable as MUV+import qualified Data.IntSet as IntSet+import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef) import Data.Word (Word32)-import Moonlight.Triangulation.Internal.PackedIndex (packIndex)+import qualified Data.Vector.Unboxed.Mutable as MUV+import Moonlight.Triangulation.Internal.Growable+  ( GrowableWord32+  , newGrowableWord32+  , popGrowableOr+  , pushGrowable+  )+import Moonlight.Triangulation.Internal.PackedIndex (noIndex, packIndex)  -- A worst-first heap has to be told a score for every face it is offered, so -- the caller derives one — circumradius, area, the encroachment verdict — for@@ -28,43 +36,70 @@ -- The size lives in an unboxed cell for the reason the growable stack's does: -- it is written on every push and every pop, and a boxed counter allocates a -- box per write once the size leaves the shared small-'Int' range.-data FaceQueue s = FaceQueue-  { fqFaces :: !(MUV.MVector s Word32)-  , fqPending :: !(MUV.MVector s Bool)-  , fqSize :: !(MUV.MVector s Int)-  }+data FaceQueue s+  = DenseFaceQueue+      !(MUV.MVector s Word32)+      !(MUV.MVector s Bool)+      !(MUV.MVector s Int)+  | SparseFaceQueue+      !(GrowableWord32 s)+      !(STRef s IntSet.IntSet) -newFaceQueue :: Int -> ST s (FaceQueue s)-newFaceQueue capacity = do-  let size = max 1 capacity-  fqFaces <- MUV.new size-  fqPending <- MUV.replicate size False-  fqSize <- MUV.replicate 1 0-  pure FaceQueue{..}+newFaceQueue :: Maybe Int -> ST s (FaceQueue s)+newFaceQueue capacity =+  case capacity of+    Just size -> do+      faces <- MUV.new (max 1 size)+      pending <- MUV.replicate (max 1 size) False+      stackSize <- MUV.replicate 1 0+      pure (DenseFaceQueue faces pending stackSize)+    Nothing -> do+      faces <- newGrowableWord32 16+      pending <- newSTRef IntSet.empty+      pure (SparseFaceQueue faces pending)  -- The checked read of the pending flag is the one bound this module does not -- establish itself: the face arrives from mesh topology. Once it succeeds, -- uniqueness proves the stack write: at most one slot exists for each pending -- flag, and the two vectors have the same length. pushFace :: FaceQueue s -> Int -> ST s ()-pushFace FaceQueue{fqFaces, fqPending, fqSize} face = do-  pending <- MUV.read fqPending face-  if pending-    then pure ()-    else do-      size <- MUV.unsafeRead fqSize 0-      MUV.unsafeWrite fqFaces size (packIndex face)-      MUV.unsafeWrite fqPending face True-      MUV.unsafeWrite fqSize 0 (size + 1)+pushFace queue face =+  case queue of+    DenseFaceQueue faces pending stackSize -> do+      isPending <- MUV.unsafeRead pending face+      if isPending+        then pure ()+        else do+          size <- MUV.unsafeRead stackSize 0+          MUV.unsafeWrite faces size (packIndex face)+          MUV.unsafeWrite pending face True+          MUV.unsafeWrite stackSize 0 (size + 1)+    SparseFaceQueue faces pending -> do+      isPending <- IntSet.member face <$> readSTRef pending+      if isPending+        then pure ()+        else do+          pushGrowable faces (packIndex face)+          modifySTRef' pending (IntSet.insert face)  popFace :: FaceQueue s -> ST s (Maybe Int)-popFace FaceQueue{fqFaces, fqPending, fqSize} = do-  size <- MUV.unsafeRead fqSize 0-  if size == 0-    then pure Nothing-    else do-      let !index = size - 1-      face <- fromIntegral <$> MUV.unsafeRead fqFaces index-      MUV.unsafeWrite fqPending face False-      MUV.unsafeWrite fqSize 0 index-      pure (Just face)+popFace queue =+  case queue of+    DenseFaceQueue faces pending stackSize -> do+      size <- MUV.unsafeRead stackSize 0+      if size == 0+        then pure Nothing+        else do+          let !index = size - 1+          face <- fromIntegral <$> MUV.unsafeRead faces index+          MUV.unsafeWrite pending face False+          MUV.unsafeWrite stackSize 0 index+          pure (Just face)+    SparseFaceQueue faces pending -> do+      packed <- popGrowableOr noIndex faces+      if packed == noIndex+        then pure Nothing+        else do+          let !face = fromIntegral packed+          modifySTRef' pending (IntSet.delete face)+          pure (Just face)
src-core/Moonlight/Triangulation/Internal/Paged.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Moonlight.Triangulation.Internal.Paged   ( Paged@@ -9,6 +10,11 @@   , fromLocalVector   , toVector   , pagedLength+  , pagedPageCount+  , pagedOverlayPageCount+  , PublicationStats (..)+  , emptyPublicationStats+  , addPublicationStats   , pagedUnsafeIndex   , pagedFoldl'   , newMutablePaged@@ -23,57 +29,115 @@   , readPaged   , writePaged   , freezePaged+  , readMutablePagedPublicationStats   ) where  import Control.DeepSeq (NFData (..))-import Control.Monad.ST (ST, runST)+import Control.Monad (when)+import Control.Monad.ST (ST) import Data.Bits (shiftL, shiftR, (.&.))-import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef)-import qualified Data.Vector as V-import qualified Data.Vector.Mutable as MV+import qualified Data.IntMap.Strict as IntMap+import Data.List (unfoldr)+import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef) import qualified Data.Vector.Unboxed as U import qualified Data.Vector.Unboxed.Mutable as MUV+import Moonlight.Triangulation.Internal.PageDirectory+  ( PageDirectory+  , directoryRestrict+  , directorySize+  , emptyDirectory+  , insertDirectory+  , lookupDirectory+  ) --- | One physical store with two lawful local sections. A bulk constructor--- publishes one contiguous plane for hot traversal. A persistent edit derives--- fixed-size shared pages from that plane without copying and republishes only--- pages it wrote. Both constructors denote exactly the same indexed sequence;--- equality, serialization, and compaction descend through that sequence.+-- | One flat root plus one cumulative page overlay. An extension never wraps+-- a published value in another 'Paged' node: dirty pages are merged into the+-- existing directory, so an untouched read remains one overlay lookup plus+-- one root lookup regardless of extension count. data Paged a   = FlatPaged {-# UNPACK #-} !Int {-# UNPACK #-} !Int !(U.Vector a)-  | SharedPaged {-# UNPACK #-} !Int {-# UNPACK #-} !Int !(V.Vector (U.Vector a))+  | SharedPaged+      {-# UNPACK #-} !Int+      {-# UNPACK #-} !Int+      !(U.Vector a)+      !(PageDirectory (U.Vector a)) --- | A fresh build owns a contiguous arena. A transaction over a published--- value owns only its mutable page table and clones a page before its first--- write. No mutable page or state token can escape this module.+-- | Exact storage work observed by one persistent publication.  The dense+-- page counters describe a whole-base traversal; dirty-page and copied-cell+-- counters describe only pages opened by the local write set.  A zero dense+-- counter is therefore a storage fact from the transaction owner, never a+-- benchmark inference; page work is independent of the resident prefix except+-- for directory-path depth.+data PublicationStats = PublicationStats+  { publicationUnboxedBasePageEnumerations :: {-# UNPACK #-} !Int+  , publicationUnboxedBasePageOpens :: {-# UNPACK #-} !Int+  , publicationUnboxedBasePageFreezes :: {-# UNPACK #-} !Int+  , publicationUnboxedDirtyBasePages :: {-# UNPACK #-} !Int+  , publicationUnboxedDirtyAppendedPages :: {-# UNPACK #-} !Int+  , publicationUnboxedCopiedCells :: {-# UNPACK #-} !Int+  , publicationBoxedBasePageEnumerations :: {-# UNPACK #-} !Int+  , publicationBoxedBasePageOpens :: {-# UNPACK #-} !Int+  , publicationBoxedBasePageFreezes :: {-# UNPACK #-} !Int+  , publicationBoxedDirtyBasePages :: {-# UNPACK #-} !Int+  , publicationBoxedDirtyAppendedPages :: {-# UNPACK #-} !Int+  , publicationBoxedCopiedCells :: {-# UNPACK #-} !Int+  }+  deriving stock (Eq, Show)++instance NFData PublicationStats where+  rnf stats = stats `seq` ()++emptyPublicationStats :: PublicationStats+emptyPublicationStats =+  PublicationStats+    { publicationUnboxedBasePageEnumerations = 0+    , publicationUnboxedBasePageOpens = 0+    , publicationUnboxedBasePageFreezes = 0+    , publicationUnboxedDirtyBasePages = 0+    , publicationUnboxedDirtyAppendedPages = 0+    , publicationUnboxedCopiedCells = 0+    , publicationBoxedBasePageEnumerations = 0+    , publicationBoxedBasePageOpens = 0+    , publicationBoxedBasePageFreezes = 0+    , publicationBoxedDirtyBasePages = 0+    , publicationBoxedDirtyAppendedPages = 0+    , publicationBoxedCopiedCells = 0+    }++addPublicationStats :: PublicationStats -> PublicationStats -> PublicationStats+addPublicationStats left right =+  PublicationStats+    { publicationUnboxedBasePageEnumerations = publicationUnboxedBasePageEnumerations left + publicationUnboxedBasePageEnumerations right+    , publicationUnboxedBasePageOpens = publicationUnboxedBasePageOpens left + publicationUnboxedBasePageOpens right+    , publicationUnboxedBasePageFreezes = publicationUnboxedBasePageFreezes left + publicationUnboxedBasePageFreezes right+    , publicationUnboxedDirtyBasePages = publicationUnboxedDirtyBasePages left + publicationUnboxedDirtyBasePages right+    , publicationUnboxedDirtyAppendedPages = publicationUnboxedDirtyAppendedPages left + publicationUnboxedDirtyAppendedPages right+    , publicationUnboxedCopiedCells = publicationUnboxedCopiedCells left + publicationUnboxedCopiedCells right+    , publicationBoxedBasePageEnumerations = publicationBoxedBasePageEnumerations left + publicationBoxedBasePageEnumerations right+    , publicationBoxedBasePageOpens = publicationBoxedBasePageOpens left + publicationBoxedBasePageOpens right+    , publicationBoxedBasePageFreezes = publicationBoxedBasePageFreezes left + publicationBoxedBasePageFreezes right+    , publicationBoxedDirtyBasePages = publicationBoxedDirtyBasePages left + publicationBoxedDirtyBasePages right+    , publicationBoxedDirtyAppendedPages = publicationBoxedDirtyAppendedPages left + publicationBoxedDirtyAppendedPages right+    , publicationBoxedCopiedCells = publicationBoxedCopiedCells left + publicationBoxedCopiedCells right+    }++-- | A fresh build owns a contiguous arena. A local transaction owns only its+-- dirty pages; clean values remain reachable through the immutable base. data MutablePaged s a   = MutableFlatPaged {-# UNPACK #-} !Int !(MUV.MVector s a)   | MutableSharedPaged       {-# UNPACK #-} !Int       !(Paged a)-      !(V.Vector (U.Vector a))-      !(MV.MVector s (MUV.MVector s a))-      !(MUV.MVector s Bool)-      !(STRef s Int)+      !(STRef s (IntMap.IntMap (MUV.MVector s a)))+      !(STRef s PublicationStats)  -- | A proof that one mutable sequence is in its contiguous physical section.--- The constructor stays private: callers can only obtain it by descending--- through 'flatMutableSection', after which hot indexed programs no longer--- re-test the storage sum at every cell.-newtype FlatMutablePaged s a = FlatMutablePaged (MUV.MVector s a)+data FlatMutablePaged s a = FlatMutablePaged !(MUV.MVector s a)  localPageBits :: Int localPageBits = 8 {-# INLINE localPageBits #-} --- A published plane's page geometry is the clone quantum of every--- copy-on-write transaction opened on it. Full ladder 2026-08-06 on the--- star-local excise (persistent-removal 10k/2.5k, min of 4 interleaved--- rounds): 62.6ms at 12 bits, 57.2ms at 10, 83.2ms at 8 — reproducing the--- 2026-08-05 finding that at 8 bits the per-transaction table build and--- freeze scan outweigh the smaller clones (profiled there at 61% of the--- lane), while at 12 the scattered-touch clones dominate instead. 10 is the--- measured saddle between the two taxes. traversalPageBits :: Int traversalPageBits = 10 {-# INLINE traversalPageBits #-}@@ -93,180 +157,146 @@ pagesFor :: Int -> Int -> Int pagesFor bits count   | count <= 0 = 0-  | otherwise = pageOf bits (count + pageSize bits - 1)+  | otherwise = pageOf bits (count - 1) + 1 {-# INLINE pagesFor #-}  pagedLength :: Paged a -> Int-pagedLength paged =-  case paged of-    FlatPaged count _ _ -> count-    SharedPaged count _ _ -> count+pagedLength paged = case paged of+  FlatPaged count _ _ -> count+  SharedPaged count _ _ _ -> count {-# INLINE pagedLength #-} +pagedPageCount :: Paged a -> Int+pagedPageCount paged = pagesFor (pagedBits paged) (pagedLength paged)+{-# INLINE pagedPageCount #-}++-- | Number of pages in the persistent overlay.  A flat root has no copied+-- pages; a shared value records only pages dirtied by its transaction.  This+-- is an observation for publication receipts, not a read-path traversal.+pagedOverlayPageCount :: Paged a -> Int+pagedOverlayPageCount paged = case paged of+  FlatPaged{} -> 0+  SharedPaged _ _ _ directory -> directorySize directory+{-# INLINE pagedOverlayPageCount #-}+ pagedBits :: Paged a -> Int pagedBits paged = case paged of   FlatPaged _ bits _ -> bits-  SharedPaged _ bits _ -> bits+  SharedPaged _ bits _ _ -> bits {-# INLINE pagedBits #-} -pageVector :: U.Unbox a => Paged a -> V.Vector (U.Vector a)-pageVector source = case source of-  FlatPaged count bits values ->-    V.generate-      (pagesFor bits count)-      (\index ->-         let !offset = index `shiftL` bits-             !width = min (pageSize bits) (count - offset)-          in U.unsafeSlice offset width values-      )-  SharedPaged _ _ pages -> pages-{-# INLINE pageVector #-}- instance (U.Unbox a, Eq a) => Eq (Paged a) where   left == right =     pagedLength left == pagedLength right       && case (left, right) of-           (FlatPaged _ _ leftValues, FlatPaged _ _ rightValues) ->-             leftValues == rightValues-           _ -> pagedChunksEqual left right+        (FlatPaged _ _ leftValues, FlatPaged _ _ rightValues) -> leftValues == rightValues+        _ -> pagedChunksEqual left right --- Structural equality is a join shortcut and must not materialize either--- operand. Chunks are compared over the overlap of the two page geometries,--- so operands with different page widths still compare slice against slice.-pagedChunksEqual :: (U.Unbox a, Eq a) => Paged a -> Paged a -> Bool-pagedChunksEqual left right = walk 0+pagedChunksEqual :: forall a. (U.Unbox a, Eq a) => Paged a -> Paged a -> Bool+pagedChunksEqual left right =+  foldl'+    (\equal (index, width) ->+       equal+         && U.unsafeSlice 0 width (pageSlice left index)+              == U.unsafeSlice 0 width (pageSlice right index))+    True+    chunks  where   !count = pagedLength left   !leftBits = pagedBits left   !rightBits = pagedBits right-  leftPages = pageVector left-  rightPages = pageVector right-  walk !offset-    | offset >= count = True-    | otherwise =-        let !leftPage = V.unsafeIndex leftPages (pageOf leftBits offset)-            !leftOffset = offsetOf leftBits offset-            !rightPage = V.unsafeIndex rightPages (pageOf rightBits offset)-            !rightOffset = offsetOf rightBits offset-            !width =-              min-                (count - offset)-                ( min-                    (U.length leftPage - leftOffset)-                    (U.length rightPage - rightOffset)-                )-         in U.unsafeSlice leftOffset width leftPage-              == U.unsafeSlice rightOffset width rightPage-              && walk (offset + width)+  chunks = unfoldr nextChunk 0 +  nextChunk !index+    | index >= count = Nothing+    | otherwise = Just ((index, width), index + width)+    where+      !leftPageEnd = (pageOf leftBits index + 1) * pageSize leftBits+      !rightPageEnd = (pageOf rightBits index + 1) * pageSize rightBits+      !width = min (count - index) (min (leftPageEnd - index) (rightPageEnd - index))+  pageSlice :: Paged a -> Int -> U.Vector a+  pageSlice paged index =+    let !bits = pagedBits paged+        !page = pageOf bits index+        !offset = offsetOf bits index+     in case paged of+          FlatPaged _ _ values -> U.unsafeSlice index (U.length values - index) values+          SharedPaged _ _ root directory ->+            case lookupDirectory page directory of+              Just values -> U.unsafeSlice offset (U.length values - offset) values+              Nothing -> U.unsafeSlice index (U.length root - index) root+ instance (U.Unbox a, Show a) => Show (Paged a) where   showsPrec precedence = showsPrec precedence . U.toList . toVector  instance NFData (Paged a) where-  rnf paged =-    case paged of-      FlatPaged count bits values -> count `seq` bits `seq` values `seq` ()-      SharedPaged count bits pages ->-        count `seq` bits `seq` V.foldl' (\() page -> page `seq` ()) () pages+  rnf paged = case paged of+    FlatPaged count bits values -> count `seq` bits `seq` values `seq` ()+    SharedPaged count bits root directory ->+      count `seq` bits `seq` root `seq` directory `seq` () -emptyPaged :: Paged a-emptyPaged = SharedPaged 0 traversalPageBits V.empty+emptyPaged :: U.Unbox a => Paged a+emptyPaged = FlatPaged 0 traversalPageBits U.empty --- The padding belongs to page publication rather than dense ingress. A dense--- plane stores exactly its live sequence and therefore needs no tail value. fromVector :: U.Unbox a => a -> U.Vector a -> Paged a fromVector _padding values = FlatPaged (U.length values) traversalPageBits values --- | Dense ingress whose future edits are expected to be local. The value is--- still one flat plane until a transaction publishes a changed leaf. fromLocalVector :: U.Unbox a => a -> U.Vector a -> Paged a fromLocalVector _padding values = FlatPaged (U.length values) localPageBits values  toVector :: U.Unbox a => Paged a -> U.Vector a-toVector paged =-  case paged of-    FlatPaged _ _ values -> values-    SharedPaged count bits pages-      | count <= 0 -> U.empty-      | otherwise -> runST $ do-          output <- MUV.new count-          let copyPage !pageIndex !offset-                | offset >= count = pure ()-                | otherwise = do-                    let !width = min (pageSize bits) (count - offset)-                    U.copy-                      (MUV.unsafeSlice offset width output)-                      (U.unsafeSlice 0 width (V.unsafeIndex pages pageIndex))-                    copyPage (pageIndex + 1) (offset + width)-          copyPage 0 0-          U.unsafeFreeze output+toVector paged = case paged of+  FlatPaged count _ values -> U.unsafeSlice 0 count values+  SharedPaged count _ _ _ -> U.generate count (pagedUnsafeIndex paged)  pagedUnsafeIndex :: U.Unbox a => Paged a -> Int -> a pagedUnsafeIndex paged index =   case paged of     FlatPaged _ _ values -> U.unsafeIndex values index-    SharedPaged _ bits pages ->-      U.unsafeIndex (V.unsafeIndex pages (pageOf bits index)) (offsetOf bits index)+    SharedPaged _ bits root directory ->+      case lookupDirectory (pageOf bits index) directory of+        Just values -> U.unsafeIndex values (offsetOf bits index)+        Nothing -> U.unsafeIndex root index {-# INLINE pagedUnsafeIndex #-} +-- | Fold directly over the physical sections. Unlike the old implementation,+-- this does not allocate a complete vector merely to consume it. pagedFoldl' :: U.Unbox a => (b -> a -> b) -> b -> Paged a -> b pagedFoldl' step initial paged =   case paged of-    FlatPaged _ _ values -> U.foldl' step initial values-    SharedPaged count bits pages -> foldPages 0 0 initial+    FlatPaged count _ values -> U.foldl' step initial (U.take count values)+    SharedPaged count bits root directory ->+      foldl' foldPage initial [0 .. pagesFor bits count - 1]      where-      foldPages !pageIndex !offset !accumulated-        | offset >= count = accumulated-        | otherwise =-            let !width = min (pageSize bits) (count - offset)-                !next =-                  U.foldl'-                    step-                    accumulated-                    (U.unsafeSlice 0 width (V.unsafeIndex pages pageIndex))-             in foldPages (pageIndex + 1) (offset + width) next+      foldPage !accumulated page =+        let !start = page * pageSize bits+            !width = min (pageSize bits) (count - start)+            !values =+              case lookupDirectory page directory of+                Just pageValues -> U.unsafeSlice 0 width pageValues+                Nothing -> U.unsafeSlice start width root+         in U.foldl' step accumulated values {-# INLINE pagedFoldl' #-} --- | Allocate the one contiguous mutable arena owned by a fresh constructor. newMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)-newMutablePaged capacity = MutableFlatPaged traversalPageBits <$> MUV.new (max 0 capacity)+newMutablePaged capacity = do+  values <- MUV.new (max 0 capacity)+  pure (MutableFlatPaged traversalPageBits values)  newLocalMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)-newLocalMutablePaged capacity = MutableFlatPaged localPageBits <$> MUV.new (max 0 capacity)+newLocalMutablePaged capacity = do+  values <- MUV.new (max 0 capacity)+  pure (MutableFlatPaged localPageBits values) --- | Derive a copy-on-write page table from a published plane. Flat bases--- become zero-copy slices of their one ByteArray; already shared bases reuse--- their page vectors. Capacity-only tail pages all point to one empty sentinel--- and acquire storage only when an append first writes them. Reads stay one--- direct page-table access — 2026-08-05, measured: routing reads through a--- dirty-map overlay instead taxed every walk and regressed the local verbs by--- 2x, so the table is built eagerly and only publication consults ownership.-thawPaged :: U.Unbox a => Int -> Paged a -> ST s (MutablePaged s a)-thawPaged requestedCapacity paged = do-  let !count = pagedLength paged-      !capacity = max requestedCapacity count-      !bits = pagedBits paged-      !basePages = pageVector paged-      !basePageCount = V.length basePages-      !pageCount = pagesFor bits capacity-  pages <- MV.new pageCount-  owned <- MUV.replicate pageCount False-  ownedCount <- newSTRef 0-  sentinel <- MUV.new 0-  V.imapM_-    (\index immutablePage ->-       U.unsafeThaw immutablePage >>= MV.unsafeWrite pages index-    )-    basePages-  MV.set-    (MV.unsafeSlice basePageCount (pageCount - basePageCount) pages)-    sentinel-  pure (MutableSharedPaged bits paged basePages pages owned ownedCount)+-- | Open a persistent transaction without constructing a page table over the+-- resident mesh. The mutable dictionary starts empty and acquires storage only+-- when a write first reaches a page.+thawPaged :: Int -> Paged a -> ST s (MutablePaged s a)+thawPaged _requestedCapacity paged = do+  dirty <- newSTRef IntMap.empty+  stats <- newSTRef emptyPublicationStats+  pure (MutableSharedPaged (pagedBits paged) paged dirty stats) --- | The two lawful physical sections of one transaction boundary. Both--- publish the same canonical sequence; they differ in what the transaction--- pays for. The choice belongs to the operation that knows its own edit--- volume, never to a public caller. data TransactionShape = DenseTransaction | LocalTransaction  thawPagedShaped :: U.Unbox a => TransactionShape -> Int -> Paged a -> ST s (MutablePaged s a)@@ -275,36 +305,18 @@   LocalTransaction -> thawPaged {-# INLINE thawPagedShaped #-} --- | Materialize one dense mutable arena from a published value. A batch--- transaction amortizes this single copy over many edits and then reads and--- writes flat storage with no page bookkeeping; the copy-on-write 'thawPaged'--- remains the local-edit section whose publication is proportional to dirty--- pages. Both freeze to the same canonical sequence. thawPagedDense :: U.Unbox a => Int -> Paged a -> ST s (MutablePaged s a) thawPagedDense requestedCapacity paged = do   let !count = pagedLength paged       !capacity = max requestedCapacity count   values <- MUV.new capacity-  case paged of-    FlatPaged _ _ source ->-      U.copy (MUV.unsafeSlice 0 count values) source-    SharedPaged _ bits pages ->-      let copyPage !pageIndex !offset-            | offset >= count = pure ()-            | otherwise = do-                let !width = min (pageSize bits) (count - offset)-                U.copy-                  (MUV.unsafeSlice offset width values)-                  (U.unsafeSlice 0 width (V.unsafeIndex pages pageIndex))-                copyPage (pageIndex + 1) (offset + width)-       in copyPage 0 0+  U.copy (MUV.unsafeSlice 0 count values) (toVector paged)   pure (MutableFlatPaged (pagedBits paged) values)  flatMutableSection :: MutablePaged s a -> Maybe (FlatMutablePaged s a)-flatMutableSection mutable =-  case mutable of-    MutableFlatPaged _ values -> Just (FlatMutablePaged values)-    MutableSharedPaged{} -> Nothing+flatMutableSection mutable = case mutable of+  MutableFlatPaged _ values -> Just (FlatMutablePaged values)+  MutableSharedPaged{} -> Nothing {-# INLINE flatMutableSection #-}  readFlatMutable :: U.Unbox a => FlatMutablePaged s a -> Int -> ST s a@@ -316,89 +328,89 @@ {-# INLINE writeFlatMutable #-}  readPaged :: U.Unbox a => MutablePaged s a -> Int -> ST s a-readPaged mutable index =-  case mutable of-    MutableFlatPaged _ values -> MUV.unsafeRead values index-    MutableSharedPaged bits _ _ pages _ _ -> do-      page <- MV.unsafeRead pages (pageOf bits index)-      MUV.unsafeRead page (offsetOf bits index)+readPaged mutable index = case mutable of+  MutableFlatPaged _ values -> MUV.unsafeRead values index+  MutableSharedPaged bits base dirtyRef _ -> do+    dirty <- readSTRef dirtyRef+    case IntMap.lookup (pageOf bits index) dirty of+      Just values -> MUV.unsafeRead values (offsetOf bits index)+      Nothing -> pure (pagedUnsafeIndex base index) {-# INLINE readPaged #-}  writePaged :: U.Unbox a => MutablePaged s a -> Int -> a -> ST s ()-writePaged mutable index value =-  case mutable of-    MutableFlatPaged _ values -> MUV.unsafeWrite values index value-    MutableSharedPaged bits _ _ pages owned ownedCount -> do-      let !slot = pageOf bits index-      mine <- MUV.unsafeRead owned slot-      page <--        if mine-          then MV.unsafeRead pages slot-          else acquireSharedWritablePage (pageSize bits) pages owned ownedCount slot-      MUV.unsafeWrite page (offsetOf bits index) value+writePaged mutable index value = case mutable of+  MutableFlatPaged _ values -> MUV.unsafeWrite values index value+  MutableSharedPaged bits base dirtyRef statsRef -> do+    let !page = pageOf bits index+        !start = page * pageSize bits+        !existingWidth = min (pageSize bits) (max 0 (pagedLength base - start))+    dirty <- readSTRef dirtyRef+    values <- case IntMap.lookup page dirty of+      Just existing -> pure existing+      Nothing -> do+        pageValues <- MUV.new (pageSize bits)+        when (existingWidth > 0) $+            U.copy+              (MUV.unsafeSlice 0 existingWidth pageValues)+              (U.generate existingWidth (\offset -> pagedUnsafeIndex base (start + offset)))+        writeSTRef dirtyRef $! IntMap.insert page pageValues dirty+        modifySTRef'+          statsRef+          (addPublicationStats+             emptyPublicationStats+               { publicationUnboxedBasePageOpens = if existingWidth > 0 then 1 else 0+               , publicationUnboxedDirtyBasePages = if existingWidth > 0 then 1 else 0+               , publicationUnboxedDirtyAppendedPages = if existingWidth > 0 then 0 else 1+               , publicationUnboxedCopiedCells = existingWidth+               })+        pure pageValues+    MUV.unsafeWrite values (offsetOf bits index) value {-# INLINE writePaged #-} --- Keep the once-per-page copy-on-write transition behind one compiled--- boundary. Both steady-state write paths remain inline; dependent modules do--- not repeatedly simplify page cloning into every element write.-acquireSharedWritablePage-  :: U.Unbox a-  => Int-  -> MV.MVector s (MUV.MVector s a)-  -> MUV.MVector s Bool-  -> STRef s Int-  -> Int-  -> ST s (MUV.MVector s a)-acquireSharedWritablePage width pages owned ownedCount slot = do-  shared <- MV.unsafeRead pages slot-  copy <--    if MUV.length shared == width-      then MUV.clone shared-      else do-        copy <- MUV.new width-        MUV.copy (MUV.unsafeSlice 0 (MUV.length shared) copy) shared-        pure copy-  MV.unsafeWrite pages slot copy-  MUV.unsafeWrite owned slot True-  modifySTRef' ownedCount (+ 1)-  pure copy-{-# INLINE[0] acquireSharedWritablePage #-}--freezePaged :: U.Unbox a => Int -> MutablePaged s a -> ST s (Paged a)+-- | Publish only dirty pages. A local publication retains one immutable root+-- and merges the changed page paths into its cumulative directory.+freezePaged :: forall s a. U.Unbox a => Int -> MutablePaged s a -> ST s (Paged a) freezePaged count mutable   | count <= 0 = pure emptyPaged-  | otherwise =-      case mutable of-        -- 'U.take' is an O(1) slice, so freezing that way republishes the whole-        -- growth reservation and holds it for the lifetime of the value. The-        -- arena is deliberately loose during construction; carrying that slack-        -- past publication is not the same decision. Above an eighth the copy-        -- is paid once and the dead tail is released, which is also what the-        -- shared branch below already does by generating exactly its pages.-        MutableFlatPaged bits values-          | MUV.length values <= count + (count `quot` 8) ->-              FlatPaged count bits . U.take count <$> U.unsafeFreeze values-          | otherwise ->-              FlatPaged count bits <$> U.freeze (MUV.unsafeSlice 0 count values)-        MutableSharedPaged bits base basePages pages owned ownedCount -> do-          dirtyPages <- readSTRef ownedCount-          if dirtyPages == 0 && count == pagedLength base-            -- An untouched plane republishes as the value it opened on.-            then pure base-            else-              SharedPaged count bits-                <$> V.generateM-                  (pagesFor bits count)-                  (\index -> do-                     mine <- MUV.unsafeRead owned index-                     if mine-                       then MV.unsafeRead pages index >>= U.unsafeFreeze-                       else-                         if index >= V.length basePages-                           -- A clean slot past the base geometry still holds-                           -- the zero-length sentinel; publish allocated-                           -- storage instead, matching the flat arena's-                           -- answer to an unwritten tail.-                           then MUV.new (pageSize bits) >>= U.unsafeFreeze-                           else pure (V.unsafeIndex basePages index)-                  )+  | otherwise = case mutable of+      MutableFlatPaged bits values+        | MUV.length values <= count + (count `quot` 8) ->+            FlatPaged count bits . U.take count <$> U.unsafeFreeze values+        | otherwise ->+            FlatPaged count bits <$> U.freeze (MUV.unsafeSlice 0 count values)+      MutableSharedPaged bits base dirtyRef _statsRef -> do+        dirty <- readSTRef dirtyRef+        if IntMap.null dirty && count == pagedLength base+          then pure base+          else do+            frozen <- traverse freezePage (IntMap.toAscList dirty)+            let !priorDirectory = sharedDirectory base+                !directory =+                  foldl'+                    (\accumulated (page, values) -> insertDirectory page values accumulated)+                    priorDirectory+                    frozen+                !root = sharedRoot base+            pure (SharedPaged count bits root (directoryRestrict (pagesFor bits count) directory))+ where+  freezePage :: (Int, MUV.MVector s a) -> ST s (Int, U.Vector a)+  freezePage (page, values) = do+    frozen <- U.unsafeFreeze values+    pure (page, frozen)++sharedRoot :: Paged a -> U.Vector a+sharedRoot paged = case paged of+  FlatPaged _ _ values -> values+  SharedPaged _ _ root _ -> root+{-# INLINE sharedRoot #-}++sharedDirectory :: Paged a -> PageDirectory (U.Vector a)+sharedDirectory paged = case paged of+  FlatPaged _ _ _ -> emptyDirectory+  SharedPaged _ _ _ directory -> directory+{-# INLINE sharedDirectory #-}++readMutablePagedPublicationStats :: MutablePaged s a -> ST s PublicationStats+readMutablePagedPublicationStats mutable = case mutable of+  MutableFlatPaged _ _ -> pure emptyPublicationStats+  MutableSharedPaged _ _ _ statsRef -> readSTRef statsRef
src-dcel/Moonlight/Triangulation/FloodFillIterator.hs view
@@ -26,6 +26,7 @@   , BoundaryOrientation (..)   , BoundaryLoop   , boundaryLoopOrientation+  , boundaryLoopResidentVertices   , boundaryLoopVertices   , RegionBoundary   , regionBoundaryOuterLoop@@ -36,6 +37,7 @@   , labelledRegionBoundaries   , componentBoundaryLoops   , componentBoundary+  , componentBoundaryForFaces   ) where  import Control.DeepSeq (NFData)@@ -83,6 +85,10 @@ -- filled area under nonzero winding; clockwise loops subtract holes. data BoundaryLoop = BoundaryLoop   { boundaryLoopOrientation :: !BoundaryOrientation+  -- | Exact resident DCEL boundary chain. Consecutive vertices, including+  -- the closing pair, are endpoints of one boundary half-edge.+  , boundaryLoopResidentVertices :: !(NonEmpty VertexId)+  -- | Collinearity-reduced polygon observation of the same resident chain.   , boundaryLoopVertices :: !(NonEmpty VertexId)   }   deriving stock (Eq, Show, Generic)@@ -429,6 +435,38 @@       traceBoundaryLoops triangulation outgoing edges         >>= regionBoundaryFromLoops +-- | Project exactly the supplied face section into one boundary. This is the+-- selected-section descent used by local extension: it does not label or+-- flood the complement, and therefore its work is proportional to the faces+-- named by the caller plus their constant-degree boundary edges.+componentBoundaryForFaces+  :: Triangulation mode vertex directed undirected face+  -> Set.Set FaceId+  -> Either BoundaryObstruction RegionBoundary+componentBoundaryForFaces triangulation selectedFaces = do+  component <- selectedFaceComponent triangulation selectedFaces+  componentBoundary triangulation component++selectedFaceComponent+  :: Triangulation mode vertex directed undirected face+  -> Set.Set FaceId+  -> Either BoundaryObstruction FaceComponent+selectedFaceComponent triangulation selectedFaces =+  case IntSet.lookupMin indices of+    Just 0 -> Left (BoundaryComponentFaceOutOfRange (FaceId 0) (numFaces triangulation))+    _ ->+      case IntSet.lookupGE (numFaces triangulation) indices of+        Just invalid ->+          Left+            ( BoundaryComponentFaceOutOfRange+                (FaceId (fromIntegral invalid))+                (numFaces triangulation)+            )+        Nothing -> Right (FaceComponent indices)+ where+  indices = IntSet.fromList (fmap faceIndex (Set.toAscList selectedFaces))+  faceIndex (FaceId raw) = fromIntegral raw+ componentBoundaryGraph   :: Triangulation mode vertex directed undirected face   -> FaceComponent@@ -574,9 +612,14 @@       winding       key       vertices+  residentVertices <-+    maybe+      (Left (BoundaryLoopDegenerate vertices))+      Right+      (NonEmpty.nonEmpty vertices)   case windingOrder of-    GT -> Right (BoundaryLoop BoundaryCounterClockwise simplifiedVertices)-    LT -> Right (BoundaryLoop BoundaryClockwise simplifiedVertices)+    GT -> Right (BoundaryLoop BoundaryCounterClockwise residentVertices simplifiedVertices)+    LT -> Right (BoundaryLoop BoundaryClockwise residentVertices simplifiedVertices)     EQ -> Left (BoundaryLoopDegenerate (NonEmpty.toList simplifiedVertices))  where   point vertex = vertexPoint triangulation vertex
src-dcel/Moonlight/Triangulation/Internal/DcelOperations/FlipRule.hs view
@@ -50,6 +50,7 @@     -- by Spade's removal legalizer: the diagonal rule alone decides.     ValidMesh -> illegalDiagonal ax ay bx by cx cy dx dy     CavityRepair _ -> illegalDiagonal ax ay bx by cx cy dx dy+    SeamPreserving _ -> illegalDiagonal ax ay bx by cx cy dx dy {-# INLINE diagonalFires #-}  -- | Whether a diagonal is locally illegal — the firing condition of the flip@@ -106,7 +107,7 @@ {-# INLINE isFlippableEdge #-}  -- | The rewrite strategy: which candidates the flip rule is allowed to fire--- on. Both laws leave the normal form alone — they restrict where the rule may+-- on. All laws leave the normal form alone — they restrict where the rule may -- be applied, never what a legal diagonal is. -- -- 'ValidMesh' legalizes a mesh that is already a triangulation. There a@@ -121,6 +122,12 @@ -- edge count taken before the fan was built: every edge at or above it was -- appended by the fan, and only those may flip, so a transiently inverted -- neighbourhood cannot flip the cavity's border away.+--+-- 'SeamPreserving' admits a candidate exactly when neither incident target+-- face handle is selected by the caller. Face handles survive a flip, so this+-- protection is inductive while legalization propagates through the complete+-- unselected seam cavity. data LegalizationLaw   = ValidMesh   | CavityRepair {-# UNPACK #-} !Int+  | SeamPreserving !(Int -> Bool)
src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Legalize.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -12,6 +11,7 @@   , legalizeStarEdge   , legalizeDenseStarEdgeInArena   , legalizeEdges+  , legalizeEdgesPinned   , legalizeCavityFanScratch   ) where @@ -25,7 +25,9 @@   , seedGenericEdges   , seedStarScratch   )-import Moonlight.Triangulation.Internal.DcelOperations.FlipRule (LegalizationLaw (..))+import Moonlight.Triangulation.Internal.DcelOperations.FlipRule+  ( LegalizationLaw (..)+  ) import Moonlight.Triangulation.Internal.DcelOperations.Normalize   ( LegalizationDrain   , drainDenseUnconstrainedStarLegalization@@ -54,6 +56,31 @@ legalizeEdges mutable operation initial = do   top <- seedGenericEdges operation 0 initial   (flips, maxDepth) <- drainLegalization @'ProbeOff mutable operation top GenericCandidates ValidMesh+  addCounter operation CounterEdgeFlips flips+  maxCounter operation CounterLegalizationMaxStack maxDepth++-- | Legalize the new seam and the source-prefix cavity whose current incident+-- face handles are both unprotected. The selector is evaluated dynamically as+-- flips propagate; it therefore admits the complete local exterior cavity+-- rather than only the zipper's first seeds. The seam owner certifies the+-- resulting cavity boundary after immutable publication.+legalizeEdgesPinned+  :: forall s vertex directed undirected face.+     (Int -> Bool)+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> [Int]+  -> ST s ()+legalizeEdgesPinned targetFaceProtected mutable operation initial = do+  top <- seedGenericEdges operation 0 initial+  (flips, maxDepth) <-+    drainLegalization+      @'ProbeOff+      mutable+      operation+      top+      GenericCandidates+      (SeamPreserving targetFaceProtected)   addCounter operation CounterEdgeFlips flips   maxCounter operation CounterLegalizationMaxStack maxDepth 
src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Normalize.hs view
@@ -145,19 +145,32 @@   -> LegalizationLaw   -> ST s (LegalizationDrain s (ProbeCounter p)) drainLegalizationInArena topology (LegalizationArena initialArena) guarded seededTop discipline law = do-  let -- Everything the cavity fan did not create is pinned. The border loop's-      -- legality is already decided by the outside triangle it keeps, and-      -- testing it against a neighbourhood that is still inverted could only-      -- produce a spurious verdict. 'ValidMesh' pins nothing, which is the-      -- floor no undirected index falls below.+  let -- The cavity-repair floor pins the fan border. Ordinary legalization+      -- pins nothing; seam legalization instead uses its incident-face+      -- selector below, so both copied and appended pairs obey the same+      -- protection law.       !floorPair = case law of         ValidMesh -> 0         CavityRepair floorEdge -> floorEdge+        SeamPreserving _ -> 0 -      eligible rawEdge-        | rawEdge `shiftR` 1 < floorPair = pure False-        | guarded = not <$> topologyReadConstraint topology rawEdge-        | otherwise = pure True+      eligible rawEdge = do+        unconstrained <-+          if guarded+            then not <$> topologyReadConstraint topology rawEdge+            else pure True+        if not unconstrained+          then pure False+          else+            case law of+              SeamPreserving targetFaceProtected -> do+                leftFace <- topologyReadFace topology rawEdge+                rightFace <- topologyReadFace topology (reverseIndex rawEdge)+                pure+                  ( not (targetFaceProtected leftFace)+                      && not (targetFaceProtected rightFace)+                  )+              _ -> pure (rawEdge `shiftR` 1 >= floorPair)        loopStar !starVertex !starX !starY !arena !top !maxTop !flips !candidates         | top <= 0 = pure (flips, maxTop, candidates, arena)
src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Subdivide.hs view
@@ -1,7 +1,6 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -9,10 +8,12 @@ module Moonlight.Triangulation.Internal.DcelOperations.Subdivide   ( insertIntoFace   , insertOnEdge+  , splitBoundaryEdgeWithLegalization   ) where -import Control.Monad (unless, when)+import Control.Monad (unless, void, when) import Control.Monad.ST (ST)+import Moonlight.Triangulation.Handles.HandleDefs (UndirectedEdgeId (..)) import Moonlight.Triangulation.Internal.DcelOperations.Legalize (legalizeScratch) import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex) import Moonlight.Triangulation.Internal.Mutable@@ -45,7 +46,7 @@   , writeScratch   ) import Moonlight.Triangulation.Internal.Probe (KnownProbe)-import Moonlight.Triangulation.Internal.Types (BuildError)+import Moonlight.Triangulation.Internal.Types (BuildError (..))  insertIntoFace :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ()) insertIntoFace mutable operation face vertex = do@@ -166,15 +167,73 @@  splitBoundaryEdge :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ()) splitBoundaryEdge mutable operation suppliedEdge vertex = do-  capacity <- ensureCellCapacity mutable 2 1-  case capacity of-    Left obstruction -> pure (Left obstruction)-    Right () -> do-      splitBoundaryEdgeWithCapacity @p mutable operation suppliedEdge vertex-      pure (Right ())+  outcome <-+    splitBoundaryEdgeWithLegalization+      (\target targetOperation insertedVertex _ _ ->+         legalizeScratch @p target targetOperation insertedVertex 2+      )+      mutable+      operation+      suppliedEdge+      vertex+  pure (void outcome) -splitBoundaryEdgeWithCapacity :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s ()-splitBoundaryEdgeWithCapacity mutable operation suppliedEdge vertex = do+-- | Split one hull edge and return its two replacement outer edges in the+-- original outer-cycle order.  Topology and constraint inheritance have one+-- owner; callers choose only the lawful legalization boundary.  Ordinary+-- insertion drains the new vertex's scratch star, while a source-preserving+-- seam can pin its immutable face section.+splitBoundaryEdgeWithLegalization+  :: ( MutableDcel s vertex directed undirected face+       -> OperationState s+       -> Int+       -> Int+       -> Int+       -> ST s ()+     )+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Int+  -> Int+  -> ST s (Either BuildError (Int, Int))+splitBoundaryEdgeWithLegalization legalize mutable operation suppliedEdge vertex = do+  suppliedFace <- readFace mutable suppliedEdge+  reverseFace <- readFace mutable (reverseIndex suppliedEdge)+  if suppliedFace /= 0 && reverseFace /= 0+    then+      pure+        ( Left+            ( BoundarySplitRequiresBoundaryEdge+                (UndirectedEdgeId (fromIntegral (suppliedEdge `quot` 2)))+            )+        )+    else do+      capacity <- ensureCellCapacity mutable 2 1+      case capacity of+        Left obstruction -> pure (Left obstruction)+        Right () ->+          Right+            <$> splitBoundaryEdgeWithCapacity+              legalize+              mutable+              operation+              suppliedEdge+              vertex++splitBoundaryEdgeWithCapacity+  :: ( MutableDcel s vertex directed undirected face+       -> OperationState s+       -> Int+       -> Int+       -> Int+       -> ST s ()+     )+  -> MutableDcel s vertex directed undirected face+  -> OperationState s+  -> Int+  -> Int+  -> ST s (Int, Int)+splitBoundaryEdgeWithCapacity legalize mutable operation suppliedEdge vertex = do   protected <- readConstraint mutable suppliedEdge   suppliedFace <- readFace mutable suppliedEdge   let !edge = if suppliedFace == 0 then reverseIndex suppliedEdge else suppliedEdge@@ -221,4 +280,5 @@   writeScratch operation 0 eCA   writeScratch operation 1 eBC   addCounter operation CounterBoundaryEdgeSplits 1-  legalizeScratch @p mutable operation vertex 2+  legalize mutable operation vertex eCA eBC+  pure (eBV, outerEdge)
src-dcel/Moonlight/Triangulation/Internal/Mutable.hs view
@@ -22,6 +22,7 @@   , thawTriangulation   , thawTriangulationDense   , freezeTriangulation+  , freezeTriangulationWithStats   , pointCapacity   , halfEdgeCapacity   , pointCount@@ -444,12 +445,12 @@   mdPointX <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triPointX) source   mdPointY <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triPointY) source   mdVertexOut <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triVertexOut) source-  mdVertexData <- thawBoxedPaged vertexDataBase+  mdVertexData <- thawBoxedPaged shape vertexDataBase   mdNewConnected <- newLocalMutablePaged (vertexCapacity - existingVertices)   mdRecycledNew <- newSTRef IntSet.empty   mdHalfTopology <- maybe (newMutablePaged (4 * halfCapacity)) (thawPagedShaped shape (4 * halfCapacity) . triHalfTopology) source-  mdDirectedData <- thawBoxedPaged directedDataBase-  mdUndirectedData <- thawBoxedPaged undirectedDataBase+  mdDirectedData <- thawBoxedPaged shape directedDataBase+  mdUndirectedData <- thawBoxedPaged shape undirectedDataBase   mdConstraint <- maybe (newMutablePaged (halfCapacity `quot` 2)) (thawPagedShaped shape (halfCapacity `quot` 2) . triConstraint) source   mdFaceEdge <-     case source of@@ -458,7 +459,7 @@         freshFaceEdges <- newLocalMutablePaged faceCapacity         writePaged freshFaceEdges 0 noIndex         pure freshFaceEdges-  mdFaceData <- thawBoxedPaged faceDataBase+  mdFaceData <- thawBoxedPaged shape faceDataBase   mdPointCount <- newSTRef existingVertices   mdPointIndex <- newSTRef pointIndexBase   mdConnectedCount <- newSTRef existingVertices@@ -481,7 +482,19 @@       }  freezeTriangulation :: MutableDcel s vertex directed undirected face -> ST s (Either BuildError (Triangulation mode vertex directed undirected face))-freezeTriangulation MutableDcel+freezeTriangulation mutable =+  fmap (fmap fst) (freezeTriangulationCore (const (pure emptyPublicationStats)) mutable)++freezeTriangulationWithStats+  :: MutableDcel s vertex directed undirected face+  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))+freezeTriangulationWithStats = freezeTriangulationCore readMutableDcelPublicationStats++freezeTriangulationCore+  :: (MutableDcel s vertex directed undirected face -> ST s PublicationStats)+  -> MutableDcel s vertex directed undirected face+  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))+freezeTriangulationCore readPublicationStats mutable@MutableDcel   { mdPointX   , mdPointY   , mdPointIndex@@ -531,8 +544,40 @@           triHalfTopology <- freezePaged (4 * halfEdges) mdHalfTopology           triConstraint <- freezePaged (halfEdges `quot` 2) mdConstraint           triFaceEdge <- freezePaged faces mdFaceEdge+          publicationStats <- readPublicationStats mutable           let triElementDefaults = mdElementDefaults-          pure (Right Triangulation{..})+          pure (Right (Triangulation{triSeamFrontier = Nothing, ..}, publicationStats))++readMutableDcelPublicationStats+  :: MutableDcel s vertex directed undirected face+  -> ST s PublicationStats+readMutableDcelPublicationStats MutableDcel+  { mdPointX+  , mdPointY+  , mdVertexOut+  , mdHalfTopology+  , mdConstraint+  , mdFaceEdge+  , mdVertexData+  , mdDirectedData+  , mdUndirectedData+  , mdFaceData+  } =+  foldl'+    addPublicationStats+    emptyPublicationStats+    <$> sequence+      [ readMutablePagedPublicationStats mdPointX+      , readMutablePagedPublicationStats mdPointY+      , readMutablePagedPublicationStats mdVertexOut+      , readMutablePagedPublicationStats mdHalfTopology+      , readMutablePagedPublicationStats mdConstraint+      , readMutablePagedPublicationStats mdFaceEdge+      , readMutableBoxedPublicationStats mdVertexData+      , readMutableBoxedPublicationStats mdDirectedData+      , readMutableBoxedPublicationStats mdUndirectedData+      , readMutableBoxedPublicationStats mdFaceData+      ]  pointCapacity :: MutableDcel s vertex directed undirected face -> Int pointCapacity = mdPointCapacity
src-dcel/Moonlight/Triangulation/Internal/Representation.hs view
@@ -6,12 +6,18 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RoleAnnotations #-}+{-# LANGUAGE ScopedTypeVariables #-}  -- | The stored representation: the structure-of-arrays mesh, the payload -- traversals that reach its four free parameters, and the records that carry a -- built mesh beside its telemetry. module Moonlight.Triangulation.Internal.Representation   ( Triangulation (..)+  , SeamFrontierIndex (..)+  , prepareSeamFrontierIndex+  , summarizeSeamChart+  , seamQuarterTurn+  , geometryOnlyPublication   , promoteConstrained   , PayloadTraversal   , vertexPayloads@@ -34,8 +40,12 @@   ) where  import Control.DeepSeq (NFData)+import Data.Foldable (toList) import qualified Data.IntSet as IntSet+import Data.List (unfoldr)+import qualified Data.List as List import Data.Primitive.PrimArray (PrimArray)+import qualified Data.Sequence as Seq import Data.Traversable (foldMapDefault) import qualified Data.Vector as V import Data.Word (Word8, Word32)@@ -44,14 +54,21 @@   , UndirectedEdgeId (..)   , VertexId (..)   )-import Moonlight.Triangulation.Internal.BoxedPaged (BoxedPaged, boxedFromVector, boxedToVector)-import Moonlight.Triangulation.Internal.Paged (Paged)+import Moonlight.Triangulation.Internal.BoxedPaged (BoxedPaged, boxedDefaulted, boxedFromVector, boxedToVector)+import Moonlight.Triangulation.Internal.Paged+  ( Paged+  , PublicationStats+  , pagedLength+  , pagedUnsafeIndex+  ) import Moonlight.Triangulation.Internal.PointIndex (PointIndex) import Moonlight.Triangulation.Internal.Types   ( BuildStats   , ConstraintMode (..)   , ElementDefaults (..)   , InsertionDisposition+  , ValidationClosureStats+  , unitElementDefaults   ) import GHC.Generics (Generic) @@ -92,11 +109,211 @@     -- transported with edge rewrites so constraint-only queries need not scan     -- every ordinary Delaunay edge.     triConstraintEdges :: !IntSet.IntSet+  , -- | Derived outer-frontier handles for persistent separated joins. The+    -- geometry/topology planes remain authoritative; this cache is omitted+    -- from equality and serialization and is rebuilt only at an explicit+    -- geometry-only preparation boundary.+    triSeamFrontier :: !(Maybe SeamFrontierIndex)   , triElementDefaults :: !(ElementDefaults directed undirected face)   }   deriving stock (Show, Generic)   deriving anyclass (NFData) +-- | Exact immutable outer cycle plus four deterministic extreme anchors per+-- chart used to seed separated tangents. The sequence is persistent: seam+-- joins compose retained source sections without copying their elements.+data SeamFrontierIndex = SeamFrontierIndex+  { seamFrontierEdges :: !(Seq.Seq Int)+  , seamFrontierXMinimum :: !Double+  , seamFrontierXMaximum :: !Double+  , seamFrontierLowerRightmost :: {-# UNPACK #-} !Int+  , seamFrontierLowerLeftmost :: {-# UNPACK #-} !Int+  , seamFrontierUpperRightmost :: {-# UNPACK #-} !Int+  , seamFrontierUpperLeftmost :: {-# UNPACK #-} !Int+  , -- | The same four extreme anchors in the orientation-preserving+    -- quarter-turn chart @(u,v) = (y,-x)@.  Keeping this alongside the+    -- existing x chart lets a later seam admit a north/south extension+    -- without walking the resident frontier again.+    seamFrontierYMinimum :: !Double+  , seamFrontierYMaximum :: !Double+  , seamFrontierYLowerRightmost :: {-# UNPACK #-} !Int+  , seamFrontierYLowerLeftmost :: {-# UNPACK #-} !Int+  , seamFrontierYUpperRightmost :: {-# UNPACK #-} !Int+  , seamFrontierYUpperLeftmost :: {-# UNPACK #-} !Int+  }+  deriving stock (Eq, Show, Generic)+  deriving anyclass (NFData)++-- | Derive the exact frontier once from an admitted DCEL. This is intentionally+-- an explicit preparation operation: ordinary mutable publication clears the+-- cache instead of silently paying an outer-cycle traversal.+prepareSeamFrontierIndex+  :: Triangulation mode vertex directed undirected face+  -> Maybe SeamFrontierIndex+prepareSeamFrontierIndex triangulation = do+  start <- frontierStart triangulation+  let !directedCount = numDirectedEdges triangulation+      !edges =+        unfoldr+          (frontierStep start directedCount)+          (start, True, directedCount + 1)+      terminal = List.foldl' (\_ edge -> Just edge) Nothing edges+      closesAtStart = do+        lastEdge <- terminal+        pure+          ( fromIntegral+              (pagedUnsafeIndex (triHalfTopology triangulation) (4 * lastEdge + 1))+              == start+          )+  if closesAtStart == Just True+    then summarizeFrontier triangulation (Seq.fromList edges)+    else Nothing+ where+  numDirectedEdges+    :: Triangulation mode vertex directed undirected face+    -> Int+  numDirectedEdges mesh = pagedLength (triHalfTopology mesh) `quot` 4++  frontierStart+    :: Triangulation mode vertex directed undirected face+    -> Maybe Int+  frontierStart mesh =+    let raw = pagedUnsafeIndex (triFaceEdge mesh) 0+     in if raw == (maxBound :: Word32)+          then Nothing+          else Just (fromIntegral raw)++  frontierStep+    :: Int+    -> Int+    -> (Int, Bool, Int)+    -> Maybe (Int, (Int, Bool, Int))+  frontierStep start directedCount (edge, first, remaining)+    | remaining <= 0 = Nothing+    | edge == start && not first = Nothing+    | edge < 0 || edge >= directedCount = Nothing+    | otherwise =+        let next =+              fromIntegral+                (pagedUnsafeIndex (triHalfTopology triangulation) (4 * edge + 1))+         in Just (edge, (next, False, remaining - 1))++summarizeFrontier+  :: Triangulation mode vertex directed undirected face+  -> Seq.Seq Int+  -> Maybe SeamFrontierIndex+summarizeFrontier triangulation edges =+  case fmap (frontierPoint triangulation) (toList edges) of+    [] -> Nothing+    points -> do+      (minimumX, maximumX, lowerRightmost, lowerLeftmost, upperRightmost, upperLeftmost) <-+        summarizeSeamChart id (zip [0 ..] points)+      (minimumY, maximumY, yLowerRightmost, yLowerLeftmost, yUpperRightmost, yUpperLeftmost) <-+        summarizeSeamChart seamQuarterTurn (zip [0 ..] points)+      pure+        SeamFrontierIndex+          { seamFrontierEdges = edges+          , seamFrontierXMinimum = minimumX+          , seamFrontierXMaximum = maximumX+          , seamFrontierLowerRightmost = lowerRightmost+          , seamFrontierLowerLeftmost = lowerLeftmost+          , seamFrontierUpperRightmost = upperRightmost+          , seamFrontierUpperLeftmost = upperLeftmost+          , seamFrontierYMinimum = minimumY+          , seamFrontierYMaximum = maximumY+          , seamFrontierYLowerRightmost = yLowerRightmost+          , seamFrontierYLowerLeftmost = yLowerLeftmost+          , seamFrontierYUpperRightmost = yUpperRightmost+          , seamFrontierYUpperLeftmost = yUpperLeftmost+          }+ where+  frontierPoint+    :: Triangulation mode vertex directed undirected face+    -> Int+    -> (Double, Double)+  frontierPoint mesh rawEdge =+    let rawVertex = pagedUnsafeIndex (triHalfTopology mesh) (4 * rawEdge)+        vertex = fromIntegral rawVertex+     in ( pagedUnsafeIndex (triPointX mesh) vertex+        , pagedUnsafeIndex (triPointY mesh) vertex+        )++-- | Summarize one chart from a bounded set of frontier points.  The index is+-- retained beside the transformed point so the seam can carry the witness+-- through a residual interval without re-reading the source frontier.+summarizeSeamChart+  :: ((Double, Double) -> (Double, Double))+  -> [(Int, (Double, Double))]+  -> Maybe (Double, Double, Int, Int, Int, Int)+summarizeSeamChart transform points =+  case fmap (\(index, point) -> (index, transform point)) points of+    [] -> Nothing+    first : remaining ->+      let (!minimumValue, !maximumValue) =+            List.foldl'+              (\(!minimumSoFar, !maximumSoFar) (_, (u, _)) ->+                 (min minimumSoFar u, max maximumSoFar u))+              (chartU (snd first), chartU (snd first))+              remaining+          choose preference =+            snd+              ( List.foldl'+                  (\(!bestPoint, !bestIndex) (index, candidate) ->+                     if preference bestPoint candidate+                       then (candidate, index)+                       else (bestPoint, bestIndex))+                  (snd first, fst first)+                  remaining+              )+       in Just+            ( minimumValue+            , maximumValue+            , choose preferRightmost+            , choose preferLeftmost+            , choose preferRightmostUpper+            , choose preferLeftmostUpper+            )+ where+  chartU :: (Double, Double) -> Double+  preferRightmost :: (Double, Double) -> (Double, Double) -> Bool+  preferLeftmost :: (Double, Double) -> (Double, Double) -> Bool+  preferRightmostUpper :: (Double, Double) -> (Double, Double) -> Bool+  preferLeftmostUpper :: (Double, Double) -> (Double, Double) -> Bool+  chartU (u, _) = u+  preferRightmost (bestU, bestV) (candidateU, candidateV) =+    candidateU > bestU || (candidateU == bestU && candidateV < bestV)+  preferLeftmost (bestU, bestV) (candidateU, candidateV) =+    candidateU < bestU || (candidateU == bestU && candidateV < bestV)+  preferRightmostUpper (bestU, bestV) (candidateU, candidateV) =+    candidateU > bestU || (candidateU == bestU && candidateV > bestV)+  preferLeftmostUpper (bestU, bestV) (candidateU, candidateV) =+    candidateU < bestU || (candidateU == bestU && candidateV > bestV)++-- | Orientation-preserving quarter turn used by the north/south seam chart.+seamQuarterTurn :: (Double, Double) -> (Double, Double)+seamQuarterTurn (x, y) = (y, -x)++-- | Forget every payload while preparing a geometry-only mesh for persistent+-- extension. Each payload plane is replaced directly with a defaulted,+-- zero-page unit store; no source payload is traversed or densely rebuilt.+-- Coordinates, topology, constraints, counts, and derived point identity+-- remain authoritative, while future elements inherit the unit defaults.+geometryOnlyPublication+  :: Triangulation mode vertex directed undirected face+  -> Triangulation mode () () () ()+geometryOnlyPublication triangulation =+  triangulation+    { triVertexData = boxedDefaulted () (pagedLength (triPointX triangulation))+    , triDirectedData = boxedDefaulted () (pagedLength (triHalfTopology triangulation) `quot` 4)+    , triUndirectedData = boxedDefaulted () (pagedLength (triHalfTopology triangulation) `quot` 8)+    , triFaceData = boxedDefaulted () (pagedLength (triFaceEdge triangulation))+    , triElementDefaults = unitElementDefaults+    , triSeamFrontier =+        case triSeamFrontier triangulation of+          Just frontier -> Just frontier+          Nothing -> prepareSeamFrontierIndex triangulation+    }+ -- The point index is a derived cache and therefore not an observable part of -- the mesh value. Structural equality compares every semantic plane and -- default while deliberately refusing to construct or compare that cache.@@ -126,12 +343,14 @@ promoteConstrained Triangulation{   triPointX, triPointY, triPointIndex, triVertexOut, triVertexData, triHalfTopology,   triDirectedData, triUndirectedData, triFaceEdge, triFaceData,-  triConstraint, triConstraintCount, triConstraintEdges, triElementDefaults+  triConstraint, triConstraintCount, triConstraintEdges, triSeamFrontier,+  triElementDefaults   } =   Triangulation{     triPointX, triPointY, triPointIndex, triVertexOut, triVertexData, triHalfTopology,     triDirectedData, triUndirectedData, triFaceEdge, triFaceData,-    triConstraint, triConstraintCount, triConstraintEdges, triElementDefaults+    triConstraint, triConstraintCount, triConstraintEdges, triSeamFrontier,+    triElementDefaults     }  -- | A traversal of every occurrence of one payload parameter, in the van@@ -380,10 +599,17 @@   { refinementVisitedJoinFaces :: !(V.Vector FaceId)   , refinementVisitedProtectedFaces :: !(V.Vector FaceId)   , refinementCreatedFaces :: !(V.Vector FaceId)+    -- ^ Face slots whose published triangle changed or was appended.+  , refinementFinalPermittedFaces :: !(V.Vector FaceId)+    -- ^ Exact final face lineage admitted by a checked local domain.   , refinementTouchedEdges :: !(V.Vector UndirectedEdgeId)   , refinementRemovedEdges :: !(V.Vector UndirectedEdgeId)   , refinementInterfaceBoundaryReads :: {-# UNPACK #-} !Int   , refinementAttemptedBoundaryCrossings :: {-# UNPACK #-} !Int+  , refinementPublicationStats :: !PublicationStats+    -- ^ Page publication work measured by the transaction owner.+  , refinementValidationClosureStats :: !ValidationClosureStats+    -- ^ Typed support counts emitted by the selected validation descent.   }   deriving stock (Eq, Show, Generic)   deriving anyclass (NFData)
src-dcel/Moonlight/Triangulation/Internal/Types.hs view
@@ -28,6 +28,7 @@   , InsertionDisposition (..)   , BuildStats (..)   , emptyBuildStats+  , ValidationClosureStats (..)   , CoordinateError (..)   , NonFiniteValue (..)   , classifyNonFinite@@ -74,6 +75,19 @@ instance KnownConstraintMode 'Constrained where   constraintModeValue _ = Constrained +-- | Exact support visited by the selected topology-validation descent.  The+-- validator constructs this from its admitted closure sets; consumers do not+-- reconstruct it from a later receipt or a global mesh census.+data ValidationClosureStats = ValidationClosureStats+  { validationClosureFaces :: {-# UNPACK #-} !Int+  , validationClosureDirectedEdges :: {-# UNPACK #-} !Int+  , validationClosureVertices :: {-# UNPACK #-} !Int+  , validationClosureInterfacePairs :: {-# UNPACK #-} !Int+  , validationClosureConstraintPairs :: {-# UNPACK #-} !Int+  }+  deriving stock (Eq, Show, Generic)+  deriving anyclass (NFData)+ -- | Cartesian binary64 point. data Point = Point   { pointX :: !Double@@ -267,6 +281,9 @@   | PointLocationFailed !(Point)   | LocationWalkExhausted !(Point) {-# UNPACK #-} !Int   | RefinementInputTopologyInvalid !InvariantViolation+  | SeamFrontierUnavailable+  | SeamSourceEdgeRequiresFlip !UndirectedEdgeId+  | SeamProtectedSourceFaceChanged !FaceId   | FreshInsertionMatchedExistingVertex !VertexId !VertexId   | DegenerateLineEndpointMissingOutgoing !VertexId   | DegenerateLineEndpointTurnMissing {-# UNPACK #-} !Int@@ -313,6 +330,8 @@   | RefinementMaximumAreaNotPositive {-# UNPACK #-} !Double   | RefinementMaximumRadiusEdgeRatioNotFinite !NonFiniteValue   | RefinementMaximumRadiusEdgeRatioNotPositive {-# UNPACK #-} !Double+  | RefinementMaximumEdgeLengthNotFinite !NonFiniteValue+  | RefinementMaximumEdgeLengthNotPositive {-# UNPACK #-} !Double   | RefinementMinimumAreaExceedsMaximum       {-# UNPACK #-} !Double       {-# UNPACK #-} !Double@@ -324,9 +343,20 @@   | RefinementDomainRequiresConvexHullPreservation   | RefinementDomainRequiresConstraintPreservation   | RefinementDomainForbidsOuterFaceExclusion+  | RefinementDomainRequiresFiniteVertexBudget+  | RefinementSeamBridgeBudgetExceeded+      {-# UNPACK #-} !Int+      {-# UNPACK #-} !Int+  | RefinementSeamBridgeMidpointCollapsed !UndirectedEdgeId+  | BoundarySplitRequiresBoundaryEdge !UndirectedEdgeId   | RefinementDomainWouldCrossInterface !UndirectedEdgeId !FaceId   | RefinementDomainWouldRewriteProtectedFace !FaceId   | RefinementDomainProtectedFaceChanged !FaceId+  | RefinementOversizedEdge+      !FaceId+      !UndirectedEdgeId+      {-# UNPACK #-} !Double+      {-# UNPACK #-} !Double   | CapacityExceeded {-# UNPACK #-} !Int   | HalfEdgeCapacityExceeded {-# UNPACK #-} !Int {-# UNPACK #-} !Int   | FaceCapacityExceeded {-# UNPACK #-} !Int {-# UNPACK #-} !Int@@ -380,6 +410,7 @@   , refineMinArea :: !(Maybe Double)   , refineMaxArea :: !(Maybe Double)   , refineMaxRadiusEdgeRatio :: !(Maybe Double)+  , refineMaxEdgeLength :: !(Maybe Double)   , refinePreserveConvexHull :: !Bool   , refineKeepConstraintEdges :: !Bool   , refineExcludeOuterFaces :: !Bool@@ -387,7 +418,7 @@   deriving stock (Eq, Show, Generic)   deriving anyclass (NFData) --- | Conservative refinement defaults with no explicit area bounds.+-- | Conservative refinement defaults with no explicit area or edge bounds. defaultRefinementParameters :: RefinementParameters defaultRefinementParameters =   RefinementParameters@@ -395,6 +426,7 @@     , refineMinArea = Nothing     , refineMaxArea = Nothing     , refineMaxRadiusEdgeRatio = Just 1+    , refineMaxEdgeLength = Nothing     , refinePreserveConvexHull = True     , refineKeepConstraintEdges = False     , refineExcludeOuterFaces = False
src-dcel/Moonlight/Triangulation/Math.hs view
@@ -23,6 +23,7 @@   , triangleArea   , triangleRadiusEdgeRatio   , triangleRadiusEdgeRatioSquaredWithArea+  , triangleRadiusEdgeRatioSquaredWithAreaAndSides   , circumcenter   , barycentricCoordinates   , inDiametralCircle@@ -302,14 +303,21 @@ -- compare an infinity in its place. triangleRadiusEdgeRatioSquaredWithArea   :: Double -> Point -> Point -> Point -> Double-triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2+triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2 =+  triangleRadiusEdgeRatioSquaredWithAreaAndSides+    area+    (squaredDistance p0 p1)+    (squaredDistance p1 p2)+    (squaredDistance p2 p0)++-- | The same squared ratio from side squares already paid for by a caller.+triangleRadiusEdgeRatioSquaredWithAreaAndSides+  :: Double -> Double -> Double -> Double -> Double+triangleRadiusEdgeRatioSquaredWithAreaAndSides area side01 side12 side20   | area <= 0 || shortest <= 0 = 1 / 0   | not (isFinite ratio) = 1 / 0   | otherwise = ratio  where-  !side01 = squaredDistance p0 p1-  !side12 = squaredDistance p1 p2-  !side20 = squaredDistance p2 p0   !shortest = min side01 (min side12 side20)   !otherProduct     | side01 <= side12 && side01 <= side20 = side12 * side20
src-dcel/Moonlight/Triangulation/Types.hs view
@@ -34,6 +34,7 @@   , NearestStats (..)   , RefinementParameters (..)   , defaultRefinementParameters+  , ValidationClosureStats (..)   , RefinementReceipt (..)   , RefinementDomainResult (..)   , RefinementResult (..)
src-dcel/Moonlight/Triangulation/Validation.hs view
@@ -4,6 +4,9 @@ -- built by any route. module Moonlight.Triangulation.Validation   ( validateTopology+  , ValidationClosureStats (..)+  , validateTopologyClosure+  , validateTopologyClosureWithStats   , validateDelaunay   , validateTriangulation   , triangulationIsValid@@ -198,7 +201,6 @@       , actualOrigin /= vertex       ] -   eulerViolations     | not (null cardinalityViolations) || verticesCount < 2 = []     | numInnerFaces triangulation == 0 =@@ -211,6 +213,258 @@         ]    where     eulerCharacteristic = verticesCount - edgeCount + facesCount++-- | Apply the same DCEL edge, face, vertex, and orientation laws as+-- 'validateTopology', but only to a certified local closure.  The caller+-- supplies the admitted inner faces and the interface pairs; the collar faces+-- on the other side of those pairs are included here so a protected source+-- cannot be changed behind the local transaction.  Cardinality and Euler+-- observations remain global scalar invariants and are deliberately not+-- rebuilt from the resident mesh.+validateTopologyClosure+  :: IntSet.IntSet+  -> IntSet.IntSet+  -> Triangulation mode vertex directed undirected face+  -> [InvariantViolation]+validateTopologyClosure admittedFaces interfacePairs triangulation =+  snd (validateTopologyClosureWithStats admittedFaces interfacePairs triangulation)++validateTopologyClosureWithStats+  :: IntSet.IntSet+  -> IntSet.IntSet+  -> Triangulation mode vertex directed undirected face+  -> (ValidationClosureStats, [InvariantViolation])+validateTopologyClosureWithStats admittedFaces interfacePairs triangulation =+  ( ValidationClosureStats+      { validationClosureFaces = IntSet.size selectedFaces+      , validationClosureDirectedEdges = IntSet.size selectedEdges+      , validationClosureVertices = IntSet.size selectedVertices+      , validationClosureInterfacePairs = IntSet.size interfacePairs+      , validationClosureConstraintPairs = IntSet.size selectedPairs+      }+  , rangeViolations ++ edgeViolations ++ faceViolations ++ vertexViolations ++ orientationViolations ++ constraintViolations+  )+ where+  verticesCount = numVertices triangulation+  halfCount = numDirectedEdges triangulation+  facesCount = numFaces triangulation++  collarFaces =+    IntSet.fromList+      [ rawFace+      | rawPair <- IntSet.toAscList interfacePairs+      , rawPair >= 0+      , rawPair < numUndirectedEdges triangulation+      , let edge = UndirectedEdgeId (fromIntegral rawPair)+      , let (forward, backward) = directedPair edge+      , rawFace <- fmap faceRaw [incidentFace triangulation forward, incidentFace triangulation backward]+      , rawFace > 0+      ]+  selectedFaces = IntSet.union admittedFaces collarFaces++  selectedFaceEdges =+    IntSet.fromList+      [ rawEdge+      | rawFace <- IntSet.toAscList selectedFaces+      , edge <- faceEdgesBounded (FaceId (fromIntegral rawFace))+      , rawEdge <- [edgeRaw edge, edgeRaw (reverseEdge edge)]+      ]+  selectedInterfaceEdges =+    IntSet.fromList+      [ rawEdge+      | rawPair <- IntSet.toAscList interfacePairs+      , rawPair >= 0+      , rawPair <= (maxBound - 1) `quot` 2+      , rawEdge <- [2 * rawPair, 2 * rawPair + 1]+      ]+  selectedEdges = IntSet.union selectedFaceEdges selectedInterfaceEdges+  selectedVertices =+    IntSet.fromList+      [ rawVertex+      | rawEdge <- IntSet.toAscList selectedEdges+      , rawEdge >= 0+      , rawEdge < halfCount+      , edgeRaw (reverseEdge (DirectedEdgeId (fromIntegral rawEdge))) < halfCount+      , rawVertex <-+          [ vertexRaw (origin triangulation (DirectedEdgeId (fromIntegral rawEdge)))+          , vertexRaw (destination triangulation (DirectedEdgeId (fromIntegral rawEdge)))+          ]+      , rawVertex < verticesCount+      ]++  rangeViolations =+    [ EdgeOriginOutOfRange edge vertex verticesCount+    | rawEdge <- IntSet.toAscList selectedEdges+    , rawEdge >= 0+    , let edge = DirectedEdgeId (fromIntegral rawEdge)+    , rawEdge < halfCount+    , let vertex = origin triangulation edge+    , vertexRaw vertex >= verticesCount+    ]+      ++ [ EdgeNextOutOfRange edge nextEdge halfCount+         | rawEdge <- IntSet.toAscList selectedEdges+         , rawEdge >= 0+         , rawEdge < halfCount+         , let edge = DirectedEdgeId (fromIntegral rawEdge)+         , let nextEdge = next triangulation edge+         , edgeRaw nextEdge >= halfCount+         ]+      ++ [ EdgePreviousOutOfRange edge previousEdge halfCount+         | rawEdge <- IntSet.toAscList selectedEdges+         , rawEdge >= 0+         , rawEdge < halfCount+         , let edge = DirectedEdgeId (fromIntegral rawEdge)+         , let previousEdge = previous triangulation edge+         , edgeRaw previousEdge >= halfCount+         ]+      ++ [ EdgeFaceOutOfRange edge face facesCount+         | rawEdge <- IntSet.toAscList selectedEdges+         , rawEdge >= 0+         , rawEdge < halfCount+         , let edge = DirectedEdgeId (fromIntegral rawEdge)+         , let face = incidentFace triangulation edge+         , faceRaw face >= facesCount+         ]+      ++ [ FaceAdjacentOutOfRange face edge halfCount+         | rawFace <- IntSet.toAscList selectedFaces+         , rawFace > 0+         , rawFace < facesCount+         , let face = FaceId (fromIntegral rawFace)+         , Just edge <- [adjacentEdge triangulation face]+         , edgeRaw edge >= halfCount+         ]++  edgeViolations =+    concatMap validateEdge (IntSet.toAscList selectedEdges)++  validateEdge rawEdge+    | rawEdge < 0 || rawEdge >= halfCount = []+    | otherwise =+        let edge = DirectedEdgeId (fromIntegral rawEdge)+            nextEdge = next triangulation edge+            previousEdge = previous triangulation edge+            twinEdge = reverseEdge edge+            nextValid = edgeRaw nextEdge < halfCount+            previousValid = edgeRaw previousEdge < halfCount+            nextNextValid = nextValid && edgeRaw (next triangulation nextEdge) < halfCount+            innerCycle =+              if incidentFace triangulation edge /= outerFace && nextValid && previousValid && nextNextValid+                then [InnerFaceNotTriangularAtEdge edge | next triangulation (next triangulation nextEdge) /= edge]+                else []+         in [ EdgeNextPreviousMismatch edge nextEdge+            | nextValid && previous triangulation nextEdge /= edge+            ]+              ++ [ EdgePreviousNextMismatch edge previousEdge+                 | previousValid && next triangulation previousEdge /= edge+                 ]+              ++ [EdgeDoubleReversalMismatch edge | reverseEdge twinEdge /= edge]+              ++ [EdgeSelfLinkedNext edge | nextEdge == edge && halfCount > 2]+              ++ [EdgeSelfLinkedPrevious edge | previousEdge == edge && halfCount > 2]+              ++ innerCycle++  faceViolations = concatMap validateFace (IntSet.toAscList selectedFaces)++  validateFace rawFace+    | rawFace <= 0 || rawFace >= facesCount = []+    | otherwise =+      let face = FaceId (fromIntegral rawFace)+       in case adjacentEdge triangulation face of+          Nothing -> [FaceMissingAdjacentEdge face]+          Just edge+            | edgeRaw edge >= halfCount -> []+            | otherwise ->+                let representedFace = incidentFace triangulation edge+                    (faceEdges, faceVertices') = triangleEdgesAndVertices face+                 in [FaceRepresentativeMismatch face edge representedFace | representedFace /= face]+                      ++ [ InnerFaceVertexCardinalityMismatch face (length faceVertices') (length (nub faceVertices'))+                         | rawFace > 0+                         , length faceVertices' /= 3 || length (nub faceVertices') /= 3+                         ]+                      ++ [InnerFaceNotTriangularAtEdge edge+                         | rawFace > 0+                         , not (triangleClosed faceEdges)+                         ]++  triangleEdgesAndVertices face =+    let edges = faceEdgesBounded face+        vertices' = fmap (vertexRaw . origin triangulation) edges+     in (edges, vertices')++  triangleClosed edges =+    case edges of+      [first, _, third] -> next triangulation third == first+      _ -> False++  vertexViolations = concatMap validateVertex (IntSet.toAscList selectedVertices)++  validateVertex rawVertex =+    let vertex = VertexId (fromIntegral rawVertex)+     in case vertexOutEdge triangulation vertex of+          Nothing+            | verticesCount > 1 -> [ConnectedVertexMissingOutgoing vertex]+            | otherwise -> []+          Just edge+            | edgeRaw edge >= halfCount ->+                [VertexOutgoingOutOfRange vertex edge halfCount]+            | otherwise ->+                [ VertexOutgoingOriginMismatch vertex edge actualOrigin+                | let actualOrigin = origin triangulation edge+                , actualOrigin /= vertex+                ]++  orientationViolations =+    [ InnerFaceNotCounterClockwise face+    | rawFace <- IntSet.toAscList selectedFaces+    , rawFace > 0+    , let face = FaceId (fromIntegral rawFace)+    , let (_, vertices') = triangleEdgesAndVertices face+    , all (< verticesCount) vertices'+    , [first, second, third] <- [fmap (VertexId . fromIntegral) vertices']+    , orient2d (vertexPoint triangulation first) (vertexPoint triangulation second) (vertexPoint triangulation third) /= GT+    ]++  constraintViolations =+    [ NonCanonicalConstraintFlag edge flag+    | rawPair <- IntSet.toAscList selectedPairs+    , rawPair >= 0+    , let edge = UndirectedEdgeId (fromIntegral rawPair)+    , rawPair < numUndirectedEdges triangulation+    , let flag = pagedUnsafeIndex (triConstraint triangulation) rawPair+    , flag /= 0 && flag /= 1+    ]+      ++ [ CachedConstraintIndexMismatch+         | rawPair <- IntSet.toAscList selectedPairs+         , rawPair >= 0+         , rawPair < numUndirectedEdges triangulation+         , let flag = pagedUnsafeIndex (triConstraint triangulation) rawPair+         , (flag == 1) /= IntSet.member rawPair (triConstraintEdges triangulation)+         ]++  selectedPairs = IntSet.fromList [rawEdge `quot` 2 | rawEdge <- IntSet.toAscList selectedEdges]++  faceEdgesBounded face =+    case face of+      FaceId raw+        | toInteger raw <= 0 || toInteger raw >= toInteger facesCount -> []+      _ -> adjacentEdges+   where+    adjacentEdges = case adjacentEdge triangulation face of+      Nothing -> []+      Just start+        | edgeRaw start >= halfCount -> []+        | otherwise ->+            let second = next triangulation start+             in if edgeRaw second >= halfCount+                  then [start]+                  else+                    let third = next triangulation second+                     in if edgeRaw third >= halfCount+                          then [start, second]+                          else [start, second, third]++  edgeRaw (DirectedEdgeId raw) = fromIntegral raw+  vertexRaw (VertexId raw) = fromIntegral raw+  faceRaw (FaceId raw) = fromIntegral raw  -- | Every edge whose circumcircle is not empty. validateDelaunay :: Triangulation mode vertex directed undirected face -> [InvariantViolation]
src-ffi/Moonlight/Triangulation/Foreign/ABI.hs view
@@ -1039,6 +1039,8 @@     T.RefinementMaximumAreaNotPositive value -> firstValue 35 value     T.RefinementMaximumRadiusEdgeRatioNotFinite value -> nonFinite 36 value     T.RefinementMaximumRadiusEdgeRatioNotPositive value -> firstValue 37 value+    T.RefinementMaximumEdgeLengthNotFinite value -> nonFinite 60 value+    T.RefinementMaximumEdgeLengthNotPositive value -> firstValue 61 value     T.RefinementMinimumAreaExceedsMaximum minimumArea maximumArea -> values 38 minimumArea maximumArea     T.RefinementSeedFaceNotActive face count -> indices 39 (T.unFaceId face) count     T.RefinementDomainInterfaceEdgeNotActive edge count -> indices 40 (T.unUndirectedEdgeId edge) count@@ -1051,12 +1053,24 @@     T.RefinementDomainWouldCrossInterface edge face -> indices 47 (T.unUndirectedEdgeId edge) (T.unFaceId face)     T.RefinementDomainWouldRewriteProtectedFace face -> firstIndex 48 (T.unFaceId face)     T.RefinementDomainProtectedFaceChanged face -> firstIndex 49 (T.unFaceId face)+    T.RefinementOversizedEdge face edge actual bound ->+      (indices 62 (T.unFaceId face) (T.unUndirectedEdgeId edge))+        { obstructionFirstValue = actual+        , obstructionSecondValue = bound+        }     T.CapacityExceeded count -> firstIndex 50 count     T.HalfEdgeCapacityExceeded requested capacity -> indices 51 requested capacity     T.FaceCapacityExceeded requested capacity -> indices 52 requested capacity     T.PayloadStorageFailure _ -> codeOnly 53     T.CoordinatePayloadCountMismatch coordinates payloads -> indices 54 coordinates payloads     T.CircleSweepRequiresDenseStorage -> codeOnly 55+    T.SeamFrontierUnavailable -> codeOnly 56+    T.RefinementDomainRequiresFiniteVertexBudget -> codeOnly 57+    T.SeamSourceEdgeRequiresFlip edge -> firstIndex 58 (T.unUndirectedEdgeId edge)+    T.SeamProtectedSourceFaceChanged face -> firstIndex 59 (T.unFaceId face)+    T.RefinementSeamBridgeBudgetExceeded required available -> indices 63 required available+    T.RefinementSeamBridgeMidpointCollapsed edge -> firstIndex 64 (T.unUndirectedEdgeId edge)+    T.BoundarySplitRequiresBoundaryEdge edge -> firstIndex 65 (T.unUndirectedEdgeId edge)   )     {obstructionMessage = show failure}  where
src-public/Moonlight/Triangulation.hs view
@@ -60,12 +60,15 @@   , buildTriangulation   , buildInputVertices   , BuildError (..)+  , PublicationStats (..)+  , ValidationClosureStats (..)   , CoordinateError (..)   , NonFiniteValue (..)      -- * The annotation functor — payloads annotate geometry, never author it;     -- the mesh is the same mesh before and after   , mapVertices+  , geometryOnlyPublication   , vertexData   , setVertexData @@ -89,6 +92,7 @@   , fromDelaunay   , constraintEdges   , isConstraintEdge+  , boundedRegionFaces   , CdtError (..)   , CorridorObstruction (..)   , CanonicalSegment@@ -105,7 +109,7 @@   , ConstrainedExtensionResult   , constrainedExtensionConstraintBatch   , constrainedExtensionBuildStats-  , ConstrainedSeamSource (..)+  , ConstrainedSeamSide (..)   , ConstrainedSeamFaceEvidence   , constrainedSeamSourceFace   , constrainedSeamTargetFace@@ -117,13 +121,15 @@   , constrainedSeamConstraintRecovery   , ConstrainedSeamResult   , constrainedSeamResultTriangulation-  , constrainedSeamLeftFaceEvidence+  , constrainedSeamLeftFaceCount   , constrainedSeamRightFaceEvidence-  , constrainedSeamNewFaces-  , constrainedSeamLeftConstraintEvidence+  , constrainedSeamJoinFaces+  , constrainedSeamLeftConstraintCount   , constrainedSeamRightConstraintEvidence   , constrainedSeamConstraintStats   , constrainedSeamBuildStats+  , constrainedSeamPublicationStats+  , constrainedSeamCachedFrontierPointReads   , constraintSegments   , unionConstrainedWith   , unionConstrained@@ -147,10 +153,13 @@   , refinementVisitedJoinFaces   , refinementVisitedProtectedFaces   , refinementCreatedFaces+  , refinementFinalPermittedFaces   , refinementTouchedEdges   , refinementRemovedEdges   , refinementInterfaceBoundaryReads   , refinementAttemptedBoundaryCrossings+  , refinementPublicationStats+  , refinementValidationClosureStats   , refinementAddedVertices   , refinementExcludedFaces   , refinementComplete@@ -183,6 +192,7 @@   , interpolateNearest   , interpolateBarycentric   , LocationHint (..)+  , floodFillFaces   , facesAtEvenBarrierDepth      -- ** Face regions and alpha filtration@@ -191,6 +201,7 @@   , BoundaryOrientation (..)   , BoundaryLoop   , boundaryLoopOrientation+  , boundaryLoopResidentVertices   , boundaryLoopVertices   , RegionBoundary   , regionBoundaryOuterLoop@@ -199,6 +210,7 @@   , faceComponents   , componentBoundaryLoops   , componentBoundary+  , componentBoundaryForFaces   , RadiusSquared   , RadiusSquaredError (..)   , mkRadiusSquared@@ -399,12 +411,15 @@   , FaceComponent   , RegionBoundary   , boundaryLoopOrientation+  , boundaryLoopResidentVertices   , boundaryLoopVertices   , componentBoundary   , componentBoundaryLoops+  , componentBoundaryForFaces   , faceComponentFaces   , faceComponents   , facesAtEvenBarrierDepth+  , floodFillFaces   , regionBoundaryHoleLoops   , regionBoundaryOuterLoop   )@@ -444,6 +459,7 @@   , vertices   ) import Moonlight.Triangulation.Internal.Canonical (canonicalize)+import Moonlight.Triangulation.Cdt (boundedRegionFaces) import Moonlight.Triangulation.Internal.Cdt.Build   ( constrainedDelaunay   , fromDelaunay@@ -454,10 +470,10 @@   , CdtError (..)   , ConstrainedUnionError (..)   , ConstrainedExtensionResult+  , ConstrainedSeamSide (..)   , ConstrainedSeamConstraintEvidence   , ConstrainedSeamFaceEvidence   , ConstrainedSeamResult-  , ConstrainedSeamSource (..)   , ConstraintBatchResult   , ConstraintConflict   , CorridorObstruction (..)@@ -470,12 +486,14 @@   , constrainedSeamConstraintRecovery   , constrainedSeamConstraintSegment   , constrainedSeamConstraintStats+  , constrainedSeamCachedFrontierPointReads   , constrainedSeamFaceFirstPoint   , constrainedSeamFaceSecondPoint   , constrainedSeamFaceThirdPoint-  , constrainedSeamLeftConstraintEvidence-  , constrainedSeamLeftFaceEvidence-  , constrainedSeamNewFaces+  , constrainedSeamLeftConstraintCount+  , constrainedSeamLeftFaceCount+  , constrainedSeamJoinFaces+  , constrainedSeamPublicationStats   , constrainedSeamResultTriangulation   , constrainedSeamRightConstraintEvidence   , constrainedSeamRightFaceEvidence@@ -493,6 +511,7 @@   , unionConstrained   , unionConstrainedWith   )+import Moonlight.Triangulation.Internal.Paged (PublicationStats (..)) import Moonlight.Triangulation.Internal.ExactRational   ( ExactArithmeticError (..)   , ExactRational@@ -510,6 +529,9 @@   , Triangulation   , buildInputVertices   , buildTriangulation+  , geometryOnlyPublication+  , refinementPublicationStats+  , refinementValidationClosureStats   , refinementDomainReceipt   , refinementDomainResult   , refinedTriangulation@@ -518,6 +540,7 @@   , refinementAddedVertices   , refinementComplete   , refinementCreatedFaces+  , refinementFinalPermittedFaces   , refinementExcludedFaces   , refinementRemovedEdges   , refinementTouchedEdges@@ -540,6 +563,7 @@   , RadiusSquared   , RadiusSquaredError (..)   , RefinementParameters (..)+  , ValidationClosureStats (..)   , SiteRelation (..)   , defaultRefinementParameters   , mkRadiusSquared
src-serialize/Moonlight/Triangulation/Serialization.hs view
@@ -343,6 +343,7 @@         , triConstraint = fromVector 0 constraints         , triConstraintCount = cachedConstraintCount         , triConstraintEdges = constraintEdgeIndex+        , triSeamFrontier = Nothing         , triElementDefaults = defaults         } 
test/algebra/Moonlight/Triangulation/AlgebraFixtures.hs view
@@ -52,6 +52,7 @@   , delaunay   , delaunayGeometry   , exactLoop+  , geometryOnlyPublication   , mapVertices   , numFaces   , numUndirectedEdges@@ -171,7 +172,8 @@  meshOf :: String -> [Point] -> IO Mesh meshOf label points =-  requireRight ("build geometry " <> label) (delaunayGeometry (V.fromList points))+  geometryOnlyPublication+    <$> requireRight ("build geometry " <> label) (delaunayGeometry (V.fromList points))  -- | Build through the annotated entrance when a law deliberately needs the -- physical numbering induced by input order. Geometry-only construction is
test/native/Moonlight/Triangulation/NativeSpec.hs view
@@ -20,3322 +20,4207 @@ import Control.Monad.ST (runST, stToIO) import GHC.Float (castDoubleToWord64) import Data.Foldable (toList, traverse_)-import qualified Data.List as List-import Data.List.NonEmpty (NonEmpty (..))-import Data.Maybe (isJust)-import qualified Data.Map.Strict as Map-import qualified Data.Set as Set-import Data.List (sort)-import qualified Data.Vector as V-import Data.Primitive.PrimArray (indexPrimArray, primArrayFromList, sizeofPrimArray)-import qualified Data.Vector.Unboxed as U-import qualified Data.Vector.Unboxed.Mutable as MUV-import Data.Word (Word32, Word64)-import Moonlight.Triangulation.Alpha-import Moonlight.Triangulation.BulkLoad-import Moonlight.Triangulation.Cdt-import Moonlight.Triangulation.Dcel-import qualified Moonlight.Triangulation.Dcel as Dcel-import Moonlight.Triangulation.FloodFillIterator-import Moonlight.Triangulation.Handles.HandleDefs-import Moonlight.Triangulation.Handles.Iterators.FixedIterators-import Moonlight.Triangulation.HintGenerator-import Moonlight.Triangulation.Interpolation-import Moonlight.Triangulation.IntersectionIterator-import Moonlight.Triangulation.Math-import Moonlight.Triangulation.Payload-import Moonlight.Triangulation.FilteredPredicateOptimizationSpec-  ( assertFilteredPredicatesSkipExactOracle-  )-import Moonlight.Triangulation.PointLocation-import Moonlight.Triangulation.Refinement-import Moonlight.Triangulation.Removal-import Moonlight.Triangulation.Scalar-import Moonlight.Triangulation.Session-import Moonlight.Triangulation.Types-import Moonlight.Triangulation.Validation-import Moonlight.Triangulation.Voronoi-import Moonlight.Triangulation.Handles.Dynamic qualified as Dynamic-import Moonlight.Triangulation.Handles.Iterators.DynamicIterators qualified as DynamicIterators-import Moonlight.Triangulation.Internal.Canonical (canonicalize)-import Moonlight.Triangulation.Internal.Paged (Paged, fromVector, toVector)-import Moonlight.Triangulation.Internal.PointIndex-  ( MutablePointIndexUpdate (..)-  , lookupMutablePoint-  , newMutablePointIndex-  , relocateMutablePoint-  , removeMutablePoint-  , seedMutablePointIndex-  )-import Moonlight.Triangulation.Internal.Representation qualified as Internal-import Moonlight.Triangulation.Voronoi.Handles qualified as VoronoiDynamic-import GHC.Generics (Generic)-import Support (assertEqual, assertValid, requireQueryPoint, requireRight)--tests :: IO ()-tests = do-  testConstructors-  testPredicates-  testParaboloidLift-  assertFilteredPredicatesSkipExactOracle-  testValidationRejectsCorruptedMeshes-  testRefinementCompletionIsAFixpoint-  testScalarFormat-  testWideDistanceNearestNeighbor-  testIncrementalLocationDescent-  testPersistentInsertionReusesFrozenLocation-  testCircleSweepBulkLoad-  testMixedEditSession-  testBulkRemovalAgreement-  testMutablePointIndexWraparoundBackshift-  testBatchIdentityIndexBatchToEmpty-  testBatchIdentityIndexToSingletonActive-  testBulkIdentityIndexDoesNotEscapeRemovalBatch-  testHierarchyRemovalAgreement-  testHandlesAndFiniteDcel-  testDegenerateConstruction-  testPersistentLocalUpdates-  testGenericPayloads-  testPayloadMaps-  testPayloadTraversals-  testRewritePayloadIdentity-  testPointLocationAndHints-  testHierarchyNestingLaw-  testSibsonInterpolation-  testVoronoiDual-  testRemoval-  testConstrainedDelaunay-  testAnnotatedConstrainedUnion-  testAsymmetricConstrainedExtension-  testLargeAsymmetricConstrainedExtension-  testSeparatedConstrainedSeam-  testConstrainedRefinement-  testCheckedLocalRefinement-  testRepeatedBoundaryAdjacentRefinement-  testFaceComponentsAndBoundaries-  testAlphaFaceFiltration-  testTraversal-  testRandomizedConstruction-  testErrors-  putStrLn "all native core tests passed"--data SampleVertex = SampleVertex-  { samplePosition :: !(Point)-  , sampleLabel :: !Int-  }-  deriving stock (Eq, Show, Generic)-  deriving anyclass (NFData)--instance HasPosition SampleVertex where-  position = samplePosition---testConstructors :: IO ()-testConstructors = do-  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)-  originQuery <- requireQueryPoint "empty location query" (Point 0 0)-  assertValid "empty" vacant-  assertEqual "empty vertex count" 0 (numVertices vacant)-  assertEqual "empty directed edge count" 0 (numDirectedEdges vacant)-  assertEqual "empty face count" 1 (numFaces vacant)-  assertEqual "empty locates nowhere" EmptyTriangulation (locatePoint vacant originQuery)-  built <- requirePointBuild "clear source" [Point 0 0, Point 1 0, Point 0 1]-  assertEqual "clear returns the origin" vacant (clear (buildTriangulation built))--type NativeMesh = Triangulation 'Unconstrained (Point) () () ()---- | A named corruption of a mesh that was valid one line earlier, paired with--- the violation it must provoke. Validation that has never been made to fail is--- evidence only that it ran.-data Corruption = Corruption-  { corruptionName :: String-  , corruptMesh :: NativeMesh -> NativeMesh-  , provokes :: InvariantViolation -> Bool-  }---- Sheared so that no four sites are cocircular: on a square grid the diagonal--- of every cell is a free choice, and a fixture that admits two answers cannot--- witness a wrong one.-corruptionFixture :: [Point]-corruptionFixture =-  [ Point (fromIntegral column + 0.25 * fromIntegral row) (1.3 * fromIntegral row)-  | column <- [0 .. 3 :: Int]-  , row <- [0 .. 3 :: Int]-  ]--rewritePlane :: (U.Unbox a, Num a) => (U.Vector a -> U.Vector a) -> Paged a -> Paged a-rewritePlane edit = fromVector 0 . edit . toVector--slot :: U.Unbox a => Int -> a -> U.Vector a -> U.Vector a-slot at value = (U.// [(at, value)])---- Past the end of every plane in the fixture, and far from the sentinels the--- packed representation reserves for absence.-beyond :: NativeMesh -> Word32-beyond mesh = fromIntegral (numDirectedEdges mesh + numVertices mesh + 64)--onTopology :: (NativeMesh -> U.Vector Word32 -> U.Vector Word32) -> NativeMesh -> NativeMesh-onTopology edit mesh =-  mesh {Internal.triHalfTopology = rewritePlane (edit mesh) (Internal.triHalfTopology mesh)}---- The half-edge plane has stride four — origin, next, previous, face — so the--- first four entries below are one surgery distinguished only by which field--- the out-of-range index lands on.-structuralCorruptions :: [Corruption]-structuralCorruptions =-  [ Corruption-      "edge origin names an absent vertex"-      (onTopology (slot 0 . beyond))-      (\case EdgeOriginOutOfRange {} -> True; _ -> False)-  , Corruption-      "edge next names an absent edge"-      (onTopology (slot 1 . beyond))-      (\case EdgeNextOutOfRange {} -> True; _ -> False)-  , Corruption-      "edge previous names an absent edge"-      (onTopology (slot 2 . beyond))-      (\case EdgePreviousOutOfRange {} -> True; _ -> False)-  , Corruption-      "edge face names an absent face"-      (onTopology (slot 3 . beyond))-      (\case EdgeFaceOutOfRange {} -> True; _ -> False)-  , Corruption-      "vertex outgoing names an absent edge"-      ( \mesh ->-          mesh-            { Internal.triVertexOut =-                rewritePlane (slot 0 (beyond mesh)) (Internal.triVertexOut mesh)-            }-      )-      (\case VertexOutgoingOutOfRange {} -> True; _ -> False)-  , Corruption-      "a coordinate plane is one short"-      (\mesh -> mesh {Internal.triPointX = rewritePlane U.init (Internal.triPointX mesh)})-      (\case CoordinatePlaneLengthMismatch {} -> True; _ -> False)-  , Corruption-      "every inner face wound clockwise"-      (\mesh -> mesh {Internal.triPointY = rewritePlane (U.map negate) (Internal.triPointY mesh)})-      (\case InnerFaceNotCounterClockwise {} -> True; _ -> False)-  ]---- Structural corruption is caught before geometry is read, so the empty-circle--- law needs a mesh that stays well formed and merely stops being Delaunay.-delaunayCorruptions :: [Corruption]-delaunayCorruptions =-  [ Corruption-      "one site dragged through its neighbours' circumcircles"-      (\mesh -> mesh {Internal.triPointX = rewritePlane (slot 5 40) (Internal.triPointX mesh)})-      (\case LocallyIllegalDelaunayEdge {} -> True; _ -> False)-  ]---- A surgery that changed nothing would report the oracle as unarmed when in--- truth it was never asked anything, so the mutant must differ before its--- rejection means a thing.-assertRejects-  :: String -> (NativeMesh -> [InvariantViolation]) -> NativeMesh -> Corruption -> IO ()-assertRejects oracle check pristine corruption = do-  let mutant = corruptMesh corruption pristine-      label = oracle <> " / " <> corruptionName corruption-      reported = check mutant-  when (mutant == pristine) $ fail (label <> ": the surgery changed nothing")-  unless (any (provokes corruption) reported) $-    fail (label <> ": admitted, reporting " <> show reported)--testValidationRejectsCorruptedMeshes :: IO ()-testValidationRejectsCorruptedMeshes = do-  built <- requirePointBuild "corruption fixture" corruptionFixture-  let pristine = buildTriangulation built-  assertValid "corruption fixture" pristine-  mapM_ (assertRejects "topology" validateTopology pristine) structuralCorruptions-  mapM_ (assertRejects "delaunay" validateDelaunay pristine) delaunayCorruptions---- Twin half-edges are arithmetic complements, so a crossing's undirected--- identity is its handle halved. Comparing cardinality alone would admit a--- traversal that returned the right number of the wrong crossings.-crossingIdentity :: Intersection -> Either Int Int-crossingIdentity = \case-  EdgeIntersection edge -> Right (fromIntegral (unDirectedEdgeId edge) `quot` 2)-  EdgeOverlap edge -> Right (fromIntegral (unDirectedEdgeId edge) `quot` 2)-  VertexIntersection vertex -> Left (fromIntegral (unVertexId vertex))---- | What @refinementComplete@ claims is that the quality worklist drained. The--- assertable content of that claim is a fixpoint: a second pass under the same--- parameters can admit nothing. The complementary run is budget-starved, where--- the run must stop short and spend exactly what it was given.-testRefinementCompletionIsAFixpoint :: IO ()-testRefinementCompletionIsAFixpoint = do-  -- Barrier parity needs constraints to bound a domain: on an unconstrained-  -- mesh every face sits at depth zero, so excluding outer faces excludes all-  -- of them and the worklist drains having refined nothing.-  bounded <--    requireRight "refinement fixpoint domain" $-      constrainedDelaunay-        unitElementDefaults-        (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8])-        (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])-  let source = buildTriangulation bounded-      parameters =-        defaultRefinementParameters-          { refineMaxAdditionalVertices = Just 500-          , refineMaxArea = Just 3-          , refineExcludeOuterFaces = True-          }-  drained <- requireRight "drained refinement" (refine id parameters source)-  unless (refinementComplete drained) $-    fail "the refinement budget was too small to drain the worklist"-  -- Without this the fixpoint below is vacuous: a run that refined nothing-  -- trivially admits nothing on a second pass.-  unless (refinementAddedVertices drained > 0) $-    fail "the drained run inserted no Steiner points, so the fixpoint proves nothing"-  assertValid "drained refinement" (refinedTriangulation drained)-  again <- requireRight "second refinement pass" (refine id parameters (refinedTriangulation drained))-  assertEqual-    "a drained worklist admits nothing on a second pass"-    0-    (refinementAddedVertices again)-  starved <--    requireRight-      "starved refinement"-      (refine id parameters {refineMaxAdditionalVertices = Just 3} source)-  when (refinementComplete starved) $-    fail "a three-vertex budget reported a drained worklist"-  assertEqual "a starved run spends exactly its budget" 3 (refinementAddedVertices starved)--testScalarFormat :: IO ()-testScalarFormat = do-  let binary64 = scalarBinaryFormat-  assertEqual "binary64 radix" 2 (formatRadix binary64)-  assertEqual "binary64 mantissa" 53 (formatMantissaDigits binary64)-  assertEqual "binary64 unit roundoff" (encodeFloat 1 (-53)) (scalarUnitRoundoff :: Double)--testWideDistanceNearestNeighbor :: IO ()-testWideDistanceNearestNeighbor = do-  let largePoints = V.fromList-        [ Point 0 0-        , Point 1.0e20 0-        , Point 2.0e20 0-        , Point 1.0e20 1.0e19-        ] :: V.Vector (Point)-  largeBuild <- requireRight "wide-distance build" (delaunay unitElementDefaults largePoints)-  let largeTriangulation = buildTriangulation largeBuild-  assertValid "wide-distance triangulation" largeTriangulation-  query <- requireQueryPoint "wide-distance query" (Point 1.05e20 0)-  case nearestNeighbor largeTriangulation Nothing query of-    Nothing -> fail "wide-distance nearest-neighbor returned Nothing"-    Just (nearest, _) -> assertEqual "wide-distance nearest-neighbor" (VertexId 1) nearest--data IncrementalLocationEvidence = IncrementalLocationEvidence-  { incrementalTriangulation :: !(DelaunayTriangulation (Point))-  , incrementalWalkSteps :: {-# UNPACK #-} !Int-  , incrementalFallbacks :: {-# UNPACK #-} !Int-  }--testIncrementalLocationDescent :: IO ()-testIncrementalLocationDescent = do-  small <- collectIncrementalLocationEvidence 500-  large <- collectIncrementalLocationEvidence 1000-  assertEqual "incremental location fallbacks/500" 0 (incrementalFallbacks small)-  assertEqual "incremental location fallbacks/1000" 0 (incrementalFallbacks large)-  unless (2 * incrementalWalkSteps large < 7 * incrementalWalkSteps small) $-    fail-      ( "incremental location approached quadratic growth: "-          <> show (incrementalWalkSteps small, incrementalWalkSteps large)-      )-  assertValid "incremental location descent/1000" (incrementalTriangulation large)--collectIncrementalLocationEvidence :: Int -> IO IncrementalLocationEvidence-collectIncrementalLocationEvidence count =-  V.foldM' insertAndAccumulate initialEvidence (V.fromList (randomPoints 0xc1ac_10ca count))- where-  initialEvidence =-    IncrementalLocationEvidence-      { incrementalTriangulation = empty unitElementDefaults-      , incrementalWalkSteps = 0-      , incrementalFallbacks = 0-      }--  insertAndAccumulate evidence point = do-    result <- requireRight "incremental location descent" (insert (incrementalTriangulation evidence) point)-    let stats = insertionStats result-    pure-      IncrementalLocationEvidence-        { incrementalTriangulation = insertionTriangulation result-        , incrementalWalkSteps = incrementalWalkSteps evidence + statLocationWalkSteps stats-        , incrementalFallbacks = incrementalFallbacks evidence + statLocationFallbacks stats-        }---- A persistent insertion locates on the frozen mesh before opening its dense--- transaction. The thaw preserves every extant handle, so a lawful frozen--- location can be interpreted directly without a second mutable walk. A--- degenerate-line outside witness lacks the terminal-edge evidence its mutable--- interpreter requires, so that one stratum deliberately retains the mutable--- fallback. Exercise every frozen stratum, including the singleton's edge-less--- outside witness, rather than testing only the ordinary face case.-testPersistentInsertionReusesFrozenLocation :: IO ()-testPersistentInsertionReusesFrozenLocation = do-  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)-  emptyLocation <- assertPersistentInsertionFromFrozenLocation "empty" Inserted vacant (Point 0 0)-  assertEqual "empty insertion frozen location" EmptyTriangulation emptyLocation--  singletonBuild <- requirePointBuild "singleton frozen location" [Point 0 0]-  singletonLocation <--    assertPersistentInsertionFromFrozenLocation-      "singleton outside insertion"-      Inserted-      (buildTriangulation singletonBuild)-      (Point 2 0)-  assertEqual "singleton insertion frozen location" (OutsideConvexHull Nothing) singletonLocation--  lineBuild <- requirePointBuild "line frozen locations" [Point 0 0, Point 2 0, Point 4 0]-  let line = buildTriangulation lineBuild-  lineEdgeLocation <- assertPersistentInsertionFromFrozenLocation "line edge insertion" Inserted line (Point 1 0)-  case lineEdgeLocation of-    OnEdge _ -> pure ()-    other -> fail ("line edge insertion located " <> show other)-  lineOutsideLocation <- assertPersistentInsertionFromFrozenLocation "line extension" Inserted line (Point 6 0)-  case lineOutsideLocation of-    OutsideConvexHull (Just _) -> pure ()-    other -> fail ("line extension located " <> show other)--  triangleBuild <- requirePointBuild "area frozen locations" [Point 0 0, Point 4 0, Point 0 4]-  let triangle = buildTriangulation triangleBuild-  faceLocation <- assertPersistentInsertionFromFrozenLocation "face insertion" Inserted triangle (Point 1 1)-  case faceLocation of-    InFace _ -> pure ()-    other -> fail ("face insertion located " <> show other)-  edgeLocation <- assertPersistentInsertionFromFrozenLocation "area edge insertion" Inserted triangle (Point 2 0)-  case edgeLocation of-    OnEdge _ -> pure ()-    other -> fail ("area edge insertion located " <> show other)-  hullLocation <- assertPersistentInsertionFromFrozenLocation "hull insertion" Inserted triangle (Point 5 1)-  case hullLocation of-    OutsideConvexHull (Just _) -> pure ()-    other -> fail ("hull insertion located " <> show other)-  duplicateLocation <- assertPersistentInsertionFromFrozenLocation "duplicate insertion" AlreadyPresent triangle (Point 0 0)-  assertEqual "duplicate insertion frozen location" (OnVertex (VertexId 0)) duplicateLocation--assertPersistentInsertionFromFrozenLocation-  :: String-  -> InsertionDisposition-  -> DelaunayTriangulation (Point)-  -> Point-  -> IO Location-assertPersistentInsertionFromFrozenLocation label expectedDisposition source point = do-  query <- requireQueryPoint (label <> " frozen query") point-  let (located, walked) = locatePointWithHint source Nothing query-      sourceVertices = numVertices source-      usesMutableFallback =-        case located of-          OutsideConvexHull (Just _) -> numInnerFaces source == 0-          _ -> False-  result <- requireRight (label <> " insert") (insert source point)-  ((referenceVertex, referenceDisposition), reference, _) <--    requireRight (label <> " session reference") $-      withSession source 1 (insertVertexAt point point)-  let stats = insertionStats result-      expectedVertices =-        case expectedDisposition of-          Inserted -> sourceVertices + 1-          AlreadyPresent -> sourceVertices-      expectedUnique =-        case expectedDisposition of-          Inserted -> 1-          AlreadyPresent -> 0-      expectedExisting =-        case expectedDisposition of-          Inserted -> 0-          AlreadyPresent -> 1-  assertEqual (label <> " disposition") expectedDisposition (insertionDisposition result)-  assertEqual (label <> " session disposition") referenceDisposition (insertionDisposition result)-  assertEqual (label <> " session vertex") referenceVertex (insertionVertex result)-  assertEqual-    (label <> " exact-location topology matches session")-    (canonicalEdges reference)-    (canonicalEdges (insertionTriangulation result))-  assertEqual (label <> " source remains unchanged") sourceVertices (numVertices source)-  assertEqual (label <> " result vertex count") expectedVertices (numVertices (insertionTriangulation result))-  assertEqual (label <> " input count") 1 (statInputPoints stats)-  assertEqual (label <> " unique count") expectedUnique (statUniquePoints stats)-  assertEqual (label <> " existing count") expectedExisting (statExistingPoints stats)-  assertEqual (label <> " duplicate count") expectedExisting (statDuplicatePoints stats)-  if usesMutableFallback-    then do-      let assertAtLeast counter expected actual =-            unless-              (actual >= expected)-              ( fail-                  ( label-                      <> " "-                      <> counter-                      <> " includes frozen evidence: expected at least "-                      <> show expected-                      <> ", got "-                      <> show actual-                  )-              )-      assertAtLeast "walk steps" (locationWalkSteps walked) (statLocationWalkSteps stats)-      assertAtLeast "walk maximum" (locationWalkSteps walked) (statLocationMaxWalk stats)-      assertAtLeast-        "fallback count"-        (if locationUsedFallback walked then 1 else 0)-        (statLocationFallbacks stats)-    else do-      assertEqual (label <> " frozen walk steps") (locationWalkSteps walked) (statLocationWalkSteps stats)-      assertEqual (label <> " frozen walk maximum") (locationWalkSteps walked) (statLocationMaxWalk stats)-      assertEqual-        (label <> " frozen fallback count")-        (if locationUsedFallback walked then 1 else 0)-        (statLocationFallbacks stats)-  assertValid (label <> " result") (insertionTriangulation result)-  pure located---- Circle sweep must be a construction schedule, not a second topology. It is--- compared against the arrival-order session kernel on the same exact inputs.--- | One session, both verbs. The reason the two published sessions became one:--- a caller who removes and inserts had to thaw twice and pay the O(n)--- publication a session exists to delete.------ Graded against the oracle that needs no second implementation — the Delaunay--- triangulation of a point set in general position is unique, so a mixed edit--- must land exactly where a fresh bulk load of the surviving set lands.-testMixedEditSession :: IO ()-testMixedEditSession = do-  let original = V.fromList (randomPoints 0x5eed1e 400)-      doomed = V.take 120 original-      survivors = V.drop 120 original-      arrivals = V.fromList (randomPoints 0xa7717a1 90)-  (_, edited, stats) <--    requireRight "mixed session" $-      withSession-        (buildTriangulation (either (error . show) id (delaunay unitElementDefaults original)))-        (V.length arrivals)-        ( do-            V.mapM_ (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ()))) doomed-            V.mapM_ insertVertex arrivals-        )-  fresh <- requireRight "fresh rebuild" (delaunay unitElementDefaults (survivors <> arrivals))-  assertEqual-    "mixed edit equals a fresh build of the surviving set"-    (canonicalEdges (buildTriangulation fresh))-    (canonicalEdges edited)-  assertValid "mixed edit session" edited-  assertEqual-    "the whole transaction charged one counter set"-    (V.length arrivals)-    (statInputPoints stats)---- The bulk removal verb must land exactly where the singleton fold lands, on--- both sides of its locate-strategy crossover: a small batch keeps the--- per-question walk, a large one buys the identity index once. Same removals,--- same order, same survivor either way.-testBulkRemovalAgreement :: IO ()-testBulkRemovalAgreement = do-  let original = V.fromList (randomPoints 0xb01dca7 400)-      base = buildTriangulation (either (error . show) id (delaunay unitElementDefaults original))-      run label doomed = do-        (outcomes, survived, _) <--          requireRight (label <> " bulk removal") (withSession base 0 (removeManyAt doomed))-        V.imapM_-          ( \index outcome ->-              maybe (fail (label <> " bulk removal missed index " <> show index)) (const (pure ())) outcome-          )-          outcomes-        (_, folded, _) <--          requireRight (label <> " singleton removal fold") $-            withSession-              base-              0-              ( V.mapM_-                  (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ())))-                  doomed-              )-        assertEqual-          (label <> " bulk removal equals singleton descent")-          (canonicalEdges folded)-          (canonicalEdges survived)-        assertValid (label <> " bulk removal") survived-  run "walking" (V.take 40 original)-  run "indexed" (V.take 120 original)---- The three fixture positions hash to home slot 15 in the 16-slot table made--- for three keys. They therefore occupy 15, 0, and 1 in insertion order. The--- middle deletion exercises both wraparound and backward shift, while the--- coordinate overwrite models the tail move performed before the table handle--- is renamed by 'swapRemoveVertex'.-testMutablePointIndexWraparoundBackshift :: IO ()-testMutablePointIndexWraparoundBackshift = do-  (removed, first, movedBeforeRelocation, relocated, movedAfterRelocation) <--    requireRight "mutable point-index wraparound/backshift law" wraparoundLaw-  case removed of-    MutablePointIndexUpdated -> pure ()-    MutablePointIndexInvalidated -> fail "mutable point-index middle delete invalidated a valid table"-  assertEqual "mutable point-index first wraparound occupant" (Just 0) first-  assertEqual "mutable point-index shifted tail before relocation" (Just 2) movedBeforeRelocation-  case relocated of-    MutablePointIndexUpdated -> pure ()-    MutablePointIndexInvalidated -> fail "mutable point-index tail relocation invalidated a valid table"-  assertEqual "mutable point-index relocated tail" (Just 1) movedAfterRelocation- where-  wraparoundLaw = runST $ do-    pointXs <- MUV.replicate 3 (0 :: Double)-    pointYs <- MUV.replicate 3 (0 :: Double)-    MUV.unsafeWrite pointXs 0 (-20)-    MUV.unsafeWrite pointYs 0 (-15)-    MUV.unsafeWrite pointXs 1 (-20)-    MUV.unsafeWrite pointYs 1 0-    MUV.unsafeWrite pointXs 2 (-20)-    MUV.unsafeWrite pointYs 2 7-    table <- newMutablePointIndex 3-    seeded <- seedMutablePointIndex table 3 (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs)-    case seeded of-      Left failure -> pure (Left failure)-      Right () -> do-        -- The tail has moved into slot one before identity transport begins.-        MUV.unsafeWrite pointXs 1 (-20)-        MUV.unsafeWrite pointYs 1 7-        removed <--          removeMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 0 1-        first <--          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) (-15)-        movedBeforeRelocation <--          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 7-        relocated <- relocateMutablePoint table (-20) 7 2 1-        movedAfterRelocation <--          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 7-        pure-          ( Right-              ( removed-              , first-              , movedBeforeRelocation-              , relocated-              , movedAfterRelocation-              )-          )---- A dense table must also close lawfully when it removes every vertex. The--- point-keyed query after freeze forces the empty published derivation rather--- than retaining an impossible ST table.-testBatchIdentityIndexBatchToEmpty :: IO ()-testBatchIdentityIndexBatchToEmpty = do-  let points = V.fromList (randomPoints 0x7a110bad 32)-  built <- requireRight "batch identity empty base" (delaunay unitElementDefaults points)-  (outcomes, emptied, _) <--    requireRight "batch identity removes every point" $-      withSession (buildTriangulation built) 0 (removeManyAt points)-  unless (V.all isJust outcomes) $-    fail "batch identity table missed a point while removing to empty"-  assertEqual "batch identity empty vertex count" 0 (numVertices emptied)-  assertValid "batch identity empty result" emptied-  absent <- requireRight "empty published identity lookup" (locateAndRemove emptied (Point 0 0))-  case absent of-    Nothing -> pure ()-    Just _ -> fail "empty published identity lookup manufactured a removal"---- A dense batch discards its ST table before the next singleton handle rewrite.--- 'excise' must therefore activate and transport the ordinary persistent index--- without consulting the expired batch representation.-testBatchIdentityIndexToSingletonActive :: IO ()-testBatchIdentityIndexToSingletonActive = do-  let original = V.fromList (randomPoints 0x51a91e 64)-      doomed = V.take 32 original-      survivors = V.drop 32 original-  built <- requireRight "batch-to-singleton identity base" (delaunay unitElementDefaults original)-  (singletonOutcome, transitioned, _) <--    requireRight "batch-to-singleton identity session" $-      withSession (buildTriangulation built) 0 $ do-        _ <- removeManyAt doomed-        excise (VertexId 0)-  let singletonPoint = removalOutcomePoint singletonOutcome-      expected = V.filter (/= singletonPoint) survivors-  fresh <- requireRight "batch-to-singleton fresh rebuild" (delaunay unitElementDefaults expected)-  assertEqual-    "batch-to-singleton identity topology"-    (canonicalEdges (buildTriangulation fresh))-    (canonicalEdges transitioned)-  assertValid "batch-to-singleton identity result" transitioned-  case V.find (/= singletonPoint) survivors of-    Nothing -> fail "batch-to-singleton fixture exhausted every survivor"-    Just remaining -> do-      located <- requireRight "batch-to-singleton published lookup" (locateAndRemove transitioned remaining)-      case located of-        Nothing -> fail "batch-to-singleton published lookup missed a survivor"-        Just removal -> assertValid "batch-to-singleton published removal" (removalTriangulation removal)---- The mutable identity table is an internal section of @removeManyAt@, never a--- session-wide owner. An insertion after the dense batch invalidates it, the--- subsequent removal walks correctly, and the frozen mesh must rederive the--- published identity cache from its final coordinate authority.-testBulkIdentityIndexDoesNotEscapeRemovalBatch :: IO ()-testBulkIdentityIndexDoesNotEscapeRemovalBatch = do-  let original = V.fromList (randomPoints 0x5a11ce 400)-      doomed = V.take 120 original-      survivors = V.drop 120 original-      arrival = Point (-0.25) 0.75-  baseBuild <- requireRight "bulk identity section base" (delaunay unitElementDefaults original)-  (_, edited, _) <--    requireRight "bulk identity section mixed session" $-      withSession (buildTriangulation baseBuild) 1 $ do-        _ <- removeManyAt doomed-        _ <- insertVertexAt arrival arrival-        removeAt arrival >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ()))-  fresh <- requireRight "bulk identity section fresh survivor rebuild" (delaunay unitElementDefaults survivors)-  assertEqual-    "bulk identity section mixed program equals fresh survivors"-    (canonicalEdges (buildTriangulation fresh))-    (canonicalEdges edited)-  assertValid "bulk identity section mixed session" edited-  case V.uncons survivors of-    Nothing -> fail "bulk identity section test has no survivor"-    Just (survivor, _) -> do-      located <- requireRight "published lazy identity lookup" (locateAndRemove edited survivor)-      case located of-        Nothing -> fail "published lazy identity lookup missed survivor"-        Just removal -> assertValid "published lazy identity removal" (removalTriangulation removal)---- The hierarchy-hinted removal program must land exactly where the unhinted--- session lands. The guesses are all computed against the original base, so--- later removals in the batch answer for guesses whose slots swap-compaction--- has renamed — the walk must correct every one of them. The repaired--- hierarchy must equal the reference rebuild over the same survivor.-testHierarchyRemovalAgreement :: IO ()-testHierarchyRemovalAgreement = do-  let original = V.fromList (randomPoints 0x5eed1e55 400)-      base = buildTriangulation (either (error . show) id (delaunay unitElementDefaults original))-  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint defaultHierarchyBranchFactor base)-  let run label doomed = do-        (outcomes, survived, repaired) <--          requireRight (label <> " hinted removal") (removeManyWithHierarchy hierarchy base doomed)-        V.imapM_-          ( \index outcome ->-              maybe (fail (label <> " hinted removal missed index " <> show index)) (const (pure ())) outcome-          )-          outcomes-        (_, folded, _) <--          requireRight (label <> " unhinted session") (withSession base 0 (removeManyAt doomed))-        reference <- requireRight (label <> " reference rebuild") (rebuildHierarchyHint hierarchy folded)-        assertEqual-          (label <> " hinted removal equals unhinted session")-          (canonicalEdges folded)-          (canonicalEdges survived)-        assertEqual (label <> " repaired hierarchy equals reference rebuild") reference repaired-        assertValid (label <> " hinted removal") survived-  run "sparse" (V.take 40 original)-  run "dense" (V.take 120 original)--testCircleSweepBulkLoad :: IO ()-testCircleSweepBulkLoad = do-  let points = V.fromList (randomPoints 0xc1ac1e 1500)-  swept <- requireRight "circle-sweep build" (delaunay unitElementDefaults points)-  (_, sessioned, _) <--    requireRight "session build" $-      withSession (empty unitElementDefaults) (V.length points) $-        V.mapM_ insertVertex points-  assertEqual-    "circle-sweep/session topology"-    (canonicalEdges sessioned)-    (canonicalEdges (buildTriangulation swept))-  assertValid "circle-sweep build" (buildTriangulation swept)-  assertValid "session build" sessioned--testPredicates :: IO ()-testPredicates = do-  let a, b, c :: Point-      a = Point 0 0-      b = Point 1 0-      c = Point 0 1-  assertEqual "orientation left" GT (orient2d a b c)-  assertEqual "orientation right" LT (orient2d b a c)-  assertEqual "orientation collinear" EQ (orient2d a b (Point 0.5 0))-  assertEqual "incircle inside" GT (inCircle a b c (Point 0.25 0.25))-  assertEqual "incircle boundary" EQ (inCircle a b c (Point 1 1))-  assertEqual "incircle outside" LT (inCircle a b c (Point 2 2))-  assertEqual "underflow mitigation" (Point 0 1) (mitigateUnderflow (Point 1.0e-44 1 :: Point))-  _ <- requireRight "point validation" (validatePoint Nothing (Point 0 1 :: Point))-  let large = encodeFloat 1 180 :: Double-      ulp = encodeFloat 1 128 :: Double-  assertEqual-    "large exact orientation"-    GT-    (orient2d (Point large large) (Point (large + ulp) large) (Point large (large + ulp)))-  assertEqual-    "large exact cocircularity"-    EQ-    (inCircle-      (Point large large)-      (Point (large + ulp) large)-      (Point large (large + ulp))-      (Point (large + ulp) (large + ulp)))--testHandlesAndFiniteDcel :: IO ()-testHandlesAndFiniteDcel = do-  built <- requirePointBuild "handle algebra" [Point 0 0, Point 3 0, Point 0 2, Point 0.4 0.7]-  let triangulation = buildTriangulation built-  assertValid "handle algebra" triangulation-  assertEqual "one outer face" (numInnerFaces triangulation + 1) (numFaces triangulation)-  forM_ (directedEdges triangulation) $ \edge -> do-    assertEqual "double reversal" edge (reverseEdge (reverseEdge edge))-    assertEqual "next/previous" edge (previous triangulation (next triangulation edge))-    assertEqual "previous/next" edge (next triangulation (previous triangulation edge))-  forM_ (undirectedEdges triangulation) $ \edge -> do-    let (forward, backward) = directedPair edge-    assertEqual "pair reversal" backward (reverseEdge forward)-    assertEqual "undirected projection" edge (asUndirected forward)-  assertEqual "vertex iterator" (numVertices triangulation) (length (vertices triangulation))-  assertEqual-    "dense vertex projection"-    (V.fromList (fmap (vertexPoint triangulation) (vertices triangulation)))-    (vertexPoints triangulation)-  assertEqual "edge iterator" (numUndirectedEdges triangulation) (length (undirectedEdges triangulation))-  assertEqual "face iterator" (numInnerFaces triangulation) (length (innerFaces triangulation))-  assertEqual-    "dense inner-face directed-edge projection"-    (Just (V.toList (innerFaceDirectedEdgeTriples triangulation)))-    (traverse (Dcel.innerFaceDirectedEdges triangulation) (innerFaces triangulation))-  assertEqual-    "dense inner-face projection"-    (Just (V.toList (innerFaceVertexTriples triangulation)))-    (traverse (Dcel.innerFaceVertices triangulation) (innerFaces triangulation))-  assertEqual "dynamic vertex iterator" (numVertices triangulation) (length (DynamicIterators.vertexHandles triangulation))-  assertEqual "dynamic edge iterator" (numDirectedEdges triangulation) (length (DynamicIterators.directedEdgeHandles triangulation))-  assertEqual "dynamic face iterator" (numInnerFaces triangulation) (length (DynamicIterators.innerFaceHandles triangulation))-  unless (geometryTopologyBytes triangulation > topologyIndexBytes triangulation) $-    fail "geometry byte accounting omitted coordinates"-  vertex0 <- requireJust "dynamic vertex handle" (Dynamic.vertexHandle triangulation (VertexId 0))-  assertEqual "dynamic vertex fix" (VertexId 0) (Dynamic.fixVertex vertex0)-  assertEqual "dynamic vertex position" (vertexPoint triangulation (VertexId 0)) (Dynamic.vertexHandlePosition vertex0)-  case Dynamic.vertexHandleOutEdge vertex0 of-    Nothing -> fail "connected dynamic vertex has no outgoing edge"-    Just edge -> do-      assertEqual "dynamic edge reversal" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgeReverse (Dynamic.directedEdgeReverse edge)))-      assertEqual "dynamic edge next/previous" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgePrevious (Dynamic.directedEdgeNext edge)))-      let face = Dynamic.directedEdgeFace edge-      if Dynamic.faceIsOuter face-        then pure ()-        else case Dynamic.faceAsInner face of-          Nothing -> fail "non-outer dynamic face did not refine to InnerTag"-          Just inner -> do-            _ <- requireJust "inner dynamic face vertices" (Dynamic.innerFaceVertices inner)-            case Dynamic.innerFaceCircumcenter inner of-              Nothing -> fail "inner dynamic face has no circumcenter"-              Just _ -> pure ()--testDegenerateConstruction :: IO ()-testDegenerateConstruction = do-  emptyBuild <- requirePointBuild "empty" []-  emptyQuery <- requireQueryPoint "empty location" (Point 0 0)-  assertEqual "empty vertices" 0 (numVertices (buildTriangulation emptyBuild))-  assertEqual "empty location" EmptyTriangulation (locatePoint (buildTriangulation emptyBuild) emptyQuery)-  assertValid "empty" (buildTriangulation emptyBuild)--  singleton <- requirePointBuild "singleton" [Point 2 3]-  singletonQuery <- requireQueryPoint "singleton lookup" (Point 2 3)-  assertEqual "singleton lookup" (OnVertex (VertexId 0)) (locatePoint (buildTriangulation singleton) singletonQuery)-  assertValid "singleton" (buildTriangulation singleton)--  duplicates <- requirePointBuild "duplicates" [Point 0 0, Point 1 0, Point (-0.0) 0, Point 1 0, Point 2 0]-  assertEqual "deduplicated count" 3 (numVertices (buildTriangulation duplicates))-  assertEqual-    "stable duplicate mapping"-    (primArrayFromList [0, 1, 0, 1, 2])-    (buildInputVertices duplicates)-  assertValid "duplicates" (buildTriangulation duplicates)--  -- Deduplicating a signed zero is not the same claim as storing it canonically:-  -- the first only needs '==', which already identifies the two, while anything-  -- reading the bit pattern — a radix ordering, a byte-for-byte cross-check —-  -- reads the sign bit and gets a coordinate below every other. Both ingest-  -- paths canonicalize, and both are held to it here rather than to the weaker-  -- statement that comparison happens to survive.-  let bits point = (castDoubleToWord64 (pointX point), castDoubleToWord64 (pointY point))-  signedZero <- requirePointBuild "signed zero" [Point (-0.0) (-0.0), Point 1 0, Point 0 1]-  assertEqual "bulk load stores a canonical zero"-    (0, 0) (bits (vertexPoint (buildTriangulation signedZero) (VertexId 0)))-  incremental <--    requireRight "signed zero insert"-      (insert (empty unitElementDefaults :: DelaunayTriangulation (Point)) (Point (-0.0) (-0.0)))-  assertEqual "insertion stores a canonical zero"-    (0, 0) (bits (vertexPoint (insertionTriangulation incremental) (VertexId 0)))--  collinear <- requirePointBuild "collinear" [Point 0 4, Point 0 0, Point 0 3, Point 0 2, Point 0 1]-  let line = buildTriangulation collinear-  assertEqual "line edge count" 4 (numUndirectedEdges line)-  assertEqual "line face count" 0 (numInnerFaces line)-  assertValid "collinear terminal splits" line--testPersistentLocalUpdates :: IO ()-testPersistentLocalUpdates = do-  base <- requirePointBuild "persistent base" (randomPoints 0x5eed 4095)-  let triangulation = buildTriangulation base-      query = Point 0.000_123 (-0.000_271)-  inserted <- requireRight "persistent insert" (insert triangulation query)-  assertEqual "persistent source remains unchanged" 4095 (numVertices triangulation)-  assertEqual "persistent result appends one vertex" 4096 (numVertices (insertionTriangulation inserted))-  assertValid "persistent insertion result" (insertionTriangulation inserted)--  let payloads = V.fromList-        [ SampleVertex (Point 0 0) 10-        , SampleVertex (Point 1 0) 20-        , SampleVertex (Point 0 1) 30-        ]-      defaults = ElementDefaults (0 :: Int) False ("face" :: String)-  payloadBuild <- requireRight "payload base" (delaunay defaults payloads)-  duplicate <- requireRight "payload-only replacement" (insert (buildTriangulation payloadBuild) (SampleVertex (Point 1 0) 99))-  assertEqual "payload update disposition" AlreadyPresent (insertionDisposition duplicate)-  assertEqual "payload replacement" 99 (sampleLabel (vertexData (insertionTriangulation duplicate) (VertexId 1)))--testGenericPayloads :: IO ()-testGenericPayloads = do-  let defaults = ElementDefaults (7 :: Int) False ("new-face" :: String)-      payloads = V.fromList-        [ SampleVertex (Point 0 0) 1-        , SampleVertex (Point 2 0) 2-        , SampleVertex (Point 0 2) 3-        , SampleVertex (Point 0.5 0.5) 4-        ]-  built <- requireRight "generic payload build" (delaunay defaults payloads)-  let triangulation = buildTriangulation built-  assertValid "generic payload build" triangulation-  assertEqual "vertex payload" 4 (sampleLabel (vertexData triangulation (VertexId 3)))-  forM_ (directedEdges triangulation) $ \edge -> assertEqual "directed default" 7 (directedEdgeData triangulation edge)-  forM_ (undirectedEdges triangulation) $ \edge -> assertEqual "undirected default" False (undirectedEdgeData triangulation edge)-  forM_ (allFaces triangulation) $ \face -> assertEqual "face default" "new-face" (faceData triangulation face)-  (firstEdge, firstFace) <- case (directedEdges triangulation, innerFaces triangulation) of-    (edge : _, face : _) -> pure (edge, face)-    _ -> fail "generic payload build produced no inner topology"-  let firstUndirected = asUndirected firstEdge-      changed = setFaceData (setUndirectedEdgeData (setDirectedEdgeData triangulation firstEdge 42) firstUndirected True) firstFace "changed"-  assertEqual "directed payload update" 42 (directedEdgeData changed firstEdge)-  assertEqual "undirected payload update" True (undirectedEdgeData changed firstUndirected)-  assertEqual "face payload update" "changed" (faceData changed firstFace)-  -- Geometry owns the points, so a payload carrying a different position is not-  -- a contradiction to be refused — it is a payload whose position nobody reads.-  let moved = setVertexData triangulation (VertexId 0) (SampleVertex (Point 9 9) 0)-  assertEqual "a payload position does not site a vertex"-    (vertexPoint triangulation (VertexId 0)) (vertexPoint moved (VertexId 0))-  assertEqual "the payload is stored as given"-    (Point 9 9) (samplePosition (vertexData moved (VertexId 0)))---- The payload layer over a fixed geometry is a product of four free components.--- Each is a functor and each is checked as such; nothing in the product can--- disturb the geometry underneath it.-testPayloadMaps :: IO ()-testPayloadMaps = do-  let defaults = ElementDefaults (7 :: Int) ("new-undirected" :: String) ("new-face" :: String)-      payloads = V.fromList-        [ SampleVertex (Point 0 0) 1-        , SampleVertex (Point 4 0) 2-        , SampleVertex (Point 4 4) 3-        , SampleVertex (Point 0 4) 4-        , SampleVertex (Point 1 2) 5-        ]-  built <- requireRight "payload map build" (delaunay defaults payloads)-  let plain = buildTriangulation built-      -- Distinct payloads everywhere: a map that permuted its component would-      -- be invisible against uniform defaults.-      withDirected = List.foldl' (\t (label, edge) -> setDirectedEdgeData t edge label) plain (zip [100 ..] (directedEdges plain))-      withUndirected = List.foldl' (\t (label, edge) -> setUndirectedEdgeData t edge ("u-" <> show label)) withDirected (zip [(0 :: Int) ..] (undirectedEdges withDirected))-      sample = List.foldl' (\t (label, face) -> setFaceData t face ("f-" <> show label)) withUndirected (zip [(0 :: Int) ..] (allFaces withUndirected))-      endpoints ::-        Triangulation mode vertex directed undirected face ->-        [(VertexId, VertexId)]-      endpoints t = [(origin t edge, destination t edge) | edge <- directedEdges t]--  -- Identity. Equality on a triangulation compares geometry, topology,-  -- constraint flags and element defaults as well as payloads, so this single-  -- equation states that each map disturbs nothing but the component it names.-  assertEqual "mapDirectedEdges identity" sample (mapDirectedEdges id sample)-  assertEqual "mapUndirectedEdges identity" sample (mapUndirectedEdges id sample)-  assertEqual "mapFaces identity" sample (mapFaces id sample)-  assertEqual "mapVertices identity" sample (mapVertices id sample)--  assertEqual "mapDirectedEdges composition"-    (mapDirectedEdges ((* 2) . (+ 1)) sample)-    (mapDirectedEdges (* 2) (mapDirectedEdges (+ 1) sample))-  assertEqual "mapFaces composition"-    (mapFaces (("<" <>) . (<> ">")) sample)-    (mapFaces ("<" <>) (mapFaces (<> ">") sample))--  -- Commutation with the accessors. Identity and composition are blind to the-  -- indexing; this is the law that pins each payload to its own handle.-  let directedMapped = mapDirectedEdges (+ 1) sample-      undirectedMapped = mapUndirectedEdges ("<" <>) sample-      facesMapped = mapFaces ("<" <>) sample-  forM_ (directedEdges sample) $ \edge ->-    assertEqual "mapDirectedEdges commutes with directedEdgeData"-      (directedEdgeData sample edge + 1) (directedEdgeData directedMapped edge)-  forM_ (undirectedEdges sample) $ \edge ->-    assertEqual "mapUndirectedEdges commutes with undirectedEdgeData"-      ("<" <> undirectedEdgeData sample edge) (undirectedEdgeData undirectedMapped edge)-  forM_ (allFaces sample) $ \face ->-    assertEqual "mapFaces commutes with faceData"-      ("<" <> faceData sample face) (faceData facesMapped face)--  -- The components are independent, and none of them is geometry.-  assertEqual "face and directed maps commute"-    (mapFaces ("<" <>) (mapDirectedEdges (+ 1) sample))-    (mapDirectedEdges (+ 1) (mapFaces ("<" <>) sample))-  assertEqual "mapFaces preserves topology" (endpoints sample) (endpoints facesMapped)-  assertEqual "mapFaces preserves the face count" (numFaces sample) (numFaces facesMapped)-  forM_ (vertices sample) $ \vertex ->-    assertEqual "mapFaces preserves geometry" (vertexPoint sample vertex) (vertexPoint facesMapped vertex)--  -- The element default is a payload and must travel with them: every element a-  -- later insertion creates is handed the default, so a map that reindexed the-  -- stored payloads and left the default behind would produce a triangulation-  -- whose future elements disagree with its present ones. Every stored payload-  -- here differs from the default, so carrying the wrong one is visible.-  grownFaces <- insertionTriangulation <$> requireRight "insertion into mapped faces" (insert facesMapped (SampleVertex (Point 2 1) 6))-  grownUndirected <- insertionTriangulation <$> requireRight "insertion into mapped undirected edges" (insert undirectedMapped (SampleVertex (Point 2 1) 6))-  grownDirected <- insertionTriangulation <$> requireRight "insertion into mapped directed edges" (insert directedMapped (SampleVertex (Point 2 1) 6))-  unless (numFaces grownFaces > numFaces facesMapped) $ fail "the insertion created no face"-  unless ("<new-face" `elem` map (faceData grownFaces) (allFaces grownFaces)) $-    fail ("new faces did not receive the mapped default: " <> show (map (faceData grownFaces) (allFaces grownFaces)))-  unless ("<new-undirected" `elem` map (undirectedEdgeData grownUndirected) (undirectedEdges grownUndirected)) $-    fail "new undirected edges did not receive the mapped default"-  unless (8 `elem` map (directedEdgeData grownDirected) (directedEdges grownDirected)) $-    fail "new directed edges did not receive the mapped default"--  -- Vertices, the component that used to be special. The map is total, and the-  -- one thing worth insisting on is that a function which does its worst to the-  -- stored position still cannot move a vertex.-  let relabelled = mapVertices (\v -> v{sampleLabel = sampleLabel v * 10}) sample-      collapsed = mapVertices (\v -> v{samplePosition = Point 9 9}) sample-  forM_ (vertices sample) $ \vertex -> do-    assertEqual "mapVertices commutes with vertexData"-      (sampleLabel (vertexData sample vertex) * 10) (sampleLabel (vertexData relabelled vertex))-    assertEqual "mapVertices preserves geometry" (vertexPoint sample vertex) (vertexPoint relabelled vertex)-    assertEqual "a payload map cannot move a vertex"-      (vertexPoint sample vertex) (vertexPoint collapsed vertex)-  assertEqual "mapVertices composes"-    (mapVertices (\v -> v{sampleLabel = sampleLabel v + 1}) relabelled)-    (mapVertices (\v -> v{sampleLabel = sampleLabel v * 10 + 1}) sample)--  -- The sharpest statement of freedom available: the image type has no-  -- 'HasPosition' instance at all. This does not typecheck under a vertex-  -- component that geometry reads through.-  let projected = mapVertices sampleLabel sample-  forM_ (vertices sample) $ \vertex -> do-    assertEqual "a vertex payload need not have a position"-      (sampleLabel (vertexData sample vertex)) (vertexData projected vertex)-    assertEqual "projecting payloads away preserves geometry"-      (vertexPoint sample vertex) (vertexPoint projected vertex)--testPayloadTraversals :: IO ()-testPayloadTraversals = do-  let defaults = ElementDefaults (7 :: Int) ("new-undirected" :: String) ("new-face" :: String)-      payloads = V.fromList-        [ SampleVertex (Point 0 0) 1-        , SampleVertex (Point 4 0) 2-        , SampleVertex (Point 4 4) 3-        , SampleVertex (Point 0 4) 4-        , SampleVertex (Point 1 2) 5-        ]-  built <- requireRight "payload traversal build" (delaunay defaults payloads)-  let plain = buildTriangulation built-      withDirected = List.foldl' (\t (label, edge) -> setDirectedEdgeData t edge label) plain (zip [100 ..] (directedEdges plain))-      withUndirected = List.foldl' (\t (label, edge) -> setUndirectedEdgeData t edge ("u-" <> show label)) withDirected (zip [(0 :: Int) ..] (undirectedEdges withDirected))-      sample = List.foldl' (\t (label, face) -> setFaceData t face ("f-" <> show label)) withUndirected (zip [(0 :: Int) ..] (allFaces withUndirected))--  -- 'overPayloads' is the traversal under 'Identity', so this is the traversal-  -- identity law. It also says that rebuilding a payload store from its own-  -- contents is not observable, which is the part a paged store could get-  -- wrong: the traversal materializes pages the map would have left absent.-  assertEqual "vertexPayloads identity" sample (overPayloads vertexPayloads id sample)-  assertEqual "directedPayloads identity" sample (overPayloads directedPayloads id sample)-  assertEqual "undirectedPayloads identity" sample (overPayloads undirectedPayloads id sample)-  assertEqual "facePayloads identity" sample (overPayloads facePayloads id sample)--  -- Each traversal and its named map are one function. The effectful-  -- generalization is not allowed a second opinion about what relabeling means.-  assertEqual "vertexPayloads agrees with mapVertices"-    (mapVertices sampleLabel sample) (overPayloads vertexPayloads sampleLabel sample)-  assertEqual "directedPayloads agrees with mapDirectedEdges"-    (mapDirectedEdges (* 2) sample) (overPayloads directedPayloads (* 2) sample)-  assertEqual "undirectedPayloads agrees with mapUndirectedEdges"-    (mapUndirectedEdges ("<" <>) sample) (overPayloads undirectedPayloads ("<" <>) sample)-  assertEqual "facePayloads agrees with mapFaces"-    (mapFaces ("<" <>) sample) (overPayloads facePayloads ("<" <>) sample)--  assertEqual "facePayloads composes"-    (overPayloads facePayloads (("<" <>) . (<> ">")) sample)-    (overPayloads facePayloads ("<" <>) (overPayloads facePayloads (<> ">") sample))--  -- The class instances range over the face payload, being the last parameter.-  assertEqual "fmap is the face payload map" (mapFaces ("<" <>) sample) (fmap ("<" <>) sample)-  assertEqual "traverse is facePayloads"-    (Just (overPayloads facePayloads ("<" <>) sample))-    (traverse (Just . ("<" <>)) sample)-  assertEqual "the Foldable instance is the face traversal"-    (payloadList facePayloads sample) (foldr (:) [] sample)--  -- Visit order, and the element default's place in it. A fold that skipped-  -- the default would report the triangulation as holding one fewer face-  -- payload than it holds.-  assertEqual "vertexPayloads visits the vertices in order"-    (map (vertexData sample) (vertices sample))-    (payloadList vertexPayloads sample)-  assertEqual "facePayloads visits the faces and then the default"-    (map (faceData sample) (allFaces sample) <> [defaultFaceData (Internal.triElementDefaults sample)])-    (payloadList facePayloads sample)--  -- The point of the exercise: relabeling under an effect, with a refusal-  -- reaching the caller instead of a half-relabelled triangulation.-  let refuseAtThree :: SampleVertex -> Either String Int-      refuseAtThree v = if sampleLabel v == 3 then Left "vertex three refuses" else Right (sampleLabel v * 10)-      keepLabel :: SampleVertex -> Either String Int-      keepLabel = Right . sampleLabel-      decorate :: String -> Either String String-      decorate = Right . ("<" <>)-  assertEqual "an effectful relabel short-circuits"-    (Left "vertex three refuses") (vertexPayloads refuseAtThree sample)-  relabelled <- requireRight "effectful relabel" (vertexPayloads keepLabel sample)-  assertEqual "a successful effectful relabel is the pure one"-    (mapVertices sampleLabel sample) relabelled--  -- The default travels through the traversal, and travels exactly once: the-  -- store's fill and the element defaults are written from a single visit, so-  -- an element created afterwards inherits precisely what the traversal made.-  traversedFaces <- requireRight "effectful face relabel" (facePayloads decorate sample)-  grown <- insertionTriangulation <$> requireRight "insertion after traversal" (insert traversedFaces (SampleVertex (Point 2 1) 6))-  unless (numFaces grown > numFaces traversedFaces) $ fail "the insertion created no face"-  unless ("<new-face" `elem` map (faceData grown) (allFaces grown)) $-    fail ("new faces did not receive the traversed default: " <> show (map (faceData grown) (allFaces grown)))---- | The in-circle predicate is the orientation of the four points lifted to--- the paraboloid @z = x² + y²@. The referent is that 4×4 determinant evaluated--- exactly over 'Rational' — deliberately not the translated 3×3 the instances--- expand, which would only be the implementation checking its own algebra.-testParaboloidLift :: IO ()-testParaboloidLift = do-  let lifted :: Point -> (Rational, Rational, Rational)-      lifted (Point x y) =-        let (rx, ry) = (toRational x, toRational y) in (rx, ry, rx * rx + ry * ry)-      minor3-        :: (Rational, Rational, Rational)-        -> (Rational, Rational, Rational)-        -> (Rational, Rational, Rational)-        -> Rational-      minor3 (a1, a2, a3) (b1, b2, b3) (c1, c2, c3) =-        a1 * (b2 * c3 - b3 * c2) - a2 * (b1 * c3 - b3 * c1) + a3 * (b1 * c2 - b2 * c1)-      -- Laplace expansion of the lifted determinant along its column of ones.-      liftedOrientation-        :: (Rational, Rational, Rational)-        -> (Rational, Rational, Rational)-        -> (Rational, Rational, Rational)-        -> (Rational, Rational, Rational)-        -> Rational-      liftedOrientation a b c d =-        negate (minor3 b c d) + minor3 a c d - minor3 a b d + minor3 a b c-      predicted :: Point -> Point -> Point -> Point -> Ordering-      predicted a b c d = compare (liftedOrientation (lifted a) (lifted b) (lifted c) (lifted d)) 0-      measured :: Point -> Point -> Point -> Point -> Ordering-      measured (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =-        inCircleCoordinates ax ay bx by cx cy dx dy-      quadruples :: [Point] -> [(Point, Point, Point, Point)]-      quadruples (a : b : c : d : rest) = (a, b, c, d) : quadruples rest-      quadruples _ = []--  forM_ (quadruples (randomPoints 0x51ca_0b17 1600)) $ \(a, b, c, d) ->-    assertEqual ("in-circle is the lifted orientation at " <> show (a, b, c, d))-      (predicted a b c d) (measured a b c d)--  -- Exactly cocircular quadruples, which are exactly the ones the floating-  -- filter must decline to answer. Every Pythagorean point of the radius-5-  -- circle is representable without rounding, so 'EQ' here is a fact about the-  -- geometry rather than about the arithmetic.-  let ring =-        [ Point x y-        | (x, y) <--            [ (5, 0), (0, 5), (-5, 0), (0, -5)-            , (3, 4), (4, 3), (-3, 4), (-4, 3)-            , (3, -4), (4, -3), (-3, -4), (-4, -3)-            ]-        ] :: [Point]-      indexed = zip [(0 :: Int) ..] ring-      cocircular =-        [ (a, b, c, d)-        | (i, a) <- indexed, (j, b) <- indexed, j > i-        , (k, c) <- indexed, k > j, (l, d) <- indexed, l > k-        ]-  unless (length cocircular == 495) $ fail ("expected 495 cocircular quadruples, got " <> show (length cocircular))-  forM_ cocircular $ \(a, b, c, d) -> do-    assertEqual ("cocircular points are cocircular at " <> show (a, b, c, d)) EQ (measured a b c d)-    assertEqual ("the lift agrees on cocircularity at " <> show (a, b, c, d)) EQ (predicted a b c d)--  -- The geometry the sign means, stated once against a circle anyone can read.-  let (a, b, c) = (Point 1 0, Point 0 1, Point (-1) 0) :: (Point, Point, Point)-  assertEqual "the centre is inside the circle" GT (measured a b c (Point 0 0))-  assertEqual "the antipode is on the circle" EQ (measured a b c (Point 0 (-1)))-  assertEqual "a distant point is outside" LT (measured a b c (Point 2 2))---- | A payload labels an element, and an element is its geometry. Every rewrite--- an insertion performs — an edge split, a face split, a Lawson flip — hands--- some slot a different element to hold, and the label the displaced one--- carried does not describe what took its place.------ So: label every element of a triangulation by its own key, insert a point,--- and demand that an element whose key survives still carries exactly the label--- it was given while every element whose key is new carries the default and--- nothing else. Both directions are checked; either alone is satisfiable by a--- store that throws everything away.------ The flip is the load-bearing case. Legalization is confluent, so which flips--- fire and in what order is not observable in the topology that comes out. A--- payload that rode through a flip would make it observable in the payload--- plane, and a triangulation that is a normal form in one component and a--- history in another is not a normal form.-testRewritePayloadIdentity :: IO ()-testRewritePayloadIdentity = do-  let defaults = ElementDefaults (0 :: Int) (0 :: Int) (0 :: Int)-      target = Point 0.001_37 (-0.002_11)-      corpus = randomPoints 0x1a2b3c4d 512-  built <- requireRight "rewrite identity base" (delaunay defaults (V.fromList corpus))-  let base = buildTriangulation built-      directedTable = labelTable (map (directedKeyOf base) (directedEdges base))-      undirectedTable = labelTable (map (undirectedKeyOf base) (undirectedEdges base))-      faceTable = labelTable (map (faceKeyOf base) (innerFaces base))--  -- Keys identify elements only if they are unique, so the premise is checked-  -- rather than assumed: a collapsed table would silently weaken everything-  -- below it into a test of nothing.-  assertEqual "directed keys are unique" (length (directedEdges base)) (Map.size directedTable)-  assertEqual "undirected keys are unique" (length (undirectedEdges base)) (Map.size undirectedTable)-  assertEqual "face keys are unique" (length (innerFaces base)) (Map.size faceTable)--  let withDirected =-        List.foldl' (\t e -> setDirectedEdgeData t e (directedTable Map.! directedKeyOf base e)) base (directedEdges base)-      withUndirected =-        List.foldl' (\t e -> setUndirectedEdgeData t e (undirectedTable Map.! undirectedKeyOf base e)) withDirected (undirectedEdges base)-      labelled =-        List.foldl' (\t f -> setFaceData t f (faceTable Map.! faceKeyOf base f)) withUndirected (innerFaces base)-  inserted <- requireRight "rewrite identity insert" (insert labelled target)-  let result = insertionTriangulation inserted-  assertValid "rewrite identity result" result-  assertEqual "the point was genuinely inserted" Inserted (insertionDisposition inserted)--  -- Both directions, on every plane. A surviving element keeps exactly its-  -- label; everything else holds the default and nothing else.-  assertPayloadSurvival "directed" directedTable-    [(directedKeyOf result e, directedEdgeData result e) | e <- directedEdges result]-  assertPayloadSurvival "undirected" undirectedTable-    [(undirectedKeyOf result e, undirectedEdgeData result e) | e <- undirectedEdges result]-  assertPayloadSurvival "face" faceTable-    [(faceKeyOf result f, faceData result f) | f <- innerFaces result]--  -- The flip signature. Every flip after an insertion joins the new vertex to-  -- the far corner of a cavity quad, so a new edge is not evidence of one — a-  -- split produces those too. A DESTROYED edge is: splitting a face destroys-  -- nothing and adds exactly three edges and two faces, which is asserted here-  -- so that the arithmetic holds, and under it every base key that is gone from-  -- the result was flipped away. Without this the paragraphs above are a claim-  -- about splits alone.-  assertEqual "the point landed strictly inside a face"-    (numUndirectedEdges base + 3, numFaces base + 2)-    (numUndirectedEdges result, numFaces result)-  let surviving = Set.fromList (map (undirectedKeyOf result) (undirectedEdges result))-      flippedAway = filter (`Set.notMember` surviving) (Map.keys undirectedTable)-  when (null flippedAway) (fail "the insertion caused no flip, so the flip case went unchecked")--  -- The same claim across one transaction that both retires and creates.-  -- Removal swap-compacts, which leaves a retired element's payload sitting in-  -- the slot it vacated; the inserts that follow are handed those slots back.-  -- Nothing else in the suite makes an allocation reissue a used slot.-  let doomed = take 60 corpus-      arrivals = randomPoints 0x5f3a19c2 60-  (_, edited, _) <--    requireRight "rewrite identity session" $-      withSession labelled (length arrivals) $ do-        mapM_-          (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ())))-          doomed-        mapM_ insertVertex arrivals-  assertValid "rewrite identity session" edited-  assertPayloadSurvival "session directed" directedTable-    [(directedKeyOf edited e, directedEdgeData edited e) | e <- directedEdges edited]-  assertPayloadSurvival "session undirected" undirectedTable-    [(undirectedKeyOf edited e, undirectedEdgeData edited e) | e <- undirectedEdges edited]-  assertPayloadSurvival "session face" faceTable-    [(faceKeyOf edited f, faceData edited f) | f <- innerFaces edited]--directedKeyOf-  :: Triangulation mode vertex directed undirected face-  -> DirectedEdgeId-  -> (Point, Point)-directedKeyOf triangulation edge =-  ( vertexPoint triangulation (origin triangulation edge)-  , vertexPoint triangulation (destination triangulation edge)-  )--undirectedKeyOf-  :: Triangulation mode vertex directed undirected face-  -> UndirectedEdgeId-  -> (Point, Point)-undirectedKeyOf triangulation edge =-  case undirectedEndpoints triangulation edge of-    (from, to) ->-      let (left, right) = (vertexPoint triangulation from, vertexPoint triangulation to)-       in if left <= right then (left, right) else (right, left)--faceKeyOf-  :: Triangulation mode vertex directed undirected face-  -> FaceId-  -> [Point]-faceKeyOf triangulation face = sort (map (vertexPoint triangulation) (faceVertices triangulation face))--labelTable :: Ord key => [key] -> Map.Map key Int-labelTable keys = Map.fromList (zip keys [1 ..])---- | Every element carries the label its key was given, or the default if its--- key is new. Both counts are asserted too: a store that kept everything and a--- store that kept nothing each satisfy one half of this on its own.-assertPayloadSurvival :: (Ord key, Show key) => String -> Map.Map key Int -> [(key, Int)] -> IO ()-assertPayloadSurvival plane labels elements = do-  let kept = length (filter ((`Map.member` labels) . fst) elements)-  when (kept == 0) (fail (plane <> ": the insertion perturbed every element"))-  when (kept == length elements) (fail (plane <> ": the insertion perturbed no element"))-  forM_ elements $ \(key, payload) ->-    assertEqual (plane <> " label at " <> show key) (Map.findWithDefault 0 key labels) payload--testPointLocationAndHints :: IO ()-testPointLocationAndHints = do-  built <- requirePointBuild "location" (randomPoints 0x1234_5678 1200)-  let triangulation = buildTriangulation built-  queries <- traverse (requireQueryPoint "location query") (take 250 (randomPoints 0xdead_beef 250))-  let-      baseline = sum [locationWalkSteps stats | query <- queries, let (_, stats) = locatePointWithHint triangulation Nothing query]-  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint 16 triangulation)-  let hinted = sum-        [ locationWalkSteps stats-        | query <- queries-        , let hint = hierarchyHint hierarchy query-              (_, stats) = locatePointWithHint triangulation hint query-        ]-  unless (hinted <= baseline) $-    fail ("hierarchy hint increased aggregate walking: " <> show (baseline, hinted))-  staleHintQuery <- requireQueryPoint "stale hint query" (Point 0.125 (-0.375))-  let staleVertexHint = VertexHint (VertexId (fromIntegral (numVertices triangulation)))-  assertEqual-    "stale vertex hint falls back to the canonical start face"-    (locatePointWithHint triangulation Nothing staleHintQuery)-    (locatePointWithHint triangulation (Just staleVertexHint) staleHintQuery)-  forM_ (vertices triangulation) $ \vertex -> do-    vertexQuery <- requireQueryPoint "vertex lookup" (vertexPoint triangulation vertex)-    assertEqual "vertex lookup" (OnVertex vertex) (locatePoint triangulation vertexQuery)-  let insertedPoint = Point 0.1234567 (-0.2345678)-  inserted <- requireRight "hierarchy incremental source" (insert triangulation insertedPoint)-  let updatedTriangulation = insertionTriangulation inserted-  updatedHierarchy <--    requireRight-      "hierarchy incremental update"-      ( updateHierarchyAfterInsertion-          hierarchy-          insertedPoint-          (insertionVertex inserted)-          (insertionDisposition inserted)-      )-  rebuiltHierarchy <- requireRight "hierarchy reference rebuild" (buildHierarchyHint 16 updatedTriangulation)-  assertEqual "incremental hierarchy equals canonical rebuild" rebuiltHierarchy updatedHierarchy---- The hierarchy replaces its level-to-base correspondence with the arithmetic--- claim that level-local handle @j@ names base vertex @j * branch@. Query each--- sampled vertex with its own position: the descent must return that very--- vertex, at every branch factor and along the whole of level 0. A stride the--- construction does not actually obey shows up here as a named mismatch.-testHierarchyNestingLaw :: IO ()-testHierarchyNestingLaw = do-  built <- requirePointBuild "hierarchy nesting" (randomPoints 0x0f1e_2d3c 900)-  let triangulation = buildTriangulation built-  forM_ [2, 3, 16] $ \branch -> do-    hierarchy <- requireRight ("hierarchy build at branch " <> show branch) (buildHierarchyHint branch triangulation)-    let sampled = [0, branch .. numVertices triangulation - 1]-    forM_ sampled $ \index -> do-      let vertex = VertexId (fromIntegral index)-      vertexQuery <- requireQueryPoint "sampled hierarchy vertex" (vertexPoint triangulation vertex)-      assertEqual-        ("branch " <> show branch <> " descent onto sampled vertex " <> show index)-        (Just (VertexHint vertex))-        (hierarchyHint hierarchy vertexQuery)--testSibsonInterpolation :: IO ()-testSibsonInterpolation = do-  built <- requirePointBuild "sibson" (gridPoints 9 9)-  let triangulation = buildTriangulation built-      queryPoint = Point 3.25 4.4-      linear vertex = let Point x y = vertexPoint triangulation vertex in 2 * x - 3 * y + 5-      expected = let Point x y = queryPoint in 2 * x - 3 * y + 5-  query <- requireQueryPoint "Sibson query" queryPoint-  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)-  result <- stToIO (naturalNeighborWeights workspace Nothing query)-  let weights = naturalNeighborValues result-  unless (V.length weights >= 3) $ fail "Sibson query did not discover a natural-neighbor cavity"-  assertNear "Sibson partition" 1.0e-11 1 (V.sum (V.map snd weights))-  unless (V.all ((>= (-1.0e-12)) . snd) weights) $ fail "Sibson produced a negative weight"-  (folded, _, foldedStats) <- stToIO (foldNaturalNeighborWeights-    (\total vertex weight -> total + weight * linear vertex)-    0-    workspace-    Nothing-    query)-  assertNear "allocation-free Sibson fold" 2.0e-9 expected folded-  assertEqual "Sibson fold neighbor count" (V.length weights) (interpolationNaturalNeighbors foldedStats)-  (interpolated, _) <- stToIO (interpolateNaturalNeighbor linear workspace Nothing query)-  case interpolated of-    Nothing -> fail "Sibson interpolation rejected an interior query"-    Just value -> assertNear "Sibson affine precision" 2.0e-9 expected value-  let gradients = estimateGradients linear triangulation-  V.forM_ gradients $ \(gx, gy) -> do-    assertNear "planar gradient x" 1.0e-9 2 gx-    assertNear "planar gradient y" 1.0e-9 (-3) gy-  let gradient (VertexId raw) = gradients V.! fromIntegral raw-  (gradientValue, _) <- stToIO (interpolateNaturalNeighborGradient linear gradient 0.5 workspace Nothing query)-  case gradientValue of-    Nothing -> fail "gradient natural-neighbor interpolation rejected an interior query"-    Just value -> assertNear "gradient affine precision" 2.0e-9 expected value-  unless (workspaceBytes workspace > 0) $ fail "Sibson workspace byte accounting is empty"--testVoronoiDual :: IO ()-testVoronoiDual = do-  built <- requirePointBuild "voronoi" [Point 0 0, Point 2 0, Point 0 2, Point 2 2, Point 1 1]-  let triangulation = buildTriangulation built-  assertEqual "Voronoi face count" (numVertices triangulation) (length (voronoiFaces triangulation))-  assertEqual "directed dual edge count" (numDirectedEdges triangulation) (length (directedVoronoiEdges triangulation))-  assertEqual "undirected dual edge count" (numUndirectedEdges triangulation) (length (undirectedVoronoiEdges triangulation))-  forM_ (directedVoronoiEdges triangulation) $ \edge -> do-    assertEqual "dual double reversal" edge (reverseVoronoiEdge (reverseVoronoiEdge edge))-    assertEqual "dual next/previous" edge (voronoiPrevious triangulation (voronoiNext triangulation edge))-    assertEqual-      "dual face/site"-      (origin triangulation (asDelaunayDirectedEdge edge))-      (voronoiFaceSite (voronoiIncidentFace triangulation edge))-    case voronoiEdgeGeometry triangulation edge of-      Nothing -> fail "valid dual edge has no geometry"-      Just _ -> pure ()-  case directedVoronoiEdges triangulation of-    [] -> fail "Voronoi test produced no directed dual edge"-    first : _ -> do-      handle <- requireJust "dynamic Voronoi edge" (VoronoiDynamic.directedVoronoiEdgeHandle triangulation first)-      assertEqual "dynamic dual fix" first (VoronoiDynamic.fixDirectedVoronoiEdge handle)-      assertEqual "dynamic dual reversal" first (VoronoiDynamic.fixDirectedVoronoiEdge (VoronoiDynamic.voronoiEdgeReverseH (VoronoiDynamic.voronoiEdgeReverseH handle)))-      assertEqual "dynamic dual/primal conversion" (asDelaunayDirectedEdge first) (Dynamic.fixDirectedEdge (VoronoiDynamic.voronoiEdgeAsDelaunayH handle))-      let dualFace = VoronoiDynamic.voronoiEdgeFaceH handle-      assertEqual "dynamic dual face site" (origin triangulation (asDelaunayDirectedEdge first)) (Dynamic.fixVertex (VoronoiDynamic.voronoiFaceSiteH dualFace))-      let source = VoronoiDynamic.voronoiEdgeFromH handle-      case VoronoiDynamic.voronoiVertexAsDelaunayFaceH source of-        Just inner -> case VoronoiDynamic.voronoiVertexPositionH source of-          Nothing -> fail "inner dynamic Voronoi vertex has no position"-          Just voronoiPosition -> assertEqual "inner dynamic Voronoi position" (Dynamic.innerFaceCircumcenter inner) (Just voronoiPosition)-        Nothing -> unless (isJust (VoronoiDynamic.voronoiVertexAsOuterEdgeH source)) $-          fail "outer dynamic Voronoi vertex has no defining edge"--testRemoval :: IO ()-testRemoval = do-  built <- requirePointBuild "removal" [Point 0 0, Point 3 0, Point 3 3, Point 0 3, Point 1.5 1.5]-  let triangulation = buildTriangulation built-  assertEqual-    "out-of-range removal obstruction"-    (Left (RemovalVertexOutOfRange (VertexId 5) 5))-    (void (removeVertex triangulation (VertexId 5)))-  removedInterior <- requireRight "interior removal" (removeVertex triangulation (VertexId 4))-  assertEqual "interior removed point" (Point 1.5 1.5) (removalOutcomePoint (removalOutcome removedInterior))-  assertEqual "interior removal count" 4 (numVertices (removalTriangulation removedInterior))-  assertValid "interior removal" (removalTriangulation removedInterior)-  let beforeHullRemoval = removalTriangulation removedInterior-      previousLastVertex = VertexId (fromIntegral (numVertices beforeHullRemoval - 1))-      swappedPoint = vertexPoint beforeHullRemoval previousLastVertex-      swappedData = vertexData beforeHullRemoval previousLastVertex-  removedHull <- requireRight "hull removal" (removeVertex beforeHullRemoval (VertexId 0))-  assertEqual "hull removal count" 3 (numVertices (removalTriangulation removedHull))-  case removalOutcomeSwap (removalOutcome removedHull) of-    Nothing -> fail "hull removal omitted the swap-compacted vertex handle"-    Just (swappedIn, swappedInPoint) -> do-      assertEqual "hull swapped-in handle" (VertexId 0) swappedIn-      assertEqual "hull swapped-in point" swappedPoint (vertexPoint (removalTriangulation removedHull) swappedIn)-      assertEqual "hull swapped-in payload" swappedData (vertexData (removalTriangulation removedHull) swappedIn)-      -- The reported position must be the one the arena now holds, bit for-      -- bit: a caller seeding a search from it is seeding from the mesh.-      assertEqual "hull swapped-in reported position" swappedPoint swappedInPoint-  assertValid "hull removal" (removalTriangulation removedHull)--  let afterHullRemoval = removalTriangulation removedHull-      lastVertex = VertexId (fromIntegral (numVertices afterHullRemoval - 1))-  removedLast <- requireRight "last-vertex removal" (removeVertex afterHullRemoval lastVertex)-  assertEqual-    "removing the last vertex relocates nothing"-    Nothing-    (removalOutcomeSwap (removalOutcome removedLast))-  assertEqual "last-vertex removal count" 2 (numVertices (removalTriangulation removedLast))-  assertValid "last-vertex removal" (removalTriangulation removedLast)--  lineBuild <- requirePointBuild "line removal" [Point 0 0, Point 1 0, Point 2 0, Point 3 0]-  lineMiddle <- requireRight "line middle removal" (removeVertex (buildTriangulation lineBuild) (VertexId 1))-  assertEqual "line middle count" 3 (numVertices (removalTriangulation lineMiddle))-  assertValid "line middle removal" (removalTriangulation lineMiddle)--  degreeThreeBuild <--    requirePointBuild-      "degree-three removal"-      [Point 0 0, Point 4 0, Point 0 4, Point 1 1]-  let degreeThreeMapping = buildInputVertices degreeThreeBuild-  case if 3 < sizeofPrimArray degreeThreeMapping-        then Just (VertexId (indexPrimArray degreeThreeMapping 3))-        else Nothing of-    Nothing -> fail "degree-three removal input mapping omitted the interior vertex"-    Just centerVertex -> do-      degreeThree <--        requireRight-          "degree-three interior removal"-          (removeVertex (buildTriangulation degreeThreeBuild) centerVertex)-      assertEqual "degree-three removal count" 3 (numVertices (removalTriangulation degreeThree))-      assertValid "degree-three interior removal" (removalTriangulation degreeThree)--  -- A removal hands its vertex's whole ring to edge/face cleanup, so a-  -- high-degree star is the only shape that exercises the ordered-set path-  -- there; an ordinary mesh keeps degrees near six and never leaves the-  -- insertion sort. Radii are jittered so no four rim points are cocircular.-  let rimCount = 48-      rimPoint index =-        let angle = 2 * pi * fromIntegral index / fromIntegral rimCount-            radius = 1 + 0.001 * fromIntegral (index `mod` 7)-         in Point (radius * cos angle) (radius * sin angle)-  starBuild <--    requirePointBuild-      "high-degree removal"-      (Point 0 0 : map rimPoint [0 .. rimCount - 1])-  let starTriangulation = buildTriangulation starBuild-      starCentre = VertexId (indexPrimArray (buildInputVertices starBuild) 0)-      centreDegree =-        length-          [ ()-          | edge <- undirectedEdges starTriangulation-          , let (from, to) = undirectedEndpoints starTriangulation edge-          , from == starCentre || to == starCentre-          ]-  assertEqual "high-degree centre ring" rimCount centreDegree-  starRemoved <--    requireRight "high-degree interior removal" (removeVertex starTriangulation starCentre)-  assertEqual "high-degree removal count" rimCount (numVertices (removalTriangulation starRemoved))-  assertEqual-    "high-degree removed point"-    (Point 0 0)-    (removalOutcomePoint (removalOutcome starRemoved))-  assertValid "high-degree interior removal" (removalTriangulation starRemoved)--  -- A transaction that refuses publishes nothing: the refusal is the whole-  -- answer, so no half-remeshed arena can reach a caller as a triangulation.-  let sessionRefusal = RemovalVertexOutOfRange (VertexId 99) 5-  assertEqual-    "a refused session publishes nothing"-    (Left sessionRefusal)-    (void (withSession triangulation 1 (refuse sessionRefusal :: Session s (Point) () () () ())))--  -- Refusal short-circuits: an edit after it never runs, so the mesh the-  -- transaction abandoned is the mesh it was handed.-  assertEqual-    "a refusal abandons the edits behind it"-    (Left sessionRefusal)-    ( void-        ( withSession-            triangulation-            1-            ( ( do-                  _ <- removeAt (Point 0 0)-                  _ <- refuse sessionRefusal-                  removeAt (Point 1 1)-              ) ::-                Session s (Point) () () () (Maybe (RemovalOutcome (Point)))-            )-        )-    )--  -- The two point-keyed entries must publish one story. A handle-keyed removal-  -- locates nothing, while the coordinate-keyed route resolves the same site-  -- through the exact derived identity section rather than a topological walk.-  -- Both therefore charge no location steps and retire the same vertex.-  handleRemoval <- requireRight "handle removal stats" (removeVertex triangulation (VertexId 4))-  assertEqual-    "a handle-keyed removal locates nothing"-    0-    (statLocationWalkSteps (removalStats handleRemoval))-  locatedRemoval <--    requireRight "point removal stats" (locateAndRemove triangulation (Point 1.5 1.5))-  pointRemoval <- requireJust "point removal located a vertex" locatedRemoval-  assertEqual-    "a point-keyed removal performs no topological location walk"-    0-    (statLocationWalkSteps (removalStats pointRemoval))-  assertEqual-    "point-keyed and handle-keyed removal publish the same mesh"-    (canonicalEdges (removalTriangulation handleRemoval))-    (canonicalEdges (removalTriangulation pointRemoval))--testConstrainedDelaunay :: IO ()-testConstrainedDelaunay = do-  base <- requirePointBuild "CDT base" [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 1 1, Point 3 3, Point 1 3, Point 3 1]-  let cdt0 = fromDelaunay (buildTriangulation base)-  diagonalBatch <--    requireRight-      "constraint recovery"-      (recoverConstraints cdt0 (V.singleton (VertexId 0, VertexId 2)))-  (diagonalPath, diagonalAdded) <--    requireAcceptedConstraint "constraint recovery" diagonalBatch-  let cdt1 = constraintBatchTriangulation diagonalBatch-  when (V.null diagonalPath) $ fail "constraint recovery returned an empty path"-  assertEqual "constraint count" diagonalAdded (numConstraints cdt1)-  assertCdtValid "constraint recovery" cdt1-  conflictBatch <--    requireRight-      "atomic conflict"-      (recoverConstraints cdt1 (V.singleton (VertexId 1, VertexId 3)))-  case V.toList (constraintBatchOutcomes conflictBatch) of-    [ConstraintRejected _] -> pure ()-    outcomes ->-      fail-        ( "crossing constraint was not rejected atomically: "-            <> show outcomes-        )-  assertEqual-    "crossing rejection preserves topology"-    cdt1-    (constraintBatchTriangulation conflictBatch)-  let constraintProgram =-        V.fromList-          [ (VertexId 0, VertexId 2)-          , (VertexId 1, VertexId 3)-          , (VertexId 4, VertexId 6)-          ]-  wholeProgram <--    requireRight-      "whole constraint program"-      (recoverConstraints cdt0 constraintProgram)-  (singletonTriangulation, singletonOutcomes) <--    requireRight-      "singleton constraint program"-      (V.foldM' replayConstraintRequest (cdt0, []) constraintProgram)-  assertEqual-    "batch outcomes equal singleton descent"-    (V.toList (constraintBatchOutcomes wholeProgram))-    (reverse singletonOutcomes)-  assertEqual-    "batch topology equals singleton descent"-    singletonTriangulation-    (constraintBatchTriangulation wholeProgram)-  assertBatchStats "whole constraint program" wholeProgram-  forM_ [0 .. 7 :: Int] $ \batchIndex -> do-    randomizedBuild <--      requirePointBuild-        ("random constraint batch " <> show batchIndex)-        (randomPoints (0x6a09_e667_f3bc_c909 + fromIntegral batchIndex) 40)-    let randomizedBase = fromDelaunay (buildTriangulation randomizedBuild)-        mapping = buildInputVertices randomizedBuild-        handles = V.generate (sizeofPrimArray mapping) (VertexId . indexPrimArray mapping)-        requests = V.take 18 (V.zip handles (V.reverse handles))-    randomizedBatch <--      requireRight-        ("random whole constraint batch " <> show batchIndex)-        (recoverConstraints randomizedBase requests)-    (randomizedSingleton, randomizedOutcomes) <--      requireRight-        ("random singleton constraint batch " <> show batchIndex)-        (V.foldM' replayConstraintRequest (randomizedBase, []) requests)-    assertEqual-      ("random batch outcomes equal singleton descent " <> show batchIndex)-      (V.toList (constraintBatchOutcomes randomizedBatch))-      (reverse randomizedOutcomes)-    assertEqual-      ("random batch topology equals singleton descent " <> show batchIndex)-      randomizedSingleton-      (constraintBatchTriangulation randomizedBatch)-    assertBatchStats-      ("random constraint batch " <> show batchIndex)-      randomizedBatch-    assertCdtValid-      ("random constraint batch " <> show batchIndex)-      (constraintBatchTriangulation randomizedBatch)-  -- Batch admission returns the first typed obstruction. Callers that need a-  -- complete diagnosis ask the explicit corridor query and alone pay for it.-  diagnosisBatch <--    requireRight-      "conflict diagnosis"-      (recoverConstraints cdt1 (V.singleton (VertexId 6, VertexId 7)))-  case V.toList (constraintBatchOutcomes diagnosisBatch) of-    [ConstraintRejected blocking] -> do-      let diagnosed =-            fmap-              asUndirected-              (getConflictingEdgesBetweenVertices cdt1 (VertexId 6) (VertexId 7))-      case diagnosed of-        [] -> fail "explicit conflict diagnosis named no edge"-        firstBlocking : _ ->-          assertEqual "batch rejection is the first corridor obstruction" firstBlocking blocking-      assertEqual-        "explicit conflict diagnosis is deduplicated"-        (length diagnosed)-        (length (Set.fromList diagnosed))-      forM_ diagnosed $ \edge ->-        unless (isConstraintEdge cdt1 edge) $-          fail ("explicit conflict diagnosis named a non-constraint edge: " <> show edge)-    outcomes ->-      fail-        ( "a constraint crossing the diagonal was not rejected: "-            <> show outcomes-        )--  split <- requireRight "constraint split" (addConstraintAndSplit id cdt1 (VertexId 1) (VertexId 3))-  let splitCdt = constraintRecoveryTriangulation split-  unless (numVertices splitCdt > numVertices cdt1) $ fail "constraint split did not insert an intersection vertex"-  assertCdtValid "constraint split" splitCdt--  -- The batch splitter must agree with singleton descent, including across a-  -- suspension: every vertical below is constrained only inside the batch-  -- itself, so no census against the base can reserve for its crossings and-  -- the chunk's vertex reservation exhausts mid-batch. The driver publishes,-  -- re-reserves against the published mesh, and resumes; the final mesh must-  -- not know any of that happened.-  let bandColumns = [0 .. 7 :: Int]-      bandVertices =-        V.fromList-          ( Point 0 5-              : Point 90 5-              : concat-                  [ [Point x 10, Point x 0]-                  | column <- bandColumns-                  , let x = 10 * fromIntegral column + 5-                  ]-          )-  bandBuild <--    requireRight-      "split band base"-      (constrainedDelaunayMaximal unitElementDefaults bandVertices V.empty)-  let bandAcceptedBuild = cdtAcceptedBuild bandBuild-      bandMapping = buildInputVertices bandAcceptedBuild-      bandHandle input = VertexId (indexPrimArray bandMapping input)-      bandBase = buildTriangulation bandAcceptedBuild-      bandRequests =-        V.fromList-          ( (bandHandle 0, bandHandle 1)-              : [ (bandHandle (2 * column + 2), bandHandle (2 * column + 3))-                | column <- bandColumns-                ]-          )-      replaySplit-        :: ConstrainedDelaunayTriangulation (Point)-        -> (VertexId, VertexId)-        -> Either (CdtError) (ConstrainedDelaunayTriangulation (Point))-      replaySplit triangulation request =-        constraintRecoveryTriangulation-          <$> uncurry (addConstraintAndSplit id triangulation) request-  bandBatch <--    requireRight-      "split batch"-      (addConstraintsAndSplit id bandBase bandRequests)-  bandDescent <--    requireRight-      "split singleton descent"-      (V.foldM' replaySplit bandBase bandRequests)-  assertEqual-    "split batch topology equals singleton descent"-    bandDescent-    (constraintRecoveryTriangulation bandBatch)-  assertEqual-    "split batch added one vertex per crossing"-    (numVertices bandBase + length bandColumns)-    (numVertices (constraintRecoveryTriangulation bandBatch))-  assertCdtValid "split batch" (constraintRecoveryTriangulation bandBatch)--  -- The same law with the reservation exhausting inside one corridor rather-  -- than between corridors: the closing horizontal crosses two constraints-  -- the census can see and fifteen it cannot, so the corridor suspends with-  -- its cursor mid-walk and the resumed transaction continues from the last-  -- split vertex, not from the corridor's start.-  let laceColumns = [0 .. 16 :: Int]-      laceVertices =-        V.fromList-          ( Point 0 5-              : Point 90 5-              : concat-                  [ [Point x 10, Point x 0]-                  | column <- laceColumns-                  , let x = 5 * fromIntegral column + 5-                  ]-          )-      laceBuiltIn = V.fromList [(2, 3), (34, 35)]-  laceBuild <--    requireRight-      "split lace base"-      (constrainedDelaunayMaximal unitElementDefaults laceVertices laceBuiltIn)-  let laceAcceptedBuild = cdtAcceptedBuild laceBuild-      laceMapping = buildInputVertices laceAcceptedBuild-      laceHandle input = VertexId (indexPrimArray laceMapping input)-      laceBase = buildTriangulation laceAcceptedBuild-      laceRequests =-        V.fromList-          ( [ (laceHandle (2 * column + 2), laceHandle (2 * column + 3))-            | column <- [1 .. 15]-            ]-              <> [(laceHandle 0, laceHandle 1)]-          )-  laceBatch <--    requireRight-      "split lace batch"-      (addConstraintsAndSplit id laceBase laceRequests)-  laceDescent <--    requireRight-      "split lace singleton descent"-      (V.foldM' replaySplit laceBase laceRequests)-  assertEqual-    "split lace batch topology equals singleton descent"-    laceDescent-    (constraintRecoveryTriangulation laceBatch)-  assertEqual-    "split lace batch added one vertex per crossing"-    (numVertices laceBase + length laceColumns)-    (numVertices (constraintRecoveryTriangulation laceBatch))-  assertCdtValid "split lace batch" (constraintRecoveryTriangulation laceBatch)--  let verticesInput :: V.Vector (Point)-      verticesInput = V.fromList [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 0 0]-      constraintsInput = V.fromList [(0, 2), (1, 3), (4, 1)]-  bulk <- requireRight "stable CDT bulk load" (constrainedDelaunayMaximal unitElementDefaults verticesInput constraintsInput)-  let bulkAcceptedBuild = cdtAcceptedBuild bulk-      bulkMapping = buildInputVertices bulkAcceptedBuild-  unless (sizeofPrimArray bulkMapping > 4) $ fail "buildInputVertices out of bounds"-  assertEqual "stable duplicate reroute" (VertexId (indexPrimArray bulkMapping 0)) (VertexId (indexPrimArray bulkMapping 4))-  assertEqual "conflict reporting" 1 (V.length (cdtRejectedConstraints bulk))-  assertCdtValid "stable CDT bulk load" (buildTriangulation bulkAcceptedBuild)--testAnnotatedConstrainedUnion :: IO ()-testAnnotatedConstrainedUnion = do-  let leftPoints :: V.Vector (Point)-      leftPoints = V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2]-      rightPoints :: V.Vector (Point)-      rightPoints = V.fromList [Point 2 0, Point 4 0, Point 4 2, Point 2 2]-      thirdPoints :: V.Vector (Point)-      thirdPoints = V.fromList [Point 4 0, Point 6 0, Point 6 2, Point 4 2]-  leftBuild <--    requireRight "annotated constrained union left" $-      constrainedDelaunay-        unitElementDefaults-        leftPoints-        (V.singleton (0, 2))-  rightBuild <--    requireRight "annotated constrained union right" $-      constrainedDelaunay-        unitElementDefaults-        rightPoints-        (V.singleton (0, 2))-  thirdBuild <--    requireRight "annotated constrained union third" $-      constrainedDelaunay-        unitElementDefaults-        thirdPoints-        (V.singleton (0, 2))-  let left =-        mapVertices-          (const (Set.singleton "left"))-          (buildTriangulation leftBuild)-      right =-        mapVertices-          (const (Set.singleton "right"))-          (buildTriangulation rightBuild)-      third =-        mapVertices-          (const (Set.singleton "third"))-          (buildTriangulation thirdBuild)-  joined <--    requireRight-      "annotated constrained union"-      (unionConstrainedWith Set.union left right)-  let expectedAnnotations =-        Map.fromList-          [ (Point 0 0, Set.singleton "left")-          , (Point 0 2, Set.singleton "left")-          , (Point 2 0, Set.fromList ["left", "right"])-          , (Point 2 2, Set.fromList ["left", "right"])-          , (Point 4 0, Set.singleton "right")-          , (Point 4 2, Set.singleton "right")-          ]-      actualAnnotations =-        Map.fromList-          [ (vertexPoint joined vertex, vertexData joined vertex)-          | vertex <- vertices joined-          ]-      expectedConstraints =-        Set.union-          (Set.fromList (V.toList (constraintSegments left)))-          (Set.fromList (V.toList (constraintSegments right)))-  assertEqual-    "annotated constrained union preserves and combines site payloads"-    expectedAnnotations-    actualAnnotations-  assertEqual-    "annotated constrained union preserves both constraint sections"-    expectedConstraints-    (Set.fromList (V.toList (constraintSegments joined)))-  assertCdtValid "annotated constrained union" joined-  commuted <--    requireRight-      "annotated constrained union commuted"-      (unionConstrainedWith Set.union right left)-  assertEqual-    "annotated constrained union is commutative under a commutative payload combiner"-    joined-    commuted-  leftAssociated <--    requireRight-      "annotated constrained union left-associated"-      (unionConstrainedWith Set.union joined third)-  rightPair <--    requireRight-      "annotated constrained union right pair"-      (unionConstrainedWith Set.union right third)-  rightAssociated <--    requireRight-      "annotated constrained union right-associated"-      (unionConstrainedWith Set.union left rightPair)-  assertEqual-    "annotated constrained union is associative wherever both descents are admitted"-    leftAssociated-    rightAssociated-  unitJoined <--    requireRight-      "unit constrained union specialization"-      ( unionConstrained-          (mapVertices (const ()) left)-          (mapVertices (const ()) right)-      )-  assertEqual-    "unit constrained union specializes annotated constrained union"-    unitJoined-    (mapVertices (const ()) joined)---- | Sequential extension retains the base authority as the transaction root.--- The semantic result agrees with canonical union on sites and constraint--- sections, but its receipt proves that only the incoming constraint section--- was interpreted.-testAsymmetricConstrainedExtension :: IO ()-testAsymmetricConstrainedExtension = do-  let basePoints :: V.Vector (Point)-      basePoints = V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2]-      extensionPoints :: V.Vector (Point)-      extensionPoints = V.fromList [Point 2 0, Point 4 0, Point 4 2, Point 2 2]-  baseBuild <--    requireRight-      "asymmetric extension base"-      (constrainedDelaunay unitElementDefaults basePoints (V.singleton (0, 2)))-  extensionBuild <--    requireRight-      "asymmetric extension incoming"-      (constrainedDelaunay unitElementDefaults extensionPoints (V.singleton (0, 2)))-  let base = mapVertices (const (Set.singleton "base")) (buildTriangulation baseBuild)-      extension = mapVertices (const (Set.singleton "extension")) (buildTriangulation extensionBuild)-      expectedAnnotations =-        Map.fromList-          [ (Point 0 0, Set.singleton "base")-          , (Point 0 2, Set.singleton "base")-          , (Point 2 0, Set.fromList ["base", "extension"])-          , (Point 2 2, Set.fromList ["base", "extension"])-          , (Point 4 0, Set.singleton "extension")-          , (Point 4 2, Set.singleton "extension")-          ]-      expectedConstraints =-        Set.union-          (Set.fromList (V.toList (constraintSegments base)))-          (Set.fromList (V.toList (constraintSegments extension)))-      baseAnnotations =-        Map.fromList-          [ (vertexPoint base vertex, vertexData base vertex)-          | vertex <- vertices base-          ]-      extensionAnnotations =-        Map.fromList-          [ (vertexPoint extension vertex, vertexData extension vertex)-          | vertex <- vertices extension-          ]-  baseAnnotationsBefore <- evaluate (force baseAnnotations)-  baseConstraintsBefore <- evaluate (force (constraintSegments base))-  extensionAnnotationsBefore <- evaluate (force extensionAnnotations)-  extensionConstraintsBefore <- evaluate (force (constraintSegments extension))-  extendedResult <--    requireRight-      "asymmetric constrained extension"-      (extendConstrainedWith Set.union base extension)-  let constraintBatch = constrainedExtensionConstraintBatch extendedResult-      extended = constraintBatchTriangulation constraintBatch-      receipt = constraintBatchStats constraintBatch-      buildReceipt = constrainedExtensionBuildStats extendedResult-      actualAnnotations =-        Map.fromList-          [ (vertexPoint extended vertex, vertexData extended vertex)-          | vertex <- vertices extended-          ]-  assertEqual-    "asymmetric extension preserves base and combines coincident site payloads"-    expectedAnnotations-    actualAnnotations-  assertEqual-    "asymmetric extension preserves resident and incoming constraint sections"-    expectedConstraints-    (Set.fromList (V.toList (constraintSegments extended)))-  assertEqual-    "asymmetric extension does not mutate the frozen base predecessor sites"-    baseAnnotationsBefore-    ( Map.fromList-        [ (vertexPoint base vertex, vertexData base vertex)-        | vertex <- vertices base-        ]-    )-  assertEqual-    "asymmetric extension does not mutate the frozen base predecessor constraints"-    baseConstraintsBefore-    (constraintSegments base)-  assertEqual-    "asymmetric extension does not mutate the frozen incoming predecessor sites"-    extensionAnnotationsBefore-    ( Map.fromList-        [ (vertexPoint extension vertex, vertexData extension vertex)-        | vertex <- vertices extension-        ]-    )-  assertEqual-    "asymmetric extension does not mutate the frozen incoming predecessor constraints"-    extensionConstraintsBefore-    (constraintSegments extension)-  assertEqual-    "asymmetric extension recovers only incoming constraints"-    (V.length (constraintSegments extension))-    (constraintBatchRequests receipt)-  assertEqual-    "asymmetric extension returns one outcome for each incoming constraint"-    (V.length (constraintSegments extension))-    (V.length (constraintBatchOutcomes constraintBatch))-  assertEqual-    "asymmetric extension admits every incoming constraint"-    (V.length (constraintSegments extension))-    (constraintBatchAccepted receipt)-  assertEqual-    "asymmetric extension reports no incoming constraint rejection"-    0-    (constraintBatchRejected receipt)-  assertEqual-    "asymmetric extension charges every incoming site once"-    (V.length extensionPoints)-    (statInputPoints buildReceipt)-  assertEqual-    "asymmetric extension distinguishes occupied incoming sites"-    2-    (statExistingPoints buildReceipt)-  assertEqual-    "asymmetric extension distinguishes newly materialized incoming sites"-    2-    (statUniquePoints buildReceipt)-  assertCdtValid "asymmetric constrained extension" extended--  crossingBuild <--    requireRight-      "asymmetric extension crossing incoming section"-      (constrainedDelaunay unitElementDefaults basePoints (V.singleton (1, 3)))-  case-      extendConstrainedWith-        Set.union-        base-        (mapVertices (const (Set.singleton "crossing")) (buildTriangulation crossingBuild)) of-    Left (ConstraintUnionConstructionFailed (ConstraintIntersection _)) -> pure ()-    other -> fail ("asymmetric extension did not return the corridor intersection witness: " <> show other)--testLargeAsymmetricConstrainedExtension :: IO ()-testLargeAsymmetricConstrainedExtension = do-  let width = 40 :: Int-      height = 28 :: Int-      basePoints :: V.Vector (Point)-      basePoints =-        V.fromList-          ( [ Point 0 0-            , Point (fromIntegral (width + 1)) 0-            , Point (fromIntegral (width + 1)) (fromIntegral (height + 1))-            , Point 0 (fromIntegral (height + 1))-            ]-              <> [ Point-                     (fromIntegral (column + 1) + fromIntegral ((column * 17 + row * 31) `mod` 13) * 1.0e-3)-                     (fromIntegral (row + 1) + fromIntegral ((column * 23 + row * 19) `mod` 17) * 1.0e-3)-                 | row <- [0 .. height - 1]-                 , column <- [0 .. width - 1]-                 ]-          )-      baseConstraints = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]-      extensionPoints :: V.Vector (Point)-      extensionPoints = V.fromList [Point 48 8, Point 53.2 8.4, Point 50.4 15.8]-      extensionConstraints = V.fromList [(0, 1), (1, 2), (2, 0)]-      conflictPoints :: V.Vector (Point)-      conflictPoints = V.fromList [Point 8 22, Point 30 5, Point 26 7]-  baseBuild <--    requireRight-      "large asymmetric extension base"-      (constrainedDelaunay unitElementDefaults basePoints baseConstraints)-  extensionBuild <--    requireRight-      "large asymmetric extension incoming"-      (constrainedDelaunay unitElementDefaults extensionPoints extensionConstraints)-  conflictBuild <--    requireRight-      "large asymmetric extension conflicting incoming"-      (constrainedDelaunay unitElementDefaults conflictPoints (V.singleton (0, 1)))-  let base = mapVertices (const (Set.singleton "base")) (buildTriangulation baseBuild)-      extension = mapVertices (const (Set.singleton "extension")) (buildTriangulation extensionBuild)-      conflicting = mapVertices (const (Set.singleton "conflict")) (buildTriangulation conflictBuild)-      predecessor-        :: Triangulation 'Constrained (Set.Set String) () () ()-        -> ( Triangulation 'Constrained (Set.Set String) () () ()-           , Map.Map VertexId (Point, Set.Set String)-           , Set.Set (CanonicalSegment)-           )-      predecessor triangulation =-        ( triangulation-        , Map.fromList-            [ (vertex, (vertexPoint triangulation vertex, vertexData triangulation vertex))-            | vertex <- vertices triangulation-            ]-        , Set.fromList (V.toList (constraintSegments triangulation))-        )-      assertPredecessor-        :: String-        -> ( Triangulation 'Constrained (Set.Set String) () () ()-           , Map.Map VertexId (Point, Set.Set String)-           , Set.Set (CanonicalSegment)-           )-        -> Triangulation 'Constrained (Set.Set String) () () ()-        -> IO ()-      assertPredecessor label snapshot triangulation =-        assertEqual label snapshot (predecessor triangulation)-  baseBefore <- evaluate (force (predecessor base))-  extensionBefore <- evaluate (force (predecessor extension))-  conflictingBefore <- evaluate (force (predecessor conflicting))-  let (_, baseVertexSnapshotBefore, baseConstraintSnapshotBefore) = baseBefore-      (_, _, extensionConstraintSnapshotBefore) = extensionBefore-  extensionResult <--    requireRight-      "large asymmetric constrained extension"-      (extendConstrainedWith Set.union base extension)-  let constraintBatch = constrainedExtensionConstraintBatch extensionResult-      extended = constraintBatchTriangulation constraintBatch-      extendedConstraints = Set.fromList (V.toList (constraintSegments extended))-      extendedVertexSnapshot =-        Map.fromList-          [ (vertex, (vertexPoint extended vertex, vertexData extended vertex))-          | vertex <- vertices extended-          ]-  assertEqual-    "large asymmetric extension retains every base vertex handle, coordinate, and payload"-    baseVertexSnapshotBefore-    (Map.restrictKeys extendedVertexSnapshot (Map.keysSet baseVertexSnapshotBefore))-  assertEqual-    "large asymmetric extension retains its complete base constraint source section"-    baseConstraintSnapshotBefore-    (Set.intersection baseConstraintSnapshotBefore extendedConstraints)-  assertEqual-    "large asymmetric extension retains its complete incoming constraint source section"-    extensionConstraintSnapshotBefore-    (Set.intersection extensionConstraintSnapshotBefore extendedConstraints)-  assertEqual-    "large asymmetric extension preserves exactly both source constraint sections"-    (Set.union baseConstraintSnapshotBefore extensionConstraintSnapshotBefore)-    extendedConstraints-  assertEqual-    "large asymmetric extension leaves the frozen base predecessor physically unchanged"-    baseBefore-    (predecessor base)-  assertPredecessor-    "large asymmetric extension leaves the frozen incoming predecessor physically unchanged"-    extensionBefore-    extension-  assertEqual-    "large asymmetric extension charges only the incoming sites"-    (V.length extensionPoints)-    (statInputPoints (constrainedExtensionBuildStats extensionResult))-  assertEqual-    "large asymmetric extension replays only the incoming constraints"-    (V.length extensionConstraints)-    (constraintBatchRequests (constraintBatchStats constraintBatch))-  assertCdtValid "large asymmetric constrained extension" extended-  case extendConstrainedWith Set.union base conflicting of-    Left (ConstraintUnionConstructionFailed (ConstraintIntersection _)) -> pure ()-    outcome ->-      fail-        ( "large asymmetric extension did not refuse the crossing incoming corridor: "-            <> show outcome-        )-  assertEqual-    "large asymmetric extension refusal leaves the frozen base predecessor physically unchanged"-    baseBefore-    (predecessor base)-  assertPredecessor-    "large asymmetric extension refusal leaves the conflicting predecessor physically unchanged"-    conflictingBefore-    conflicting---- | A separated constrained seam is a restriction-preserving operation, not--- the canonical site-set union wearing a cheaper costume. Both closed source--- sections survive face-for-face; only the corridor contributes new faces.-testSeparatedConstrainedSeam :: IO ()-testSeparatedConstrainedSeam = do-  let leftPoints :: V.Vector (Point)-      leftPoints =-        V.fromList-          [ Point (-4) (-1)-          , Point (-2) (-1)-          , Point (-2) 1-          , Point (-4) 1-          ]-      rightPoints :: V.Vector (Point)-      rightPoints =-        V.fromList-          [ Point 2 (-1)-          , Point 4 (-1)-          , Point 4 1-          , Point 2 1-          ]-      closedContour = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]-  leftBuild <--    requireRight-      "separated constrained seam left"-      (constrainedDelaunay unitElementDefaults leftPoints closedContour)-  rightBuild <--    requireRight-      "separated constrained seam right"-      (constrainedDelaunay unitElementDefaults rightPoints closedContour)-  let left =-        mapVertices-          (const (Set.singleton "left"))-          (buildTriangulation leftBuild)-      right =-        mapVertices-          (const (Set.singleton "right"))-          (buildTriangulation rightBuild)-      leftFaceKeys = Set.fromList (fmap (faceKeyOf left) (innerFaces left))-      rightFaceKeys = Set.fromList (fmap (faceKeyOf right) (innerFaces right))-      expectedConstraints =-        Set.union-          (Set.fromList (V.toList (constraintSegments left)))-          (Set.fromList (V.toList (constraintSegments right)))-  seam <--    requireRight-      "source-preserving separated constrained seam"-      (joinSeparatedConstrained left right)-  let joined = constrainedSeamResultTriangulation seam-      joinedFaceKeys = Set.fromList (fmap (faceKeyOf joined) (innerFaces joined))-      actualAnnotations =-        Map.fromList-          [ (vertexPoint joined vertex, vertexData joined vertex)-          | vertex <- vertices joined-          ]-      expectedAnnotations =-        Map.union-          ( Map.fromList-              [ (vertexPoint left vertex, vertexData left vertex)-              | vertex <- vertices left-              ]-          )-          ( Map.fromList-              [ (vertexPoint right vertex, vertexData right vertex)-              | vertex <- vertices right-              ]-          )-  assertCdtValid "source-preserving separated constrained seam" joined-  assertEqual-    "separated seam preserves all source annotations"-    expectedAnnotations-    actualAnnotations-  assertEqual-    "separated seam preserves the exact constraint section"-    expectedConstraints-    (Set.fromList (V.toList (constraintSegments joined)))-  unless (leftFaceKeys `Set.isSubsetOf` joinedFaceKeys) $-    fail "separated seam removed or retriangulated a left source face"-  unless (rightFaceKeys `Set.isSubsetOf` joinedFaceKeys) $-    fail "separated seam removed or retriangulated a right source face"-  assertEqual-    "separated seam has one left face witness per source face"-    (numInnerFaces left)-    (V.length (constrainedSeamLeftFaceEvidence seam))-  assertEqual-    "separated seam has one right face witness per source face"-    (numInnerFaces right)-    (V.length (constrainedSeamRightFaceEvidence seam))-  forM_ (V.toList (constrainedSeamLeftFaceEvidence seam)) $ \evidence ->-    assertEqual-      "left face witness names the exact target triangle"-      (faceKeyOf left (constrainedSeamSourceFace evidence))-      (faceKeyOf joined (constrainedSeamTargetFace evidence))-  forM_ (V.toList (constrainedSeamRightFaceEvidence seam)) $ \evidence ->-    assertEqual-      "right face witness names the exact target triangle"-      (faceKeyOf right (constrainedSeamSourceFace evidence))-      (faceKeyOf joined (constrainedSeamTargetFace evidence))-  when (V.null (constrainedSeamNewFaces seam)) $-    fail "separated seam did not identify any new corridor face"-  assertEqual-    "copied source constraints require no corridor recovery"-    0-    (constraintBatchRequests (constrainedSeamConstraintStats seam))-  unless-    ( all-        ((== Nothing) . constrainedSeamConstraintRecovery)-        ( V.toList (constrainedSeamLeftConstraintEvidence seam)-            <> V.toList (constrainedSeamRightConstraintEvidence seam)-        )-    ) $-    fail "separated seam recovered a constraint already represented by its copied source"--  reversed <--    requireRight-      "source-preserving reversed separated constrained seam"-      (joinSeparatedConstrained right left)-  assertEqual-    "reversing separated seam operands preserves the published constrained value"-    joined-    (constrainedSeamResultTriangulation reversed)--  case joinSeparatedConstrained left left of-    Left ConstraintUnionNotSeparated -> pure ()-    other -> fail ("non-separated constrained seam was not refused: " <> show other)--testConstrainedRefinement :: IO ()-testConstrainedRefinement = do-  cdtBuild <- requireRight "bounded domain" $ constrainedDelaunay-    unitElementDefaults-    (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8, Point 4 2, Point 4 6])-    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])-  let cdt = buildTriangulation cdtBuild-      parameters = defaultRefinementParameters-        { refineMaxAdditionalVertices = Just 80-        , refineMaxArea = Just 3-        , refineMaxRadiusEdgeRatio = Just 1.4-        , refineExcludeOuterFaces = True-        , refineKeepConstraintEdges = False-        }-  refined <- requireRight "constrained refinement" (refine id parameters cdt)-  let result = refinedTriangulation refined-  canonicalResult <- requireRight "canonical constrained refinement" (canonicalize result)-  unless (refinementAddedVertices refined > 0) $ fail "constrained refinement inserted no Steiner points"-  assertCdtValid "constrained refinement" result-  unless (numConstraints result >= numConstraints cdt) $-    fail "constraint splitting lost the constrained boundary"-  assertEqual-    "canonical publication preserves constraint segments"-    (constraintSegments result)-    (constraintSegments canonicalResult)--  -- Refinement maintains the outer-region classification incrementally, from-  -- the touched patch alone. That is a claim about what an insertion cannot-  -- reach, so it is gated against an independent flood over the finished mesh-  -- rather than trusted. The domain here is deliberately narrower than its-  -- convex hull, so the excluded set is non-empty and the two can disagree.-  notchBuild <- requireRight "notched domain" $ constrainedDelaunay-    unitElementDefaults-    (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8, Point 13 4, Point 4 4])-    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])-  let notch = buildTriangulation notchBuild-      notchParameters = defaultRefinementParameters-        { refineMaxAdditionalVertices = Just 120-        , refineMaxArea = Just 1.5-        , refineExcludeOuterFaces = True-        , refineKeepConstraintEdges = False-        }-  notchRefined <- requireRight "notched refinement" (refine id notchParameters notch)-  let notchResult = refinedTriangulation notchRefined-      maintained = sort (V.toList (refinementExcludedFaces notchRefined))-      independent = sort (outerRegionFaces notchResult)-  unless (refinementAddedVertices notchRefined > 0) $ fail "notched refinement inserted no Steiner points"-  when (null independent) $ fail "notched domain produced no outer region: the gate is vacuous"-  assertEqual "incremental exclusion agrees with an independent flood" independent maintained-  assertCdtValid "notched refinement" notchResult--  annulusBuild <- requireRight "annular domain" $ constrainedDelaunay-    unitElementDefaults-    ( V.fromList-        [ Point 0 0-        , Point 12 0-        , Point 12 12-        , Point 0 12-        , Point 4 4-        , Point 8 4-        , Point 8 8-        , Point 4 8-        ]-    )-    ( V.fromList-        [ (0, 1)-        , (1, 2)-        , (2, 3)-        , (3, 0)-        , (4, 5)-        , (5, 6)-        , (6, 7)-        , (7, 4)-        ]-    )-  let annulus = buildTriangulation annulusBuild-      annulusParameters :: Int -> Maybe Double -> RefinementParameters-      annulusParameters budget maximumArea =-        defaultRefinementParameters-          { refineMaxAdditionalVertices = Just budget-          , refineMaxArea = maximumArea-          , refineExcludeOuterFaces = True-          , refineKeepConstraintEdges = False-          }-  annulusUnchanged <--    requireRight-      "budget-zero annular refinement"-      (refine id (annulusParameters 0 Nothing) annulus)-  let initialAnnulusOutside = sort (outerRegionFaces annulus)-      budgetZeroOutside =-        sort (V.toList (refinementExcludedFaces annulusUnchanged))-  assertEqual "annulus has one two-crossing hole" 2 (length initialAnnulusOutside)-  assertEqual-    "budget-zero refinement uses the authoritative annulus classification"-    initialAnnulusOutside-    budgetZeroOutside--  annulusRefined <--    requireRight-      "positive-budget annular refinement"-      (refine id (annulusParameters 40 (Just 4)) annulus)-  let refinedAnnulus = refinedTriangulation annulusRefined-      maintainedAnnulus =-        sort (V.toList (refinementExcludedFaces annulusRefined))-      independentAnnulus = sort (outerRegionFaces refinedAnnulus)-  unless (refinementAddedVertices annulusRefined > 0) $-    fail "annular refinement inserted no Steiner points"-  assertEqual-    "incremental annulus exclusion agrees with authoritative barrier depth"-    independentAnnulus-    maintainedAnnulus-  assertCdtValid "annular constrained refinement" refinedAnnulus--  -- A tiny constraint can share a mesh with coordinates of vastly different-  -- magnitude. Encroachment is discovered by a local cavity walk, so there is-  -- no broad phase for the range to overflow; the pin is that the scaled-  -- circumcenter and diametral predicates still produce exactly one vertex.-  wideGridBuild <- requireRight "wide-grid constrained domain" $ constrainedDelaunay-    unitElementDefaults-    ( V.fromList-        [ Point 0 0-        , Point 2.0e-43 0-        , Point 1.0e60 0-        , Point 0 1.0e60-        ] :: V.Vector (Point)-    )-    (V.singleton (0, 1))-  wideGridRefined <--    requireRight-      "wide-grid constrained refinement"-      ( refine-          id-          defaultRefinementParameters-            { refineMaxAdditionalVertices = Just 1-            , refineMaxArea = Just 1.0e119-            , refineKeepConstraintEdges = False-            }-          (buildTriangulation wideGridBuild)-      )-  assertEqual "wide-grid refinement count" 1 (refinementAddedVertices wideGridRefined)-  assertCdtValid "wide-grid constrained refinement" (refinedTriangulation wideGridRefined)---- | A local domain is a closed section, not a hopeful initial queue. Its--- interface is exact, its protected faces survive point-for-point, and the--- receipt contains no visit to the protected side.-testCheckedLocalRefinement :: IO ()-testCheckedLocalRefinement = do-  built <--    requireRight-      "checked local refinement source"-      ( constrainedDelaunay-          unitElementDefaults-          ( V.fromList-              [ Point 0 0-              , Point 4.1 0-              , Point 8 0.2-              , Point 0.1 4-              , Point 4 4.2-              , Point 8.1 4-              ]-          )-          (V.singleton (2, 5))-      )-  let source = buildTriangulation built-      permitted =-        Set.fromList-          [ face-          | face <- innerFaces source-          , faceCentroidX source face < 4.05-          ]-      interface =-        Set.fromList-          [ edge-          | edge <- undirectedEdges source-          , let (forward, backward) = directedPair edge-                forwardFace = incidentFace source forward-                backwardFace = incidentFace source backward-          , forwardFace /= outerFace-          , backwardFace /= outerFace-          , Set.member forwardFace permitted /= Set.member backwardFace permitted-          ]-      protected = filter (`Set.notMember` permitted) (innerFaces source)-      protectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint source) (faceVertices source face)))) protected-      calmParameters =-        defaultRefinementParameters-          { refineMaxAdditionalVertices = Just 3-          , refineMaxRadiusEdgeRatio = Nothing-          , refineKeepConstraintEdges = True-          }-      crossingParameters = calmParameters{refineMaxArea = Just 1}-  unless (not (Set.null permitted) && not (null protected) && not (Set.null interface)) $-    fail "checked local refinement fixture did not form a nontrivial cover"-  case Set.minView interface of-    Nothing -> fail "checked local refinement fixture has no interface"-    Just (_, incomplete) ->-      case refineWithinDomain id calmParameters permitted incomplete source of-        Left (RefinementDomainInterfaceMissing _) -> pure ()-        Left obstruction -> fail ("checked local refinement returned the wrong incomplete-interface obstruction: " <> show obstruction)-        Right _ -> fail "checked local refinement accepted an incomplete interface"-  refined <--    requireRight-      "checked local refinement"-      (refineWithinDomain id calmParameters permitted interface source)-  let localResult = refinementDomainResult refined-      target = refinedTriangulation localResult-      targetProtectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint target) (faceVertices target face)))) protected-      receipt = refinementDomainReceipt refined-  assertEqual "checked local protected face restriction" protectedSignatures targetProtectedSignatures-  assertEqual "checked local protected visit receipt" V.empty (refinementVisitedProtectedFaces receipt)-  assertEqual "checked local boundary crossing receipt" 0 (refinementAttemptedBoundaryCrossings receipt)-  assertEqual-    "checked local protected constraint restriction"-    (constraintSegments source)-    (constraintSegments target)-  assertValid "checked local refinement" target-  case refineWithinDomain id crossingParameters permitted interface source of-    Left (RefinementDomainWouldCrossInterface _ _) -> pure ()-    Left obstruction -> fail ("checked local refinement returned the wrong crossing obstruction: " <> show obstruction)-    Right _ -> fail "checked local refinement silently crossed its immutable interface"-  case refineWithinDomain id calmParameters{refinePreserveConvexHull = False} permitted interface source of-    Left RefinementDomainRequiresConvexHullPreservation -> pure ()-    outcome -> fail ("checked local refinement accepted hull mutation: " <> either show (const "success") outcome)-  case refineWithinDomain id calmParameters{refineKeepConstraintEdges = False} permitted interface source of-    Left RefinementDomainRequiresConstraintPreservation -> pure ()-    outcome -> fail ("checked local refinement accepted constraint mutation: " <> either show (const "success") outcome)-  case refineWithinDomain id calmParameters{refineExcludeOuterFaces = True} permitted interface source of-    Left RefinementDomainForbidsOuterFaceExclusion -> pure ()-    outcome -> fail ("checked local refinement accepted outer-face exclusion: " <> either show (const "success") outcome)-  wholeRefined <--    requireRight-      "checked whole-section refinement"-      ( refineWithinDomain-          id-          crossingParameters-          (Set.fromList (innerFaces source))-          Set.empty-          source-      )-  let wholeResult = refinementDomainResult wholeRefined-      wholeReceipt = refinementDomainReceipt wholeRefined-  unless (refinementAddedVertices wholeResult > 0) $-    fail "checked whole-section refinement did not improve its admitted section"-  unless (not (V.null (refinementCreatedFaces wholeReceipt))) $-    fail "checked whole-section refinement omitted semantically rewritten face slots"-  unless-    ( V.any-        (\(FaceId raw) -> toInteger raw < toInteger (numFaces source))-        (refinementCreatedFaces wholeReceipt)-    ) $-    fail "checked whole-section refinement omitted recycled face slots"-  assertValid "checked whole-section refinement" (refinedTriangulation wholeResult)- where-  faceCentroidX-    :: Triangulation mode vertex directed undirected face-    -> FaceId-    -> Double-  faceCentroidX triangulation face =-    case fmap (vertexPoint triangulation) (faceVertices triangulation face) of-      [] -> 0-      points ->-        sum [x | Point x _ <- points] / fromIntegral (length points)--testRepeatedBoundaryAdjacentRefinement :: IO ()-testRepeatedBoundaryAdjacentRefinement = (do-  built <--    requireRight-      "repeated boundary-adjacent constrained source"-      ( constrainedDelaunay-          unitElementDefaults-          ( V.fromList-              [ Point 0 0-              , Point 4 0-              , Point 8 0-              , Point 12 0-              , Point 0 4-              , Point 4 4-              , Point 8 4-              , Point 12 4-              ]-          )-          (V.singleton (2, 6))-      )-  let source = buildTriangulation built-      (sourcePermitted, sourceInterface) = leftSection source-      firstParameters =-        defaultRefinementParameters-          { refineMaxAdditionalVertices = Just 20-          , refineMaxArea = Just 1-          , refineMaxRadiusEdgeRatio = Nothing-          , refineKeepConstraintEdges = True-          }-  firstRefinement <--    requireRight-      "first boundary-adjacent refinement"-      ( refineWithinDomain-          id-          firstParameters-          sourcePermitted-          sourceInterface-          source-      )-  let firstResult = refinementDomainResult firstRefinement-      firstTarget = refinedTriangulation firstResult-      firstReceipt = refinementDomainReceipt firstRefinement-      (secondPermitted, secondInterface) = leftSection firstTarget-      secondParameters = firstParameters{refineMaxAdditionalVertices = Just 1}-  unless (refinementAddedVertices firstResult > 0) $-    fail "first boundary-adjacent refinement did not refine its admitted section"-  unless (refinementInterfaceBoundaryReads firstReceipt > 0) $-    fail "boundary-adjacent refinement did not report its immutable interface read"-  assertEqual-    "boundary-adjacent crossing attempts"-    0-    (refinementAttemptedBoundaryCrossings firstReceipt)-  assertEqual-    "first boundary-adjacent constraint restriction"-    (constraintSegments source)-    (constraintSegments firstTarget)-  secondRefinement <--    requireRight-      "repeated boundary-adjacent refinement"-      ( refineWithinDomain-          id-          secondParameters-          secondPermitted-          secondInterface-          firstTarget-      )-  let secondResult = refinementDomainResult secondRefinement-      secondTarget = refinedTriangulation secondResult-      secondReceipt = refinementDomainReceipt secondRefinement-  unless (refinementAddedVertices secondResult > 0) $-    fail "repeated boundary-adjacent refinement did not retain its dynamic join-face support"-  assertEqual-    "repeated boundary-adjacent protected visits"-    V.empty-    (refinementVisitedProtectedFaces secondReceipt)-  assertEqual-    "repeated boundary-adjacent constraint restriction"-    (constraintSegments firstTarget)-    (constraintSegments secondTarget)-  assertValid "repeated boundary-adjacent refinement" secondTarget)- where-  leftSection-    :: Triangulation mode vertex directed undirected face-    -> (Set.Set FaceId, Set.Set UndirectedEdgeId)-  leftSection triangulation =-    let permitted =-          Set.fromList-            [ face-            | face <- innerFaces triangulation-            , faceCentroidX triangulation face < 8-            ]-        interface =-          Set.fromList-            [ edge-            | edge <- undirectedEdges triangulation-            , let (forward, backward) = directedPair edge-                  forwardFace = incidentFace triangulation forward-                  backwardFace = incidentFace triangulation backward-            , forwardFace /= outerFace-            , backwardFace /= outerFace-            , Set.member forwardFace permitted /= Set.member backwardFace permitted-            ]-     in (permitted, interface)-  faceCentroidX-    :: Triangulation mode vertex directed undirected face-    -> FaceId-    -> Double-  faceCentroidX triangulation face =-    case fmap (vertexPoint triangulation) (faceVertices triangulation face) of-      [] -> 0-      points ->-        sum [x | Point x _ <- points] / fromIntegral (length points)--testFaceComponentsAndBoundaries :: IO ()-testFaceComponentsAndBoundaries = do-  emptyBuild <- requirePointBuild "empty region components" []-  collinearBuild <--    requirePointBuild-      "collinear region components"-      [Point 0 0, Point 1 0, Point 2 0]-  assertEqual-    "empty mesh has no face components"-    ([] :: [(Bool, FaceComponent)])-    (faceComponents (buildTriangulation emptyBuild) (const True))-  assertEqual-    "collinear mesh has no face components"-    ([] :: [(Bool, FaceComponent)])-    (faceComponents (buildTriangulation collinearBuild) (const True))--  triangle <--    regionMeshFromPoints-      "single triangle region"-      [Point 0 0, Point 2 0, Point 0 2]-  triangleBoundary <--    requireComponentBoundary "single triangle" True triangle (const True)-  assertBoundaryShape "single triangle" triangle 3 [] triangleBoundary--  square <--    regionMeshFromPoints-      "uniform square region"-      [Point 0 0, Point 2 0, Point 2 2, Point 0 2]-  squareComponent <--    requireLabelledComponent-      "uniform square component"-      True-      (faceComponents square (const True))-  assertEqual-    "component provenance mismatch is typed before DCEL lookup"-    (Left (BoundaryComponentFaceOutOfRange (FaceId 2) 2))-    (componentBoundary triangle squareComponent)-  assertEqual-    "component loop provenance mismatch is typed before DCEL lookup"-    (Left (BoundaryComponentFaceOutOfRange (FaceId 2) 2))-    (componentBoundaryLoops triangle squareComponent)-  squareBoundary <- requireRight "uniform square boundary" (componentBoundary square squareComponent)-  squareLoops <--    requireRight-      "uniform square oriented boundary loops"-      (componentBoundaryLoops square squareComponent)-  repeatedSquareBoundary <--    requireRight "repeated uniform square boundary" (componentBoundary square squareComponent)-  assertEqual "boundary extraction is deterministic" squareBoundary repeatedSquareBoundary-  assertEqual-    "ordinary oriented loops agree with strict boundary"-    (regionBoundaryOuterLoop squareBoundary :| regionBoundaryHoleLoops squareBoundary)-    squareLoops-  assertEqual-    "uniform square drops its Delaunay diagonal"-    (Set.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])-    (loopPointSet square (regionBoundaryOuterLoop squareBoundary))-  assertBoundaryShape "uniform square" square 4 [] squareBoundary--  let splitComponents =-        faceComponents square (regionFaceSatisfies square (\point -> pointX point < 1))-  assertEqual "split square component count" 2 (length splitComponents)-  traverse_-    (\(_, component) -> do-       boundary <- requireRight "split square boundary" (componentBoundary square component)-       assertBoundaryShape "split square component" square 3 [] boundary)-    splitComponents--  collinearHull <--    regionMeshFromPoints-      "collinear hull simplification"-      [ Point 0 0-      , Point 1 0-      , Point 2 0-      , Point 2 2-      , Point 0 2-      , Point 1 1-      ]-  collinearBoundary <--    requireComponentBoundary "collinear hull" True collinearHull (const True)-  assertEqual-    "exact simplification drops a redundant hull site"-    (Set.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])-    (loopPointSet collinearHull (regionBoundaryOuterLoop collinearBoundary))--  lShape <- regionMesh "concave L region" 2 2-  let inL = regionFaceSatisfies lShape (\(Point x y) -> not (x > 1 && y > 1))-  lBoundary <- requireComponentBoundary "concave L" True lShape inL-  assertEqual-    "concave L boundary"-    ( Set.fromList-        [ Point 0 0-        , Point 2 0-        , Point 2 1-        , Point 1 1-        , Point 1 2-        , Point 0 2-        ]-    )-    (loopPointSet lShape (regionBoundaryOuterLoop lBoundary))-  assertBoundaryShape "concave L" lShape 6 [] lBoundary--  annulus <- regionMesh "annulus region" 3 3-  let outsideCenter =-        regionFaceSatisfies annulus (\(Point x y) -> not (x > 1 && x < 2 && y > 1 && y < 2))-  annulusBoundary <--    requireComponentBoundary "annulus outer" True annulus outsideCenter-  assertBoundaryShape "annulus" annulus 4 [4] annulusBoundary--  twoHoles <- regionMesh "ordered hole regions" 5 3-  let outsideTwoCells =-        regionFaceSatisfies twoHoles $ \(Point x y) ->-          let cell = (floor x :: Int, floor y :: Int)-           in cell /= (1, 1) && cell /= (3, 1)-  twoHoleBoundary <--    requireComponentBoundary "two-hole outer" True twoHoles outsideTwoCells-  assertBoundaryShape "two-hole" twoHoles 4 [4, 4] twoHoleBoundary--  disconnected <- regionMesh "disconnected equal labels" 3 1-  let outsideMiddle =-        regionFaceSatisfies disconnected (\(Point x _) -> x < 1 || x > 2)-      disconnectedComponents = faceComponents disconnected outsideMiddle-      equalLabelComponents =-        [component | (True, component) <- disconnectedComponents]-  assertEqual "equal labels remain two disconnected components" 2 (length equalLabelComponents)-  assertEqual-    "face component order is deterministic"-    disconnectedComponents-    (faceComponents disconnected outsideMiddle)--  pinched <- regionMesh "pinched region" 3 3-  let pinchedSelection =-        regionFaceSatisfies pinched $ \(Point x y) ->-          let cell = (floor x :: Int, floor y :: Int)-           in cell /= (0, 0) && cell /= (1, 1)-  pinchedComponent <--    requireLabelledComponent-      "pinched selected component"-      True-      (faceComponents pinched pinchedSelection)-  pinchedLoops <--    requireRight-      "pinched oriented boundary loops"-      (componentBoundaryLoops pinched pinchedComponent)-  let pinchedLoopList = toList pinchedLoops-  assertEqual "pinched simple loop count" 2 (length pinchedLoopList)-  assertEqual-    "pinched simple loop vertex counts"-    [4, 6]-    (sort (fmap (length . boundaryLoopVertices) pinchedLoopList))-  assertEqual-    "pinched loop orientations"-    [BoundaryCounterClockwise, BoundaryClockwise]-    (sort (fmap boundaryLoopOrientation pinchedLoopList))-  traverse_-    (\loop -> do-       let loopVertexIds = toList (boundaryLoopVertices loop)-       assertEqual-         "pinched loop has no repeated vertex"-         (length loopVertexIds)-         (Set.size (Set.fromList loopVertexIds))-       assertLoopWinding-         "pinched loop winding agrees with orientation"-         (case boundaryLoopOrientation loop of-            BoundaryCounterClockwise -> GT-            BoundaryClockwise -> LT)-         pinched-         loop)-    pinchedLoopList-  case componentBoundary pinched pinchedComponent of-    Left (BoundaryPinch vertex firstEdge secondEdge) -> do-      assertEqual "pinch vertex" (Point 1 1) (vertexPoint pinched vertex)-      when (firstEdge == secondEdge) $-        fail ("pinch repeated one outgoing edge: " <> show firstEdge)-    other -> fail ("pinched boundary produced " <> show other)--testAlphaFaceFiltration :: IO ()-testAlphaFaceFiltration = do-  triangle <--    regionMeshFromPoints-      "alpha right triangle"-      [Point 0 0, Point 2 0, Point 0 2]-  face <- case innerFaces triangle of-    [singleFace] -> pure singleFace-    faces -> fail ("alpha fixture faces: " <> show faces)-  exactThreshold <- requireRight "exact alpha threshold" (mkRadiusSquared 2)-  lowerThreshold <- requireRight "lower alpha threshold" (mkRadiusSquared 1.999)-  zeroThreshold <- requireRight "zero alpha threshold" (mkRadiusSquared 0)-  assertEqual "zero alpha threshold excludes face" False-    (alphaShapeContainsFace zeroThreshold triangle face)-  assertEqual "closed alpha threshold includes equality" True (alphaShapeContainsFace exactThreshold triangle face)-  assertEqual "lower alpha threshold excludes face" False (alphaShapeContainsFace lowerThreshold triangle face)-  assertEqual "outer face is absent from alpha filtration" False (alphaShapeContainsFace exactThreshold triangle outerFace)--  let roundedPoints =-        ( Point (-20) (-20)-        , Point (-19) (-13)-        , Point (-2) (-19)-        )-      (roundedFirst, roundedSecond, roundedThird) = roundedPoints-      roundedRadiusSquared = 84.5-  roundedTriangle <--    regionMeshFromPoints-      "rounded alpha equality"-      [roundedFirst, roundedSecond, roundedThird]-  roundedFace <- case innerFaces roundedTriangle of-    [singleFace] -> pure singleFace-    faces -> fail ("rounded alpha fixture faces: " <> show faces)-  roundedThreshold <--    requireRight-      "rounded exact alpha threshold"-      (mkRadiusSquared roundedRadiusSquared)-  roundedLowerThreshold <--    requireRight-      "rounded lower alpha threshold"-      (mkRadiusSquared (roundedRadiusSquared - encodeFloat 1 (-46)))-  assertEqual-    "closed alpha equality is exact"-    True-    (alphaShapeContainsFace roundedThreshold roundedTriangle roundedFace)-  assertEqual-    "one binary64 step below exact alpha equality is excluded"-    False-    (alphaShapeContainsFace roundedLowerThreshold roundedTriangle roundedFace)--  alphaBoundary <--    requireComponentBoundary-      "alpha"-      True-      triangle-      (alphaShapeContainsFace exactThreshold triangle)-  assertBoundaryShape "alpha component" triangle 3 [] alphaBoundary--  annulus <--    regionMeshFromPoints-      "alpha annulus"-      (ringPoints 4 32 <> ringPoints 2 16 <> ringPoints 3 24)-  annulusThreshold <--    requireRight "alpha annulus threshold" (mkRadiusSquared 0.4)-  annulusBoundary <--    requireComponentBoundary-      "alpha annulus"-      True-      annulus-      (alphaShapeContainsFace annulusThreshold annulus)-  assertEqual-    "alpha annulus hole count"-    1-    (length (regionBoundaryHoleLoops annulusBoundary))-  assertLoopWinding-    "alpha annulus outer winding"-    GT-    annulus-    (regionBoundaryOuterLoop annulusBoundary)-  traverse_-    (assertLoopWinding "alpha annulus hole winding" LT annulus)-    (regionBoundaryHoleLoops annulusBoundary)-  traverse_-    (\(label, value, failure) ->-       assertEqual label (Left failure) (mkRadiusSquared value))-    [ ("negative alpha threshold refusal", -1, NegativeRadiusSquared (-1))-    , ("NaN alpha threshold refusal", 0 / 0, NonFiniteRadiusSquared ValueNaN)-    , ("positive-infinite alpha threshold refusal", 1 / 0, NonFiniteRadiusSquared ValuePositiveInfinity)-    , ("negative-infinite alpha threshold refusal", (-1) / 0, NonFiniteRadiusSquared ValueNegativeInfinity)-    ]--ringPoints :: Double -> Int -> [Point]-ringPoints radius count =-  [ Point (radius * cos angle) (radius * sin angle)-  | index <- [0 .. count - 1]-  , let angle = 2 * pi * fromIntegral index / fromIntegral count-  ]--regionMesh :: String -> Int -> Int -> IO NativeMesh-regionMesh label widthInCells heightInCells =-  regionMeshFromPoints-    label-    [ Point (fromIntegral x) (fromIntegral y)-    | y <- [0 .. heightInCells]-    , x <- [0 .. widthInCells]-    ]--regionMeshFromPoints :: String -> [Point] -> IO NativeMesh-regionMeshFromPoints label points =-  buildTriangulation <$> requirePointBuild label points--regionFaceCentroid-  :: Triangulation mode vertex directed undirected face-  -> FaceId-  -> Maybe Point-regionFaceCentroid triangulation face =-  (\(first, second, third) ->-     centroid-       (vertexPoint triangulation first)-       (vertexPoint triangulation second)-       (vertexPoint triangulation third))-    <$> Dcel.innerFaceVertices triangulation face--regionFaceSatisfies-  :: Triangulation mode vertex directed undirected face-  -> (Point -> Bool)-  -> FaceId-  -> Bool-regionFaceSatisfies triangulation predicate =-  maybe False predicate . regionFaceCentroid triangulation--requireLabelledComponent-  :: (Eq label, Show label)-  => String-  -> label-  -> [(label, FaceComponent)]-  -> IO FaceComponent-requireLabelledComponent label expected components =-  case [component | (actual, component) <- components, actual == expected] of-    [component] -> pure component-    matches ->-      fail-        ( label-            <> ": expected one component for label "-            <> show expected-            <> ", got "-            <> show (length matches)-        )--requireComponentBoundary-  :: (Eq label, Show label)-  => String-  -> label-  -> Triangulation mode vertex directed undirected face-  -> (FaceId -> label)-  -> IO RegionBoundary-requireComponentBoundary label expected triangulation labelFace = do-  component <--    requireLabelledComponent-      (label <> " component")-      expected-      (faceComponents triangulation labelFace)-  requireRight (label <> " boundary") (componentBoundary triangulation component)--loopPointSet-  :: Triangulation mode vertex directed undirected face-  -> BoundaryLoop-  -> Set.Set Point-loopPointSet triangulation =-  Set.fromList-    . fmap (vertexPoint triangulation)-    . toList-    . boundaryLoopVertices--assertLoopWinding-  :: String-  -> Ordering-  -> Triangulation mode vertex directed undirected face-  -> BoundaryLoop-  -> IO ()-assertLoopWinding label expected triangulation loop =-  let first :| remaining = fmap (vertexPoint triangulation) (boundaryLoopVertices loop)-      points = first : remaining-      twiceArea =-        sum-          ( zipWith-              (\(Point ax ay) (Point bx by) -> ax * by - ay * bx)-              points-              (remaining <> [first])-          )-   in assertEqual label expected (compare twiceArea 0)--assertBoundaryShape-  :: String-  -> Triangulation mode vertex directed undirected face-  -> Int-  -> [Int]-  -> RegionBoundary-  -> IO ()-assertBoundaryShape label triangulation outerVertexCount holeVertexCounts boundary = do-  assertEqual-    (label <> " outer orientation")-    BoundaryCounterClockwise-    (boundaryLoopOrientation (regionBoundaryOuterLoop boundary))-  assertEqual (label <> " outer vertex count") outerVertexCount-    (length (boundaryLoopVertices (regionBoundaryOuterLoop boundary)))-  assertLoopWinding (label <> " outer winding") GT triangulation-    (regionBoundaryOuterLoop boundary)-  let holes = regionBoundaryHoleLoops boundary-  assertEqual (label <> " hole count") (length holeVertexCounts) (length holes)-  traverse_-    (\(vertexCount, hole) -> do-       assertEqual-         (label <> " hole orientation")-         BoundaryClockwise-         (boundaryLoopOrientation hole)-       assertEqual (label <> " hole vertex count") vertexCount-         (length (boundaryLoopVertices hole))-       assertLoopWinding (label <> " hole winding") LT triangulation hole)-    (zip holeVertexCounts holes)--testTraversal :: IO ()-testTraversal = do-  built <- requirePointBuild "traversal" [Point (-3) 0, Point (-1) (-2), Point (-1) 2, Point 1 (-2), Point 1 2, Point 3 0]-  forwardStart <- requireQueryPoint "forward traversal start" (Point (-4) 0)-  forwardEnd <- requireQueryPoint "forward traversal end" (Point 4 0)-  let triangulation = buildTriangulation built-      forward = lineIntersections triangulation forwardStart forwardEnd-      backward = lineIntersections triangulation forwardEnd forwardStart-  when (null forward) $ fail "ordered line traversal crossed nothing"-  assertEqual-    "reversing the segment reverses the crossings"-    (map crossingIdentity forward)-    (reverse (map crossingIdentity backward))-  circleEdges <- Set.fromList <$> requireRight "circle edge query" (edgesInCircle triangulation (Point 0 0) 4)-  let bruteCircle = Set.fromList-        [ edge-        | edge <- undirectedEdges triangulation-        , let (a, b) = undirectedEndpoints triangulation edge-        , segmentDistanceSquared (vertexPoint triangulation a) (vertexPoint triangulation b) (Point 0 0) <= 4-        ]-  assertEqual "circle edge flood" bruteCircle circleEdges-  assertEqual-    "negative circle metric refusal"-    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))-    (circleMetric (Point 0 0 :: Point) (-1))-  assertEqual-    "negative circle edge-query refusal"-    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))-    (edgesInCircle triangulation (Point 0 0) (-1))-  assertEqual-    "negative circle vertex-query refusal"-    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))-    (verticesInCircle triangulation (Point 0 0) (-1))-  assertEqual-    "NaN circle metric refusal"-    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNaN)))-    (circleMetric (Point 0 0 :: Point) (0 / 0))-  assertEqual-    "NaN circle center-x refusal"-    (Left (InvalidCircleCenter (InvalidPointX CoordinateNaN)))-    (circleMetric (Point (0 / 0) 0 :: Point) 1)-  assertEqual-    "positive-infinite circle center-y refusal"-    (Left (InvalidCircleCenter (InvalidPointY CoordinateInfinite)))-    (edgesInCircle triangulation (Point 0 (1 / 0)) 1)-  assertEqual-    "negative-infinite circle center-x refusal"-    (Left (InvalidCircleCenter (InvalidPointX CoordinateInfinite)))-    (verticesInCircle triangulation (Point ((-1) / 0) 0) 1)-  assertEqual-    "infinite circle edge-query refusal"-    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValuePositiveInfinity)))-    (edgesInCircle triangulation (Point 0 0) (1 / 0))-  assertEqual-    "negative-infinite circle vertex-query refusal"-    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNegativeInfinity)))-    (verticesInCircle triangulation (Point 0 0) ((-1) / 0))-  rectangleVertices <--    Set.fromList-      <$> requireRight-        "rectangle query"-        (verticesInRectangle triangulation (Point (-1.1) (-2.1)) (Point 1.1 2.1))-  let bruteVertices = Set.fromList-        [ vertex-        | vertex <- vertices triangulation-        , let Point x y = vertexPoint triangulation vertex-        , x >= (-1.1), x <= 1.1, y >= (-2.1), y <= 2.1-        ]-  assertEqual "rectangle vertex flood" bruteVertices rectangleVertices--testRandomizedConstruction :: IO ()-testRandomizedConstruction = do-  forM_ [0 .. 31] $ \index -> do-    let points = randomPoints (0x9e37_79b9 + fromIntegral index) (40 + index * 7)-    built <- requirePointBuild ("random build " <> show index) points-    let triangulation = buildTriangulation built-    assertValid ("random build " <> show index) triangulation-    assertEqual "random input mapping" (length points) (sizeofPrimArray (buildInputVertices built))--testErrors :: IO ()-testErrors = do-  assertEqual-    "NaN query x refusal"-    (Left (InvalidPointX CoordinateNaN))-    (mkQueryPoint (Point (0 / 0) 0))-  assertEqual-    "infinite query y refusal"-    (Left (InvalidPointY CoordinateInfinite))-    (mkQueryPoint (Point 0 (1 / 0)))-  assertEqual-    "negative-infinite query x refusal"-    (Left (InvalidPointX CoordinateInfinite))-    (mkQueryPoint (Point ((-1) / 0) 0))-  case delaunay unitElementDefaults (V.singleton (Point (0 / 0) 0 :: Point)) of-    Left (InvalidCoordinate (Just 0) _ CoordinateNaN) -> pure ()-    Left other -> fail ("NaN insertion result: " <> show other)-    Right built ->-      fail-        ( "NaN insertion result: built a triangulation of "-            <> show (numVertices (buildTriangulation built))-            <> " vertices"-        )-  expectBuildFailure-    "NaN minimum angle"-    (RefinementMinimumAngleNotFinite ValueNaN)-    (withMinimumAngle (0 / 0 :: Double) defaultRefinementParameters)-  expectBuildFailure-    "infinite minimum angle"-    (RefinementMinimumAngleNotFinite ValuePositiveInfinity)-    (withMinimumAngle (1 / 0 :: Double) defaultRefinementParameters)-  expectBuildFailure-    "out-of-range minimum angle"-    (RefinementMinimumAngleOutOfRange 61)-    (withMinimumAngle (61 :: Double) defaultRefinementParameters)-  expectBuildFailure-    "minimum angle whose derived ratio overflows"-    (RefinementMinimumAngleDerivedRatioNotFinite ValuePositiveInfinity)-    (withMinimumAngle (encodeFloat 1 (-1074) :: Double) defaultRefinementParameters)-  square <- requirePointBuild "area validation square" [Point 0 0, Point 1 0, Point 1 1, Point 0 1]-  let refineWith parameters =-        refine id parameters (buildTriangulation square)-      refineWithArea area =-        refineWith defaultRefinementParameters{refineMaxArea = Just area}-  expectBuildFailure-    "negative refinement vertex budget"-    (RefinementMaximumAdditionalVerticesNegative (-1))-    (refineWith defaultRefinementParameters{refineMaxAdditionalVertices = Just (-1)})-  expectBuildFailure-    "infinite minimum area"-    (RefinementMinimumAreaNotFinite ValuePositiveInfinity)-    (refineWith defaultRefinementParameters{refineMinArea = Just (1 / 0)})-  expectBuildFailure-    "negative minimum area"-    (RefinementMinimumAreaNegative (-1))-    (refineWith defaultRefinementParameters{refineMinArea = Just (-1)})-  expectBuildFailure-    "NaN maximum area"-    (RefinementMaximumAreaNotFinite ValueNaN)-    (refineWithArea (0 / 0))-  expectBuildFailure-    "infinite maximum area"-    (RefinementMaximumAreaNotFinite ValuePositiveInfinity)-    (refineWithArea (1 / 0))-  expectBuildFailure-    "zero maximum area"-    (RefinementMaximumAreaNotPositive 0)-    (refineWithArea 0)-  expectBuildFailure-    "negative maximum area"-    (RefinementMaximumAreaNotPositive (-1))-    (refineWithArea (-1))-  expectBuildFailure-    "infinite maximum radius/edge ratio"-    (RefinementMaximumRadiusEdgeRatioNotFinite ValuePositiveInfinity)-    (refineWith defaultRefinementParameters{refineMaxRadiusEdgeRatio = Just (1 / 0)})-  expectBuildFailure-    "non-positive maximum radius/edge ratio"-    (RefinementMaximumRadiusEdgeRatioNotPositive 0)-    (refineWith defaultRefinementParameters{refineMaxRadiusEdgeRatio = Just 0})-  expectBuildFailure-    "minimum area above maximum area"-    (RefinementMinimumAreaExceedsMaximum 2 1)-    ( refineWith-        defaultRefinementParameters-          { refineMinArea = Just 2-          , refineMaxArea = Just 1-          }-    )-  case refineWithArea 0.25 of-    Right _ -> pure ()-    Left failure -> fail ("positive maximum area rejected: " <> show failure)--expectBuildFailure :: String -> BuildError -> Either BuildError value -> IO ()-expectBuildFailure label expected outcome =-  case outcome of-    Left actual -> assertEqual label expected actual-    Right _ -> fail (label <> ": expected " <> show expected <> ", got success")---canonicalEdges-  :: Triangulation mode vertex directed undirected face-  -> Set.Set (Point, Point)-canonicalEdges triangulation =-  Set.fromList-    [ ordered (vertexPoint triangulation (origin triangulation edge)) (vertexPoint triangulation (destination triangulation edge))-    | undirected <- undirectedEdges triangulation-    , let edge = normalizedDirected undirected-    ]- where-  ordered :: Ord value => value -> value -> (value, value)-  ordered left right = if left <= right then (left, right) else (right, left)--requireJust :: String -> Maybe value -> IO value-requireJust _ (Just value) = pure value-requireJust label Nothing = fail (label <> ": expected Just")--requireAcceptedConstraint-  :: String-  -> ConstraintBatchResult vertex directed undirected face-  -> IO (V.Vector DirectedEdgeId, Int)-requireAcceptedConstraint label batch =-  case V.toList (constraintBatchOutcomes batch) of-    [ConstraintAccepted path added] -> pure (path, added)-    outcomes ->-      fail-        ( label-            <> ": expected one accepted constraint, got "-            <> show outcomes-        )--assertBatchStats-  :: String-  -> ConstraintBatchResult vertex directed undirected face-  -> IO ()-assertBatchStats label batch = do-  let stats = constraintBatchStats batch-      (accepted, rejected) =-        V.foldl'-          (\(!acceptedCount, !rejectedCount) outcome ->-            case outcome of-              ConstraintAccepted _ _ -> (acceptedCount + 1, rejectedCount)-              ConstraintRejected _ -> (acceptedCount, rejectedCount + 1)-          )-          (0, 0)-          (constraintBatchOutcomes batch)-  assertEqual-    (label <> " request count")-    (V.length (constraintBatchOutcomes batch))-    (constraintBatchRequests stats)-  assertEqual-    (label <> " accepted count")-    accepted-    (constraintBatchAccepted stats)-  assertEqual-    (label <> " rejected count")-    rejected-    (constraintBatchRejected stats)--replayConstraintRequest-  :: (ConstrainedDelaunayTriangulation (Point), [ConstraintOutcome])-  -> (VertexId, VertexId)-  -> Either-      (CdtError)-      (ConstrainedDelaunayTriangulation (Point), [ConstraintOutcome])-replayConstraintRequest (current, outcomes) request = do-  singleton <- recoverConstraints current (V.singleton request)-  case V.toList (constraintBatchOutcomes singleton) of-    [outcome] ->-      Right-        ( constraintBatchTriangulation singleton-        , outcome : outcomes-        )-    cardinality ->-      Left-        ( ConstraintBatchCardinalityMismatch-            1-            (length cardinality)-        )--requirePointBuild :: String -> [Point] -> IO (BuildResult 'Unconstrained (Point) () () ())-requirePointBuild label points = requireRight label (delaunay unitElementDefaults (V.fromList points))--assertCdtValid :: String -> Triangulation 'Constrained vertex () () () -> IO ()-assertCdtValid label triangulation =-  case validateTriangulation triangulation of-    [] -> pure ()-    violations -> fail (label <> " CDT violations: " <> show violations)+import qualified Data.IntSet as IntSet+import qualified Data.List as List+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (isJust)+import qualified Data.Map.Strict as Map+import qualified Data.Set as Set+import Data.List (sort)+import qualified Data.Vector as V+import Data.Primitive.PrimArray (indexPrimArray, primArrayFromList, sizeofPrimArray)+import qualified Data.Vector.Unboxed as U+import qualified Data.Vector.Unboxed.Mutable as MUV+import Data.Word (Word32, Word64)+import Moonlight.Triangulation.Alpha+import Moonlight.Triangulation.BulkLoad+import Moonlight.Triangulation.Cdt+import Moonlight.Triangulation.Dcel+import qualified Moonlight.Triangulation.Dcel as Dcel+import Moonlight.Triangulation.FloodFillIterator+import Moonlight.Triangulation.Handles.HandleDefs+import Moonlight.Triangulation.Handles.Iterators.FixedIterators+import Moonlight.Triangulation.HintGenerator+import Moonlight.Triangulation.Interpolation+import Moonlight.Triangulation.IntersectionIterator+import Moonlight.Triangulation.Math+import Moonlight.Triangulation.Payload+import Moonlight.Triangulation.FilteredPredicateOptimizationSpec+  ( assertFilteredPredicatesSkipExactOracle+  )+import Moonlight.Triangulation.PointLocation+import Moonlight.Triangulation.Refinement+import Moonlight.Triangulation.Removal+import Moonlight.Triangulation.Scalar+import Moonlight.Triangulation.Session+import Moonlight.Triangulation.Types+import Moonlight.Triangulation.Validation+import Moonlight.Triangulation.Voronoi+import Moonlight.Triangulation.Handles.Dynamic qualified as Dynamic+import Moonlight.Triangulation.Handles.Iterators.DynamicIterators qualified as DynamicIterators+import Moonlight.Triangulation.Internal.BoxedPaged (boxedMaterializedPageCount)+import Moonlight.Triangulation.Internal.Canonical (canonicalize)+import Moonlight.Triangulation.Internal.Paged+  ( Paged+  , PublicationStats (..)+  , fromVector+  , toVector+  )+import Moonlight.Triangulation.Internal.PointIndex+  ( MutablePointIndexUpdate (..)+  , lookupMutablePoint+  , newMutablePointIndex+  , relocateMutablePoint+  , removeMutablePoint+  , seedMutablePointIndex+  )+import Moonlight.Triangulation.Internal.Representation qualified as Internal+import Moonlight.Triangulation.Voronoi.Handles qualified as VoronoiDynamic+import GHC.Generics (Generic)+import Support (assertEqual, assertValid, requireQueryPoint, requireRight)++tests :: IO ()+tests = do+  testConstructors+  testPredicates+  testParaboloidLift+  assertFilteredPredicatesSkipExactOracle+  testValidationRejectsCorruptedMeshes+  testRefinementCompletionIsAFixpoint+  testScalarFormat+  testWideDistanceNearestNeighbor+  testIncrementalLocationDescent+  testPersistentInsertionReusesFrozenLocation+  testCircleSweepBulkLoad+  testMixedEditSession+  testBulkRemovalAgreement+  testMutablePointIndexWraparoundBackshift+  testBatchIdentityIndexBatchToEmpty+  testBatchIdentityIndexToSingletonActive+  testBulkIdentityIndexDoesNotEscapeRemovalBatch+  testHierarchyRemovalAgreement+  testHandlesAndFiniteDcel+  testDegenerateConstruction+  testPersistentLocalUpdates+  testGenericPayloads+  testPayloadMaps+  testGeometryOnlyPublication+  testPayloadTraversals+  testRewritePayloadIdentity+  testPointLocationAndHints+  testHierarchyNestingLaw+  testSibsonInterpolation+  testVoronoiDual+  testRemoval+  testConstrainedDelaunay+  testAnnotatedConstrainedUnion+  testAsymmetricConstrainedExtension+  testLargeAsymmetricConstrainedExtension+  testSeparatedConstrainedSeam+  testAxisSeparatedConstrainedSeams+  testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite+  testLocalRefinementRejectsEncroachedHullSplit+  testConstrainedRefinement+  testCheckedLocalRefinement+  testMaximumEdgeLengthQuality+  testLocalRefinementRejectsTrueInterfaceCrossing+  testRepeatedBoundaryAdjacentRefinement+  testFaceComponentsAndBoundaries+  testAlphaFaceFiltration+  testTraversal+  testRandomizedConstruction+  testErrors+  putStrLn "all native core tests passed"++data SampleVertex = SampleVertex+  { samplePosition :: !(Point)+  , sampleLabel :: !Int+  }+  deriving stock (Eq, Show, Generic)+  deriving anyclass (NFData)++instance HasPosition SampleVertex where+  position = samplePosition+++testConstructors :: IO ()+testConstructors = do+  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)+  originQuery <- requireQueryPoint "empty location query" (Point 0 0)+  assertValid "empty" vacant+  assertEqual "empty vertex count" 0 (numVertices vacant)+  assertEqual "empty directed edge count" 0 (numDirectedEdges vacant)+  assertEqual "empty face count" 1 (numFaces vacant)+  assertEqual "empty locates nowhere" EmptyTriangulation (locatePoint vacant originQuery)+  built <- requirePointBuild "clear source" [Point 0 0, Point 1 0, Point 0 1]+  assertEqual "clear returns the origin" vacant (clear (buildTriangulation built))++type NativeMesh = Triangulation 'Unconstrained (Point) () () ()++-- | A named corruption of a mesh that was valid one line earlier, paired with+-- the violation it must provoke. Validation that has never been made to fail is+-- evidence only that it ran.+data Corruption = Corruption+  { corruptionName :: String+  , corruptMesh :: NativeMesh -> NativeMesh+  , provokes :: InvariantViolation -> Bool+  }++-- Sheared so that no four sites are cocircular: on a square grid the diagonal+-- of every cell is a free choice, and a fixture that admits two answers cannot+-- witness a wrong one.+corruptionFixture :: [Point]+corruptionFixture =+  [ Point (fromIntegral column + 0.25 * fromIntegral row) (1.3 * fromIntegral row)+  | column <- [0 .. 3 :: Int]+  , row <- [0 .. 3 :: Int]+  ]++rewritePlane :: (U.Unbox a, Num a) => (U.Vector a -> U.Vector a) -> Paged a -> Paged a+rewritePlane edit = fromVector 0 . edit . toVector++slot :: U.Unbox a => Int -> a -> U.Vector a -> U.Vector a+slot at value = (U.// [(at, value)])++-- Past the end of every plane in the fixture, and far from the sentinels the+-- packed representation reserves for absence.+beyond :: NativeMesh -> Word32+beyond mesh = fromIntegral (numDirectedEdges mesh + numVertices mesh + 64)++onTopology :: (NativeMesh -> U.Vector Word32 -> U.Vector Word32) -> NativeMesh -> NativeMesh+onTopology edit mesh =+  mesh {Internal.triHalfTopology = rewritePlane (edit mesh) (Internal.triHalfTopology mesh)}++-- The half-edge plane has stride four — origin, next, previous, face — so the+-- first four entries below are one surgery distinguished only by which field+-- the out-of-range index lands on.+structuralCorruptions :: [Corruption]+structuralCorruptions =+  [ Corruption+      "edge origin names an absent vertex"+      (onTopology (slot 0 . beyond))+      (\case EdgeOriginOutOfRange {} -> True; _ -> False)+  , Corruption+      "edge next names an absent edge"+      (onTopology (slot 1 . beyond))+      (\case EdgeNextOutOfRange {} -> True; _ -> False)+  , Corruption+      "edge previous names an absent edge"+      (onTopology (slot 2 . beyond))+      (\case EdgePreviousOutOfRange {} -> True; _ -> False)+  , Corruption+      "edge face names an absent face"+      (onTopology (slot 3 . beyond))+      (\case EdgeFaceOutOfRange {} -> True; _ -> False)+  , Corruption+      "vertex outgoing names an absent edge"+      ( \mesh ->+          mesh+            { Internal.triVertexOut =+                rewritePlane (slot 0 (beyond mesh)) (Internal.triVertexOut mesh)+            }+      )+      (\case VertexOutgoingOutOfRange {} -> True; _ -> False)+  , Corruption+      "a coordinate plane is one short"+      (\mesh -> mesh {Internal.triPointX = rewritePlane U.init (Internal.triPointX mesh)})+      (\case CoordinatePlaneLengthMismatch {} -> True; _ -> False)+  , Corruption+      "every inner face wound clockwise"+      (\mesh -> mesh {Internal.triPointY = rewritePlane (U.map negate) (Internal.triPointY mesh)})+      (\case InnerFaceNotCounterClockwise {} -> True; _ -> False)+  ]++-- Structural corruption is caught before geometry is read, so the empty-circle+-- law needs a mesh that stays well formed and merely stops being Delaunay.+delaunayCorruptions :: [Corruption]+delaunayCorruptions =+  [ Corruption+      "one site dragged through its neighbours' circumcircles"+      (\mesh -> mesh {Internal.triPointX = rewritePlane (slot 5 40) (Internal.triPointX mesh)})+      (\case LocallyIllegalDelaunayEdge {} -> True; _ -> False)+  ]++-- A surgery that changed nothing would report the oracle as unarmed when in+-- truth it was never asked anything, so the mutant must differ before its+-- rejection means a thing.+assertRejects+  :: String -> (NativeMesh -> [InvariantViolation]) -> NativeMesh -> Corruption -> IO ()+assertRejects oracle check pristine corruption = do+  let mutant = corruptMesh corruption pristine+      label = oracle <> " / " <> corruptionName corruption+      reported = check mutant+  when (mutant == pristine) $ fail (label <> ": the surgery changed nothing")+  unless (any (provokes corruption) reported) $+    fail (label <> ": admitted, reporting " <> show reported)++testValidationRejectsCorruptedMeshes :: IO ()+testValidationRejectsCorruptedMeshes = do+  built <- requirePointBuild "corruption fixture" corruptionFixture+  let pristine = buildTriangulation built+  assertValid "corruption fixture" pristine+  mapM_ (assertRejects "topology" validateTopology pristine) structuralCorruptions+  mapM_ (assertRejects "delaunay" validateDelaunay pristine) delaunayCorruptions+  case innerFaces pristine of+    [] -> fail "local validation fixture has no inner face"+    witnessFace : _ ->+      case faceDirectedEdges pristine witnessFace of+        [] -> fail "local validation fixture has no face edge"+        DirectedEdgeId rawEdge : _ -> do+          let admitted = IntSet.singleton (fromIntegral (unFaceId witnessFace))+              corrupted =+                pristine+                  { Internal.triHalfTopology =+                      rewritePlane+                        (slot (4 * fromIntegral rawEdge + 1) rawEdge)+                        (Internal.triHalfTopology pristine)+                  }+          assertEqual+            "selected validation accepts valid closure"+            []+            (validateTopologyClosure admitted IntSet.empty pristine)+          unless (not (null (validateTopology corrupted))) $+            fail "global validation missed the locally corrupt face closure"+          unless (not (null (validateTopologyClosure admitted IntSet.empty corrupted))) $+            fail "selected validation missed a locally corrupt face closure"++-- Twin half-edges are arithmetic complements, so a crossing's undirected+-- identity is its handle halved. Comparing cardinality alone would admit a+-- traversal that returned the right number of the wrong crossings.+crossingIdentity :: Intersection -> Either Int Int+crossingIdentity = \case+  EdgeIntersection edge -> Right (fromIntegral (unDirectedEdgeId edge) `quot` 2)+  EdgeOverlap edge -> Right (fromIntegral (unDirectedEdgeId edge) `quot` 2)+  VertexIntersection vertex -> Left (fromIntegral (unVertexId vertex))++-- | What @refinementComplete@ claims is that the quality worklist drained. The+-- assertable content of that claim is a fixpoint: a second pass under the same+-- parameters can admit nothing. The complementary run is budget-starved, where+-- the run must stop short and spend exactly what it was given.+testRefinementCompletionIsAFixpoint :: IO ()+testRefinementCompletionIsAFixpoint = do+  -- Barrier parity needs constraints to bound a domain: on an unconstrained+  -- mesh every face sits at depth zero, so excluding outer faces excludes all+  -- of them and the worklist drains having refined nothing.+  bounded <-+    requireRight "refinement fixpoint domain" $+      constrainedDelaunay+        unitElementDefaults+        (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8])+        (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])+  let source = buildTriangulation bounded+      parameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 500+          , refineMaxArea = Just 3+          , refineExcludeOuterFaces = True+          }+  drained <- requireRight "drained refinement" (refine id parameters source)+  unless (refinementComplete drained) $+    fail "the refinement budget was too small to drain the worklist"+  -- Without this the fixpoint below is vacuous: a run that refined nothing+  -- trivially admits nothing on a second pass.+  unless (refinementAddedVertices drained > 0) $+    fail "the drained run inserted no Steiner points, so the fixpoint proves nothing"+  assertValid "drained refinement" (refinedTriangulation drained)+  again <- requireRight "second refinement pass" (refine id parameters (refinedTriangulation drained))+  assertEqual+    "a drained worklist admits nothing on a second pass"+    0+    (refinementAddedVertices again)+  starved <-+    requireRight+      "starved refinement"+      (refine id parameters {refineMaxAdditionalVertices = Just 3} source)+  when (refinementComplete starved) $+    fail "a three-vertex budget reported a drained worklist"+  assertEqual "a starved run spends exactly its budget" 3 (refinementAddedVertices starved)++testScalarFormat :: IO ()+testScalarFormat = do+  let binary64 = scalarBinaryFormat+  assertEqual "binary64 radix" 2 (formatRadix binary64)+  assertEqual "binary64 mantissa" 53 (formatMantissaDigits binary64)+  assertEqual "binary64 unit roundoff" (encodeFloat 1 (-53)) (scalarUnitRoundoff :: Double)++testWideDistanceNearestNeighbor :: IO ()+testWideDistanceNearestNeighbor = do+  let largePoints = V.fromList+        [ Point 0 0+        , Point 1.0e20 0+        , Point 2.0e20 0+        , Point 1.0e20 1.0e19+        ] :: V.Vector (Point)+  largeBuild <- requireRight "wide-distance build" (delaunay unitElementDefaults largePoints)+  let largeTriangulation = buildTriangulation largeBuild+  assertValid "wide-distance triangulation" largeTriangulation+  query <- requireQueryPoint "wide-distance query" (Point 1.05e20 0)+  case nearestNeighbor largeTriangulation Nothing query of+    Nothing -> fail "wide-distance nearest-neighbor returned Nothing"+    Just (nearest, _) -> assertEqual "wide-distance nearest-neighbor" (VertexId 1) nearest++data IncrementalLocationEvidence = IncrementalLocationEvidence+  { incrementalTriangulation :: !(DelaunayTriangulation (Point))+  , incrementalWalkSteps :: {-# UNPACK #-} !Int+  , incrementalFallbacks :: {-# UNPACK #-} !Int+  }++testIncrementalLocationDescent :: IO ()+testIncrementalLocationDescent = do+  small <- collectIncrementalLocationEvidence 500+  large <- collectIncrementalLocationEvidence 1000+  assertEqual "incremental location fallbacks/500" 0 (incrementalFallbacks small)+  assertEqual "incremental location fallbacks/1000" 0 (incrementalFallbacks large)+  unless (2 * incrementalWalkSteps large < 7 * incrementalWalkSteps small) $+    fail+      ( "incremental location approached quadratic growth: "+          <> show (incrementalWalkSteps small, incrementalWalkSteps large)+      )+  assertValid "incremental location descent/1000" (incrementalTriangulation large)++collectIncrementalLocationEvidence :: Int -> IO IncrementalLocationEvidence+collectIncrementalLocationEvidence count =+  V.foldM' insertAndAccumulate initialEvidence (V.fromList (randomPoints 0xc1ac_10ca count))+ where+  initialEvidence =+    IncrementalLocationEvidence+      { incrementalTriangulation = empty unitElementDefaults+      , incrementalWalkSteps = 0+      , incrementalFallbacks = 0+      }++  insertAndAccumulate evidence point = do+    result <- requireRight "incremental location descent" (insert (incrementalTriangulation evidence) point)+    let stats = insertionStats result+    pure+      IncrementalLocationEvidence+        { incrementalTriangulation = insertionTriangulation result+        , incrementalWalkSteps = incrementalWalkSteps evidence + statLocationWalkSteps stats+        , incrementalFallbacks = incrementalFallbacks evidence + statLocationFallbacks stats+        }++-- A persistent insertion locates on the frozen mesh before opening its dense+-- transaction. The thaw preserves every extant handle, so a lawful frozen+-- location can be interpreted directly without a second mutable walk. A+-- degenerate-line outside witness lacks the terminal-edge evidence its mutable+-- interpreter requires, so that one stratum deliberately retains the mutable+-- fallback. Exercise every frozen stratum, including the singleton's edge-less+-- outside witness, rather than testing only the ordinary face case.+testPersistentInsertionReusesFrozenLocation :: IO ()+testPersistentInsertionReusesFrozenLocation = do+  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)+  emptyLocation <- assertPersistentInsertionFromFrozenLocation "empty" Inserted vacant (Point 0 0)+  assertEqual "empty insertion frozen location" EmptyTriangulation emptyLocation++  singletonBuild <- requirePointBuild "singleton frozen location" [Point 0 0]+  singletonLocation <-+    assertPersistentInsertionFromFrozenLocation+      "singleton outside insertion"+      Inserted+      (buildTriangulation singletonBuild)+      (Point 2 0)+  assertEqual "singleton insertion frozen location" (OutsideConvexHull Nothing) singletonLocation++  lineBuild <- requirePointBuild "line frozen locations" [Point 0 0, Point 2 0, Point 4 0]+  let line = buildTriangulation lineBuild+  lineEdgeLocation <- assertPersistentInsertionFromFrozenLocation "line edge insertion" Inserted line (Point 1 0)+  case lineEdgeLocation of+    OnEdge _ -> pure ()+    other -> fail ("line edge insertion located " <> show other)+  lineOutsideLocation <- assertPersistentInsertionFromFrozenLocation "line extension" Inserted line (Point 6 0)+  case lineOutsideLocation of+    OutsideConvexHull (Just _) -> pure ()+    other -> fail ("line extension located " <> show other)++  triangleBuild <- requirePointBuild "area frozen locations" [Point 0 0, Point 4 0, Point 0 4]+  let triangle = buildTriangulation triangleBuild+  faceLocation <- assertPersistentInsertionFromFrozenLocation "face insertion" Inserted triangle (Point 1 1)+  case faceLocation of+    InFace _ -> pure ()+    other -> fail ("face insertion located " <> show other)+  edgeLocation <- assertPersistentInsertionFromFrozenLocation "area edge insertion" Inserted triangle (Point 2 0)+  case edgeLocation of+    OnEdge _ -> pure ()+    other -> fail ("area edge insertion located " <> show other)+  hullLocation <- assertPersistentInsertionFromFrozenLocation "hull insertion" Inserted triangle (Point 5 1)+  case hullLocation of+    OutsideConvexHull (Just _) -> pure ()+    other -> fail ("hull insertion located " <> show other)+  duplicateLocation <- assertPersistentInsertionFromFrozenLocation "duplicate insertion" AlreadyPresent triangle (Point 0 0)+  assertEqual "duplicate insertion frozen location" (OnVertex (VertexId 0)) duplicateLocation++assertPersistentInsertionFromFrozenLocation+  :: String+  -> InsertionDisposition+  -> DelaunayTriangulation (Point)+  -> Point+  -> IO Location+assertPersistentInsertionFromFrozenLocation label expectedDisposition source point = do+  query <- requireQueryPoint (label <> " frozen query") point+  let (located, walked) = locatePointWithHint source Nothing query+      sourceVertices = numVertices source+      usesMutableFallback =+        case located of+          OutsideConvexHull (Just _) -> numInnerFaces source == 0+          _ -> False+  result <- requireRight (label <> " insert") (insert source point)+  ((referenceVertex, referenceDisposition), reference, _) <-+    requireRight (label <> " session reference") $+      withSession source 1 (insertVertexAt point point)+  let stats = insertionStats result+      expectedVertices =+        case expectedDisposition of+          Inserted -> sourceVertices + 1+          AlreadyPresent -> sourceVertices+      expectedUnique =+        case expectedDisposition of+          Inserted -> 1+          AlreadyPresent -> 0+      expectedExisting =+        case expectedDisposition of+          Inserted -> 0+          AlreadyPresent -> 1+  assertEqual (label <> " disposition") expectedDisposition (insertionDisposition result)+  assertEqual (label <> " session disposition") referenceDisposition (insertionDisposition result)+  assertEqual (label <> " session vertex") referenceVertex (insertionVertex result)+  assertEqual+    (label <> " exact-location topology matches session")+    (canonicalEdges reference)+    (canonicalEdges (insertionTriangulation result))+  assertEqual (label <> " source remains unchanged") sourceVertices (numVertices source)+  assertEqual (label <> " result vertex count") expectedVertices (numVertices (insertionTriangulation result))+  assertEqual (label <> " input count") 1 (statInputPoints stats)+  assertEqual (label <> " unique count") expectedUnique (statUniquePoints stats)+  assertEqual (label <> " existing count") expectedExisting (statExistingPoints stats)+  assertEqual (label <> " duplicate count") expectedExisting (statDuplicatePoints stats)+  if usesMutableFallback+    then do+      let assertAtLeast counter expected actual =+            unless+              (actual >= expected)+              ( fail+                  ( label+                      <> " "+                      <> counter+                      <> " includes frozen evidence: expected at least "+                      <> show expected+                      <> ", got "+                      <> show actual+                  )+              )+      assertAtLeast "walk steps" (locationWalkSteps walked) (statLocationWalkSteps stats)+      assertAtLeast "walk maximum" (locationWalkSteps walked) (statLocationMaxWalk stats)+      assertAtLeast+        "fallback count"+        (if locationUsedFallback walked then 1 else 0)+        (statLocationFallbacks stats)+    else do+      assertEqual (label <> " frozen walk steps") (locationWalkSteps walked) (statLocationWalkSteps stats)+      assertEqual (label <> " frozen walk maximum") (locationWalkSteps walked) (statLocationMaxWalk stats)+      assertEqual+        (label <> " frozen fallback count")+        (if locationUsedFallback walked then 1 else 0)+        (statLocationFallbacks stats)+  assertValid (label <> " result") (insertionTriangulation result)+  pure located++-- Circle sweep must be a construction schedule, not a second topology. It is+-- compared against the arrival-order session kernel on the same exact inputs.+-- | One session, both verbs. The reason the two published sessions became one:+-- a caller who removes and inserts had to thaw twice and pay the O(n)+-- publication a session exists to delete.+--+-- Graded against the oracle that needs no second implementation — the Delaunay+-- triangulation of a point set in general position is unique, so a mixed edit+-- must land exactly where a fresh bulk load of the surviving set lands.+testMixedEditSession :: IO ()+testMixedEditSession = do+  let original = V.fromList (randomPoints 0x5eed1e 400)+      doomed = V.take 120 original+      survivors = V.drop 120 original+      arrivals = V.fromList (randomPoints 0xa7717a1 90)+  (_, edited, stats) <-+    requireRight "mixed session" $+      withSession+        (buildTriangulation (either (error . show) id (delaunay unitElementDefaults original)))+        (V.length arrivals)+        ( do+            V.mapM_ (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ()))) doomed+            V.mapM_ insertVertex arrivals+        )+  fresh <- requireRight "fresh rebuild" (delaunay unitElementDefaults (survivors <> arrivals))+  assertEqual+    "mixed edit equals a fresh build of the surviving set"+    (canonicalEdges (buildTriangulation fresh))+    (canonicalEdges edited)+  assertValid "mixed edit session" edited+  assertEqual+    "the whole transaction charged one counter set"+    (V.length arrivals)+    (statInputPoints stats)++-- The bulk removal verb must land exactly where the singleton fold lands, on+-- both sides of its locate-strategy crossover: a small batch keeps the+-- per-question walk, a large one buys the identity index once. Same removals,+-- same order, same survivor either way.+testBulkRemovalAgreement :: IO ()+testBulkRemovalAgreement = do+  let original = V.fromList (randomPoints 0xb01dca7 400)+      base = buildTriangulation (either (error . show) id (delaunay unitElementDefaults original))+      run label doomed = do+        (outcomes, survived, _) <-+          requireRight (label <> " bulk removal") (withSession base 0 (removeManyAt doomed))+        V.imapM_+          ( \index outcome ->+              maybe (fail (label <> " bulk removal missed index " <> show index)) (const (pure ())) outcome+          )+          outcomes+        (_, folded, _) <-+          requireRight (label <> " singleton removal fold") $+            withSession+              base+              0+              ( V.mapM_+                  (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ())))+                  doomed+              )+        assertEqual+          (label <> " bulk removal equals singleton descent")+          (canonicalEdges folded)+          (canonicalEdges survived)+        assertValid (label <> " bulk removal") survived+  run "walking" (V.take 40 original)+  run "indexed" (V.take 120 original)++-- The three fixture positions hash to home slot 15 in the 16-slot table made+-- for three keys. They therefore occupy 15, 0, and 1 in insertion order. The+-- middle deletion exercises both wraparound and backward shift, while the+-- coordinate overwrite models the tail move performed before the table handle+-- is renamed by 'swapRemoveVertex'.+testMutablePointIndexWraparoundBackshift :: IO ()+testMutablePointIndexWraparoundBackshift = do+  (removed, first, movedBeforeRelocation, relocated, movedAfterRelocation) <-+    requireRight "mutable point-index wraparound/backshift law" wraparoundLaw+  case removed of+    MutablePointIndexUpdated -> pure ()+    MutablePointIndexInvalidated -> fail "mutable point-index middle delete invalidated a valid table"+  assertEqual "mutable point-index first wraparound occupant" (Just 0) first+  assertEqual "mutable point-index shifted tail before relocation" (Just 2) movedBeforeRelocation+  case relocated of+    MutablePointIndexUpdated -> pure ()+    MutablePointIndexInvalidated -> fail "mutable point-index tail relocation invalidated a valid table"+  assertEqual "mutable point-index relocated tail" (Just 1) movedAfterRelocation+ where+  wraparoundLaw = runST $ do+    pointXs <- MUV.replicate 3 (0 :: Double)+    pointYs <- MUV.replicate 3 (0 :: Double)+    MUV.unsafeWrite pointXs 0 (-20)+    MUV.unsafeWrite pointYs 0 (-15)+    MUV.unsafeWrite pointXs 1 (-20)+    MUV.unsafeWrite pointYs 1 0+    MUV.unsafeWrite pointXs 2 (-20)+    MUV.unsafeWrite pointYs 2 7+    table <- newMutablePointIndex 3+    seeded <- seedMutablePointIndex table 3 (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs)+    case seeded of+      Left failure -> pure (Left failure)+      Right () -> do+        -- The tail has moved into slot one before identity transport begins.+        MUV.unsafeWrite pointXs 1 (-20)+        MUV.unsafeWrite pointYs 1 7+        removed <-+          removeMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 0 1+        first <-+          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) (-15)+        movedBeforeRelocation <-+          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 7+        relocated <- relocateMutablePoint table (-20) 7 2 1+        movedAfterRelocation <-+          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 7+        pure+          ( Right+              ( removed+              , first+              , movedBeforeRelocation+              , relocated+              , movedAfterRelocation+              )+          )++-- A dense table must also close lawfully when it removes every vertex. The+-- point-keyed query after freeze forces the empty published derivation rather+-- than retaining an impossible ST table.+testBatchIdentityIndexBatchToEmpty :: IO ()+testBatchIdentityIndexBatchToEmpty = do+  let points = V.fromList (randomPoints 0x7a110bad 32)+  built <- requireRight "batch identity empty base" (delaunay unitElementDefaults points)+  (outcomes, emptied, _) <-+    requireRight "batch identity removes every point" $+      withSession (buildTriangulation built) 0 (removeManyAt points)+  unless (V.all isJust outcomes) $+    fail "batch identity table missed a point while removing to empty"+  assertEqual "batch identity empty vertex count" 0 (numVertices emptied)+  assertValid "batch identity empty result" emptied+  absent <- requireRight "empty published identity lookup" (locateAndRemove emptied (Point 0 0))+  case absent of+    Nothing -> pure ()+    Just _ -> fail "empty published identity lookup manufactured a removal"++-- A dense batch discards its ST table before the next singleton handle rewrite.+-- 'excise' must therefore activate and transport the ordinary persistent index+-- without consulting the expired batch representation.+testBatchIdentityIndexToSingletonActive :: IO ()+testBatchIdentityIndexToSingletonActive = do+  let original = V.fromList (randomPoints 0x51a91e 64)+      doomed = V.take 32 original+      survivors = V.drop 32 original+  built <- requireRight "batch-to-singleton identity base" (delaunay unitElementDefaults original)+  (singletonOutcome, transitioned, _) <-+    requireRight "batch-to-singleton identity session" $+      withSession (buildTriangulation built) 0 $ do+        _ <- removeManyAt doomed+        excise (VertexId 0)+  let singletonPoint = removalOutcomePoint singletonOutcome+      expected = V.filter (/= singletonPoint) survivors+  fresh <- requireRight "batch-to-singleton fresh rebuild" (delaunay unitElementDefaults expected)+  assertEqual+    "batch-to-singleton identity topology"+    (canonicalEdges (buildTriangulation fresh))+    (canonicalEdges transitioned)+  assertValid "batch-to-singleton identity result" transitioned+  case V.find (/= singletonPoint) survivors of+    Nothing -> fail "batch-to-singleton fixture exhausted every survivor"+    Just remaining -> do+      located <- requireRight "batch-to-singleton published lookup" (locateAndRemove transitioned remaining)+      case located of+        Nothing -> fail "batch-to-singleton published lookup missed a survivor"+        Just removal -> assertValid "batch-to-singleton published removal" (removalTriangulation removal)++-- The mutable identity table is an internal section of @removeManyAt@, never a+-- session-wide owner. An insertion after the dense batch invalidates it, the+-- subsequent removal walks correctly, and the frozen mesh must rederive the+-- published identity cache from its final coordinate authority.+testBulkIdentityIndexDoesNotEscapeRemovalBatch :: IO ()+testBulkIdentityIndexDoesNotEscapeRemovalBatch = do+  let original = V.fromList (randomPoints 0x5a11ce 400)+      doomed = V.take 120 original+      survivors = V.drop 120 original+      arrival = Point (-0.25) 0.75+  baseBuild <- requireRight "bulk identity section base" (delaunay unitElementDefaults original)+  (_, edited, _) <-+    requireRight "bulk identity section mixed session" $+      withSession (buildTriangulation baseBuild) 1 $ do+        _ <- removeManyAt doomed+        _ <- insertVertexAt arrival arrival+        removeAt arrival >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ()))+  fresh <- requireRight "bulk identity section fresh survivor rebuild" (delaunay unitElementDefaults survivors)+  assertEqual+    "bulk identity section mixed program equals fresh survivors"+    (canonicalEdges (buildTriangulation fresh))+    (canonicalEdges edited)+  assertValid "bulk identity section mixed session" edited+  case V.uncons survivors of+    Nothing -> fail "bulk identity section test has no survivor"+    Just (survivor, _) -> do+      located <- requireRight "published lazy identity lookup" (locateAndRemove edited survivor)+      case located of+        Nothing -> fail "published lazy identity lookup missed survivor"+        Just removal -> assertValid "published lazy identity removal" (removalTriangulation removal)++-- The hierarchy-hinted removal program must land exactly where the unhinted+-- session lands. The guesses are all computed against the original base, so+-- later removals in the batch answer for guesses whose slots swap-compaction+-- has renamed — the walk must correct every one of them. The repaired+-- hierarchy must equal the reference rebuild over the same survivor.+testHierarchyRemovalAgreement :: IO ()+testHierarchyRemovalAgreement = do+  let original = V.fromList (randomPoints 0x5eed1e55 400)+      base = buildTriangulation (either (error . show) id (delaunay unitElementDefaults original))+  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint defaultHierarchyBranchFactor base)+  let run label doomed = do+        (outcomes, survived, repaired) <-+          requireRight (label <> " hinted removal") (removeManyWithHierarchy hierarchy base doomed)+        V.imapM_+          ( \index outcome ->+              maybe (fail (label <> " hinted removal missed index " <> show index)) (const (pure ())) outcome+          )+          outcomes+        (_, folded, _) <-+          requireRight (label <> " unhinted session") (withSession base 0 (removeManyAt doomed))+        reference <- requireRight (label <> " reference rebuild") (rebuildHierarchyHint hierarchy folded)+        assertEqual+          (label <> " hinted removal equals unhinted session")+          (canonicalEdges folded)+          (canonicalEdges survived)+        assertEqual (label <> " repaired hierarchy equals reference rebuild") reference repaired+        assertValid (label <> " hinted removal") survived+  run "sparse" (V.take 40 original)+  run "dense" (V.take 120 original)++testCircleSweepBulkLoad :: IO ()+testCircleSweepBulkLoad = do+  let points = V.fromList (randomPoints 0xc1ac1e 1500)+  swept <- requireRight "circle-sweep build" (delaunay unitElementDefaults points)+  (_, sessioned, _) <-+    requireRight "session build" $+      withSession (empty unitElementDefaults) (V.length points) $+        V.mapM_ insertVertex points+  assertEqual+    "circle-sweep/session topology"+    (canonicalEdges sessioned)+    (canonicalEdges (buildTriangulation swept))+  assertValid "circle-sweep build" (buildTriangulation swept)+  assertValid "session build" sessioned++testPredicates :: IO ()+testPredicates = do+  let a, b, c :: Point+      a = Point 0 0+      b = Point 1 0+      c = Point 0 1+  assertEqual "orientation left" GT (orient2d a b c)+  assertEqual "orientation right" LT (orient2d b a c)+  assertEqual "orientation collinear" EQ (orient2d a b (Point 0.5 0))+  assertEqual "incircle inside" GT (inCircle a b c (Point 0.25 0.25))+  assertEqual "incircle boundary" EQ (inCircle a b c (Point 1 1))+  assertEqual "incircle outside" LT (inCircle a b c (Point 2 2))+  assertEqual "underflow mitigation" (Point 0 1) (mitigateUnderflow (Point 1.0e-44 1 :: Point))+  _ <- requireRight "point validation" (validatePoint Nothing (Point 0 1 :: Point))+  let large = encodeFloat 1 180 :: Double+      ulp = encodeFloat 1 128 :: Double+  assertEqual+    "large exact orientation"+    GT+    (orient2d (Point large large) (Point (large + ulp) large) (Point large (large + ulp)))+  assertEqual+    "large exact cocircularity"+    EQ+    (inCircle+      (Point large large)+      (Point (large + ulp) large)+      (Point large (large + ulp))+      (Point (large + ulp) (large + ulp)))++testHandlesAndFiniteDcel :: IO ()+testHandlesAndFiniteDcel = do+  built <- requirePointBuild "handle algebra" [Point 0 0, Point 3 0, Point 0 2, Point 0.4 0.7]+  let triangulation = buildTriangulation built+  assertValid "handle algebra" triangulation+  assertEqual "one outer face" (numInnerFaces triangulation + 1) (numFaces triangulation)+  forM_ (directedEdges triangulation) $ \edge -> do+    assertEqual "double reversal" edge (reverseEdge (reverseEdge edge))+    assertEqual "next/previous" edge (previous triangulation (next triangulation edge))+    assertEqual "previous/next" edge (next triangulation (previous triangulation edge))+  forM_ (undirectedEdges triangulation) $ \edge -> do+    let (forward, backward) = directedPair edge+    assertEqual "pair reversal" backward (reverseEdge forward)+    assertEqual "undirected projection" edge (asUndirected forward)+  assertEqual "vertex iterator" (numVertices triangulation) (length (vertices triangulation))+  assertEqual+    "dense vertex projection"+    (V.fromList (fmap (vertexPoint triangulation) (vertices triangulation)))+    (vertexPoints triangulation)+  assertEqual "edge iterator" (numUndirectedEdges triangulation) (length (undirectedEdges triangulation))+  assertEqual "face iterator" (numInnerFaces triangulation) (length (innerFaces triangulation))+  assertEqual+    "dense inner-face directed-edge projection"+    (Just (V.toList (innerFaceDirectedEdgeTriples triangulation)))+    (traverse (Dcel.innerFaceDirectedEdges triangulation) (innerFaces triangulation))+  assertEqual+    "dense inner-face projection"+    (Just (V.toList (innerFaceVertexTriples triangulation)))+    (traverse (Dcel.innerFaceVertices triangulation) (innerFaces triangulation))+  assertEqual "dynamic vertex iterator" (numVertices triangulation) (length (DynamicIterators.vertexHandles triangulation))+  assertEqual "dynamic edge iterator" (numDirectedEdges triangulation) (length (DynamicIterators.directedEdgeHandles triangulation))+  assertEqual "dynamic face iterator" (numInnerFaces triangulation) (length (DynamicIterators.innerFaceHandles triangulation))+  unless (geometryTopologyBytes triangulation > topologyIndexBytes triangulation) $+    fail "geometry byte accounting omitted coordinates"+  vertex0 <- requireJust "dynamic vertex handle" (Dynamic.vertexHandle triangulation (VertexId 0))+  assertEqual "dynamic vertex fix" (VertexId 0) (Dynamic.fixVertex vertex0)+  assertEqual "dynamic vertex position" (vertexPoint triangulation (VertexId 0)) (Dynamic.vertexHandlePosition vertex0)+  case Dynamic.vertexHandleOutEdge vertex0 of+    Nothing -> fail "connected dynamic vertex has no outgoing edge"+    Just edge -> do+      assertEqual "dynamic edge reversal" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgeReverse (Dynamic.directedEdgeReverse edge)))+      assertEqual "dynamic edge next/previous" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgePrevious (Dynamic.directedEdgeNext edge)))+      let face = Dynamic.directedEdgeFace edge+      if Dynamic.faceIsOuter face+        then pure ()+        else case Dynamic.faceAsInner face of+          Nothing -> fail "non-outer dynamic face did not refine to InnerTag"+          Just inner -> do+            _ <- requireJust "inner dynamic face vertices" (Dynamic.innerFaceVertices inner)+            case Dynamic.innerFaceCircumcenter inner of+              Nothing -> fail "inner dynamic face has no circumcenter"+              Just _ -> pure ()++testDegenerateConstruction :: IO ()+testDegenerateConstruction = do+  emptyBuild <- requirePointBuild "empty" []+  emptyQuery <- requireQueryPoint "empty location" (Point 0 0)+  assertEqual "empty vertices" 0 (numVertices (buildTriangulation emptyBuild))+  assertEqual "empty location" EmptyTriangulation (locatePoint (buildTriangulation emptyBuild) emptyQuery)+  assertValid "empty" (buildTriangulation emptyBuild)++  singleton <- requirePointBuild "singleton" [Point 2 3]+  singletonQuery <- requireQueryPoint "singleton lookup" (Point 2 3)+  assertEqual "singleton lookup" (OnVertex (VertexId 0)) (locatePoint (buildTriangulation singleton) singletonQuery)+  assertValid "singleton" (buildTriangulation singleton)++  duplicates <- requirePointBuild "duplicates" [Point 0 0, Point 1 0, Point (-0.0) 0, Point 1 0, Point 2 0]+  assertEqual "deduplicated count" 3 (numVertices (buildTriangulation duplicates))+  assertEqual+    "stable duplicate mapping"+    (primArrayFromList [0, 1, 0, 1, 2])+    (buildInputVertices duplicates)+  assertValid "duplicates" (buildTriangulation duplicates)++  -- Deduplicating a signed zero is not the same claim as storing it canonically:+  -- the first only needs '==', which already identifies the two, while anything+  -- reading the bit pattern — a radix ordering, a byte-for-byte cross-check —+  -- reads the sign bit and gets a coordinate below every other. Both ingest+  -- paths canonicalize, and both are held to it here rather than to the weaker+  -- statement that comparison happens to survive.+  let bits point = (castDoubleToWord64 (pointX point), castDoubleToWord64 (pointY point))+  signedZero <- requirePointBuild "signed zero" [Point (-0.0) (-0.0), Point 1 0, Point 0 1]+  assertEqual "bulk load stores a canonical zero"+    (0, 0) (bits (vertexPoint (buildTriangulation signedZero) (VertexId 0)))+  incremental <-+    requireRight "signed zero insert"+      (insert (empty unitElementDefaults :: DelaunayTriangulation (Point)) (Point (-0.0) (-0.0)))+  assertEqual "insertion stores a canonical zero"+    (0, 0) (bits (vertexPoint (insertionTriangulation incremental) (VertexId 0)))++  collinear <- requirePointBuild "collinear" [Point 0 4, Point 0 0, Point 0 3, Point 0 2, Point 0 1]+  let line = buildTriangulation collinear+  assertEqual "line edge count" 4 (numUndirectedEdges line)+  assertEqual "line face count" 0 (numInnerFaces line)+  assertValid "collinear terminal splits" line++testPersistentLocalUpdates :: IO ()+testPersistentLocalUpdates = do+  base <- requirePointBuild "persistent base" (randomPoints 0x5eed 4095)+  let triangulation = buildTriangulation base+      query = Point 0.000_123 (-0.000_271)+  inserted <- requireRight "persistent insert" (insert triangulation query)+  assertEqual "persistent source remains unchanged" 4095 (numVertices triangulation)+  assertEqual "persistent result appends one vertex" 4096 (numVertices (insertionTriangulation inserted))+  assertValid "persistent insertion result" (insertionTriangulation inserted)++  let payloads = V.fromList+        [ SampleVertex (Point 0 0) 10+        , SampleVertex (Point 1 0) 20+        , SampleVertex (Point 0 1) 30+        ]+      defaults = ElementDefaults (0 :: Int) False ("face" :: String)+  payloadBuild <- requireRight "payload base" (delaunay defaults payloads)+  duplicate <- requireRight "payload-only replacement" (insert (buildTriangulation payloadBuild) (SampleVertex (Point 1 0) 99))+  assertEqual "payload update disposition" AlreadyPresent (insertionDisposition duplicate)+  assertEqual "payload replacement" 99 (sampleLabel (vertexData (insertionTriangulation duplicate) (VertexId 1)))++testGenericPayloads :: IO ()+testGenericPayloads = do+  let defaults = ElementDefaults (7 :: Int) False ("new-face" :: String)+      payloads = V.fromList+        [ SampleVertex (Point 0 0) 1+        , SampleVertex (Point 2 0) 2+        , SampleVertex (Point 0 2) 3+        , SampleVertex (Point 0.5 0.5) 4+        ]+  built <- requireRight "generic payload build" (delaunay defaults payloads)+  let triangulation = buildTriangulation built+  assertValid "generic payload build" triangulation+  assertEqual "vertex payload" 4 (sampleLabel (vertexData triangulation (VertexId 3)))+  forM_ (directedEdges triangulation) $ \edge -> assertEqual "directed default" 7 (directedEdgeData triangulation edge)+  forM_ (undirectedEdges triangulation) $ \edge -> assertEqual "undirected default" False (undirectedEdgeData triangulation edge)+  forM_ (allFaces triangulation) $ \face -> assertEqual "face default" "new-face" (faceData triangulation face)+  (firstEdge, firstFace) <- case (directedEdges triangulation, innerFaces triangulation) of+    (edge : _, face : _) -> pure (edge, face)+    _ -> fail "generic payload build produced no inner topology"+  let firstUndirected = asUndirected firstEdge+      changed = setFaceData (setUndirectedEdgeData (setDirectedEdgeData triangulation firstEdge 42) firstUndirected True) firstFace "changed"+  assertEqual "directed payload update" 42 (directedEdgeData changed firstEdge)+  assertEqual "undirected payload update" True (undirectedEdgeData changed firstUndirected)+  assertEqual "face payload update" "changed" (faceData changed firstFace)+  -- Geometry owns the points, so a payload carrying a different position is not+  -- a contradiction to be refused — it is a payload whose position nobody reads.+  let moved = setVertexData triangulation (VertexId 0) (SampleVertex (Point 9 9) 0)+  assertEqual "a payload position does not site a vertex"+    (vertexPoint triangulation (VertexId 0)) (vertexPoint moved (VertexId 0))+  assertEqual "the payload is stored as given"+    (Point 9 9) (samplePosition (vertexData moved (VertexId 0)))++-- The payload layer over a fixed geometry is a product of four free components.+-- Each is a functor and each is checked as such; nothing in the product can+-- disturb the geometry underneath it.+testPayloadMaps :: IO ()+testPayloadMaps = do+  let defaults = ElementDefaults (7 :: Int) ("new-undirected" :: String) ("new-face" :: String)+      payloads = V.fromList+        [ SampleVertex (Point 0 0) 1+        , SampleVertex (Point 4 0) 2+        , SampleVertex (Point 4 4) 3+        , SampleVertex (Point 0 4) 4+        , SampleVertex (Point 1 2) 5+        ]+  built <- requireRight "payload map build" (delaunay defaults payloads)+  let plain = buildTriangulation built+      -- Distinct payloads everywhere: a map that permuted its component would+      -- be invisible against uniform defaults.+      withDirected = List.foldl' (\t (label, edge) -> setDirectedEdgeData t edge label) plain (zip [100 ..] (directedEdges plain))+      withUndirected = List.foldl' (\t (label, edge) -> setUndirectedEdgeData t edge ("u-" <> show label)) withDirected (zip [(0 :: Int) ..] (undirectedEdges withDirected))+      sample = List.foldl' (\t (label, face) -> setFaceData t face ("f-" <> show label)) withUndirected (zip [(0 :: Int) ..] (allFaces withUndirected))+      endpoints ::+        Triangulation mode vertex directed undirected face ->+        [(VertexId, VertexId)]+      endpoints t = [(origin t edge, destination t edge) | edge <- directedEdges t]++  -- Identity. Equality on a triangulation compares geometry, topology,+  -- constraint flags and element defaults as well as payloads, so this single+  -- equation states that each map disturbs nothing but the component it names.+  assertEqual "mapDirectedEdges identity" sample (mapDirectedEdges id sample)+  assertEqual "mapUndirectedEdges identity" sample (mapUndirectedEdges id sample)+  assertEqual "mapFaces identity" sample (mapFaces id sample)+  assertEqual "mapVertices identity" sample (mapVertices id sample)++  assertEqual "mapDirectedEdges composition"+    (mapDirectedEdges ((* 2) . (+ 1)) sample)+    (mapDirectedEdges (* 2) (mapDirectedEdges (+ 1) sample))+  assertEqual "mapFaces composition"+    (mapFaces (("<" <>) . (<> ">")) sample)+    (mapFaces ("<" <>) (mapFaces (<> ">") sample))++  -- Commutation with the accessors. Identity and composition are blind to the+  -- indexing; this is the law that pins each payload to its own handle.+  let directedMapped = mapDirectedEdges (+ 1) sample+      undirectedMapped = mapUndirectedEdges ("<" <>) sample+      facesMapped = mapFaces ("<" <>) sample+  forM_ (directedEdges sample) $ \edge ->+    assertEqual "mapDirectedEdges commutes with directedEdgeData"+      (directedEdgeData sample edge + 1) (directedEdgeData directedMapped edge)+  forM_ (undirectedEdges sample) $ \edge ->+    assertEqual "mapUndirectedEdges commutes with undirectedEdgeData"+      ("<" <> undirectedEdgeData sample edge) (undirectedEdgeData undirectedMapped edge)+  forM_ (allFaces sample) $ \face ->+    assertEqual "mapFaces commutes with faceData"+      ("<" <> faceData sample face) (faceData facesMapped face)++  -- The components are independent, and none of them is geometry.+  assertEqual "face and directed maps commute"+    (mapFaces ("<" <>) (mapDirectedEdges (+ 1) sample))+    (mapDirectedEdges (+ 1) (mapFaces ("<" <>) sample))+  assertEqual "mapFaces preserves topology" (endpoints sample) (endpoints facesMapped)+  assertEqual "mapFaces preserves the face count" (numFaces sample) (numFaces facesMapped)+  forM_ (vertices sample) $ \vertex ->+    assertEqual "mapFaces preserves geometry" (vertexPoint sample vertex) (vertexPoint facesMapped vertex)++  -- The element default is a payload and must travel with them: every element a+  -- later insertion creates is handed the default, so a map that reindexed the+  -- stored payloads and left the default behind would produce a triangulation+  -- whose future elements disagree with its present ones. Every stored payload+  -- here differs from the default, so carrying the wrong one is visible.+  grownFaces <- insertionTriangulation <$> requireRight "insertion into mapped faces" (insert facesMapped (SampleVertex (Point 2 1) 6))+  grownUndirected <- insertionTriangulation <$> requireRight "insertion into mapped undirected edges" (insert undirectedMapped (SampleVertex (Point 2 1) 6))+  grownDirected <- insertionTriangulation <$> requireRight "insertion into mapped directed edges" (insert directedMapped (SampleVertex (Point 2 1) 6))+  unless (numFaces grownFaces > numFaces facesMapped) $ fail "the insertion created no face"+  unless ("<new-face" `elem` map (faceData grownFaces) (allFaces grownFaces)) $+    fail ("new faces did not receive the mapped default: " <> show (map (faceData grownFaces) (allFaces grownFaces)))+  unless ("<new-undirected" `elem` map (undirectedEdgeData grownUndirected) (undirectedEdges grownUndirected)) $+    fail "new undirected edges did not receive the mapped default"+  unless (8 `elem` map (directedEdgeData grownDirected) (directedEdges grownDirected)) $+    fail "new directed edges did not receive the mapped default"++  -- Vertices, the component that used to be special. The map is total, and the+  -- one thing worth insisting on is that a function which does its worst to the+  -- stored position still cannot move a vertex.+  let relabelled = mapVertices (\v -> v{sampleLabel = sampleLabel v * 10}) sample+      collapsed = mapVertices (\v -> v{samplePosition = Point 9 9}) sample+  forM_ (vertices sample) $ \vertex -> do+    assertEqual "mapVertices commutes with vertexData"+      (sampleLabel (vertexData sample vertex) * 10) (sampleLabel (vertexData relabelled vertex))+    assertEqual "mapVertices preserves geometry" (vertexPoint sample vertex) (vertexPoint relabelled vertex)+    assertEqual "a payload map cannot move a vertex"+      (vertexPoint sample vertex) (vertexPoint collapsed vertex)+  assertEqual "mapVertices composes"+    (mapVertices (\v -> v{sampleLabel = sampleLabel v + 1}) relabelled)+    (mapVertices (\v -> v{sampleLabel = sampleLabel v * 10 + 1}) sample)++  -- The sharpest statement of freedom available: the image type has no+  -- 'HasPosition' instance at all. This does not typecheck under a vertex+  -- component that geometry reads through.+  let projected = mapVertices sampleLabel sample+  forM_ (vertices sample) $ \vertex -> do+    assertEqual "a vertex payload need not have a position"+      (sampleLabel (vertexData sample vertex)) (vertexData projected vertex)+    assertEqual "projecting payloads away preserves geometry"+      (vertexPoint sample vertex) (vertexPoint projected vertex)++-- Geometry publication is payload forgetting, not a dense payload map.  The+-- polymorphic entrance accepts all four non-unit payload planes and publishes+-- zero materialized pages while retaining the exact structural planes.+testGeometryOnlyPublication :: IO ()+testGeometryOnlyPublication = do+  let defaults = ElementDefaults (17 :: Int) False ("source-face" :: String)+      payloads =+        V.fromList+          [ SampleVertex (Point 0 0) 11+          , SampleVertex (Point 4 0) 22+          , SampleVertex (Point 4 4) 33+          , SampleVertex (Point 0 4) 44+          , SampleVertex (Point 1 2) 55+          ]+  built <- requireRight "geometry-only payload source" (delaunay defaults payloads)+  let source = buildTriangulation built+      published = Internal.geometryOnlyPublication source+      republished = Internal.geometryOnlyPublication published+      structuralPlanes triangulation =+        ( Internal.triPointX triangulation+        , Internal.triPointY triangulation+        , Internal.triVertexOut triangulation+        , Internal.triHalfTopology triangulation+        , Internal.triFaceEdge triangulation+        , Internal.triConstraint triangulation+        , Internal.triConstraintCount triangulation+        , Internal.triConstraintEdges triangulation+        )+      payloadPageCounts triangulation =+        ( boxedMaterializedPageCount (Internal.triVertexData triangulation)+        , boxedMaterializedPageCount (Internal.triDirectedData triangulation)+        , boxedMaterializedPageCount (Internal.triUndirectedData triangulation)+        , boxedMaterializedPageCount (Internal.triFaceData triangulation)+        )+  unless (sumPayloadPages (payloadPageCounts source) > 0) $+    fail "geometry-only payload fixture did not materialize a source payload page"+  assertEqual+    "geometry-only publication preserves structural planes"+    (structuralPlanes source)+    (structuralPlanes published)+  assertEqual+    "geometry-only publication replaces every payload plane with a zero-page store"+    (0, 0, 0, 0)+    (payloadPageCounts published)+  assertEqual+    "geometry-only publication installs unit element defaults"+    unitElementDefaults+    (Internal.triElementDefaults published)+  assertEqual+    "geometry-only publication prepares the exact seam frontier"+    (Internal.prepareSeamFrontierIndex source)+    (Internal.triSeamFrontier published)+  assertEqual+    "geometry-only publication preserves an existing seam frontier"+    (Internal.triSeamFrontier published)+    (Internal.triSeamFrontier republished)+  assertValid "geometry-only publication" published+ where+  sumPayloadPages (vertexPages, directedPages, undirectedPages, facePages) =+    vertexPages + directedPages + undirectedPages + facePages++testPayloadTraversals :: IO ()+testPayloadTraversals = do+  let defaults = ElementDefaults (7 :: Int) ("new-undirected" :: String) ("new-face" :: String)+      payloads = V.fromList+        [ SampleVertex (Point 0 0) 1+        , SampleVertex (Point 4 0) 2+        , SampleVertex (Point 4 4) 3+        , SampleVertex (Point 0 4) 4+        , SampleVertex (Point 1 2) 5+        ]+  built <- requireRight "payload traversal build" (delaunay defaults payloads)+  let plain = buildTriangulation built+      withDirected = List.foldl' (\t (label, edge) -> setDirectedEdgeData t edge label) plain (zip [100 ..] (directedEdges plain))+      withUndirected = List.foldl' (\t (label, edge) -> setUndirectedEdgeData t edge ("u-" <> show label)) withDirected (zip [(0 :: Int) ..] (undirectedEdges withDirected))+      sample = List.foldl' (\t (label, face) -> setFaceData t face ("f-" <> show label)) withUndirected (zip [(0 :: Int) ..] (allFaces withUndirected))++  -- 'overPayloads' is the traversal under 'Identity', so this is the traversal+  -- identity law. It also says that rebuilding a payload store from its own+  -- contents is not observable, which is the part a paged store could get+  -- wrong: the traversal materializes pages the map would have left absent.+  assertEqual "vertexPayloads identity" sample (overPayloads vertexPayloads id sample)+  assertEqual "directedPayloads identity" sample (overPayloads directedPayloads id sample)+  assertEqual "undirectedPayloads identity" sample (overPayloads undirectedPayloads id sample)+  assertEqual "facePayloads identity" sample (overPayloads facePayloads id sample)++  -- Each traversal and its named map are one function. The effectful+  -- generalization is not allowed a second opinion about what relabeling means.+  assertEqual "vertexPayloads agrees with mapVertices"+    (mapVertices sampleLabel sample) (overPayloads vertexPayloads sampleLabel sample)+  assertEqual "directedPayloads agrees with mapDirectedEdges"+    (mapDirectedEdges (* 2) sample) (overPayloads directedPayloads (* 2) sample)+  assertEqual "undirectedPayloads agrees with mapUndirectedEdges"+    (mapUndirectedEdges ("<" <>) sample) (overPayloads undirectedPayloads ("<" <>) sample)+  assertEqual "facePayloads agrees with mapFaces"+    (mapFaces ("<" <>) sample) (overPayloads facePayloads ("<" <>) sample)++  assertEqual "facePayloads composes"+    (overPayloads facePayloads (("<" <>) . (<> ">")) sample)+    (overPayloads facePayloads ("<" <>) (overPayloads facePayloads (<> ">") sample))++  -- The class instances range over the face payload, being the last parameter.+  assertEqual "fmap is the face payload map" (mapFaces ("<" <>) sample) (fmap ("<" <>) sample)+  assertEqual "traverse is facePayloads"+    (Just (overPayloads facePayloads ("<" <>) sample))+    (traverse (Just . ("<" <>)) sample)+  assertEqual "the Foldable instance is the face traversal"+    (payloadList facePayloads sample) (foldr (:) [] sample)++  -- Visit order, and the element default's place in it. A fold that skipped+  -- the default would report the triangulation as holding one fewer face+  -- payload than it holds.+  assertEqual "vertexPayloads visits the vertices in order"+    (map (vertexData sample) (vertices sample))+    (payloadList vertexPayloads sample)+  assertEqual "facePayloads visits the faces and then the default"+    (map (faceData sample) (allFaces sample) <> [defaultFaceData (Internal.triElementDefaults sample)])+    (payloadList facePayloads sample)++  -- The point of the exercise: relabeling under an effect, with a refusal+  -- reaching the caller instead of a half-relabelled triangulation.+  let refuseAtThree :: SampleVertex -> Either String Int+      refuseAtThree v = if sampleLabel v == 3 then Left "vertex three refuses" else Right (sampleLabel v * 10)+      keepLabel :: SampleVertex -> Either String Int+      keepLabel = Right . sampleLabel+      decorate :: String -> Either String String+      decorate = Right . ("<" <>)+  assertEqual "an effectful relabel short-circuits"+    (Left "vertex three refuses") (vertexPayloads refuseAtThree sample)+  relabelled <- requireRight "effectful relabel" (vertexPayloads keepLabel sample)+  assertEqual "a successful effectful relabel is the pure one"+    (mapVertices sampleLabel sample) relabelled++  -- The default travels through the traversal, and travels exactly once: the+  -- store's fill and the element defaults are written from a single visit, so+  -- an element created afterwards inherits precisely what the traversal made.+  traversedFaces <- requireRight "effectful face relabel" (facePayloads decorate sample)+  grown <- insertionTriangulation <$> requireRight "insertion after traversal" (insert traversedFaces (SampleVertex (Point 2 1) 6))+  unless (numFaces grown > numFaces traversedFaces) $ fail "the insertion created no face"+  unless ("<new-face" `elem` map (faceData grown) (allFaces grown)) $+    fail ("new faces did not receive the traversed default: " <> show (map (faceData grown) (allFaces grown)))++-- | The in-circle predicate is the orientation of the four points lifted to+-- the paraboloid @z = x² + y²@. The referent is that 4×4 determinant evaluated+-- exactly over 'Rational' — deliberately not the translated 3×3 the instances+-- expand, which would only be the implementation checking its own algebra.+testParaboloidLift :: IO ()+testParaboloidLift = do+  let lifted :: Point -> (Rational, Rational, Rational)+      lifted (Point x y) =+        let (rx, ry) = (toRational x, toRational y) in (rx, ry, rx * rx + ry * ry)+      minor3+        :: (Rational, Rational, Rational)+        -> (Rational, Rational, Rational)+        -> (Rational, Rational, Rational)+        -> Rational+      minor3 (a1, a2, a3) (b1, b2, b3) (c1, c2, c3) =+        a1 * (b2 * c3 - b3 * c2) - a2 * (b1 * c3 - b3 * c1) + a3 * (b1 * c2 - b2 * c1)+      -- Laplace expansion of the lifted determinant along its column of ones.+      liftedOrientation+        :: (Rational, Rational, Rational)+        -> (Rational, Rational, Rational)+        -> (Rational, Rational, Rational)+        -> (Rational, Rational, Rational)+        -> Rational+      liftedOrientation a b c d =+        negate (minor3 b c d) + minor3 a c d - minor3 a b d + minor3 a b c+      predicted :: Point -> Point -> Point -> Point -> Ordering+      predicted a b c d = compare (liftedOrientation (lifted a) (lifted b) (lifted c) (lifted d)) 0+      measured :: Point -> Point -> Point -> Point -> Ordering+      measured (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =+        inCircleCoordinates ax ay bx by cx cy dx dy+      quadruples :: [Point] -> [(Point, Point, Point, Point)]+      quadruples (a : b : c : d : rest) = (a, b, c, d) : quadruples rest+      quadruples _ = []++  forM_ (quadruples (randomPoints 0x51ca_0b17 1600)) $ \(a, b, c, d) ->+    assertEqual ("in-circle is the lifted orientation at " <> show (a, b, c, d))+      (predicted a b c d) (measured a b c d)++  -- Exactly cocircular quadruples, which are exactly the ones the floating+  -- filter must decline to answer. Every Pythagorean point of the radius-5+  -- circle is representable without rounding, so 'EQ' here is a fact about the+  -- geometry rather than about the arithmetic.+  let ring =+        [ Point x y+        | (x, y) <-+            [ (5, 0), (0, 5), (-5, 0), (0, -5)+            , (3, 4), (4, 3), (-3, 4), (-4, 3)+            , (3, -4), (4, -3), (-3, -4), (-4, -3)+            ]+        ] :: [Point]+      indexed = zip [(0 :: Int) ..] ring+      cocircular =+        [ (a, b, c, d)+        | (i, a) <- indexed, (j, b) <- indexed, j > i+        , (k, c) <- indexed, k > j, (l, d) <- indexed, l > k+        ]+  unless (length cocircular == 495) $ fail ("expected 495 cocircular quadruples, got " <> show (length cocircular))+  forM_ cocircular $ \(a, b, c, d) -> do+    assertEqual ("cocircular points are cocircular at " <> show (a, b, c, d)) EQ (measured a b c d)+    assertEqual ("the lift agrees on cocircularity at " <> show (a, b, c, d)) EQ (predicted a b c d)++  -- The geometry the sign means, stated once against a circle anyone can read.+  let (a, b, c) = (Point 1 0, Point 0 1, Point (-1) 0) :: (Point, Point, Point)+  assertEqual "the centre is inside the circle" GT (measured a b c (Point 0 0))+  assertEqual "the antipode is on the circle" EQ (measured a b c (Point 0 (-1)))+  assertEqual "a distant point is outside" LT (measured a b c (Point 2 2))++-- | A payload labels an element, and an element is its geometry. Every rewrite+-- an insertion performs — an edge split, a face split, a Lawson flip — hands+-- some slot a different element to hold, and the label the displaced one+-- carried does not describe what took its place.+--+-- So: label every element of a triangulation by its own key, insert a point,+-- and demand that an element whose key survives still carries exactly the label+-- it was given while every element whose key is new carries the default and+-- nothing else. Both directions are checked; either alone is satisfiable by a+-- store that throws everything away.+--+-- The flip is the load-bearing case. Legalization is confluent, so which flips+-- fire and in what order is not observable in the topology that comes out. A+-- payload that rode through a flip would make it observable in the payload+-- plane, and a triangulation that is a normal form in one component and a+-- history in another is not a normal form.+testRewritePayloadIdentity :: IO ()+testRewritePayloadIdentity = do+  let defaults = ElementDefaults (0 :: Int) (0 :: Int) (0 :: Int)+      target = Point 0.001_37 (-0.002_11)+      corpus = randomPoints 0x1a2b3c4d 512+  built <- requireRight "rewrite identity base" (delaunay defaults (V.fromList corpus))+  let base = buildTriangulation built+      directedTable = labelTable (map (directedKeyOf base) (directedEdges base))+      undirectedTable = labelTable (map (undirectedKeyOf base) (undirectedEdges base))+      faceTable = labelTable (map (faceKeyOf base) (innerFaces base))++  -- Keys identify elements only if they are unique, so the premise is checked+  -- rather than assumed: a collapsed table would silently weaken everything+  -- below it into a test of nothing.+  assertEqual "directed keys are unique" (length (directedEdges base)) (Map.size directedTable)+  assertEqual "undirected keys are unique" (length (undirectedEdges base)) (Map.size undirectedTable)+  assertEqual "face keys are unique" (length (innerFaces base)) (Map.size faceTable)++  let withDirected =+        List.foldl' (\t e -> setDirectedEdgeData t e (directedTable Map.! directedKeyOf base e)) base (directedEdges base)+      withUndirected =+        List.foldl' (\t e -> setUndirectedEdgeData t e (undirectedTable Map.! undirectedKeyOf base e)) withDirected (undirectedEdges base)+      labelled =+        List.foldl' (\t f -> setFaceData t f (faceTable Map.! faceKeyOf base f)) withUndirected (innerFaces base)+  inserted <- requireRight "rewrite identity insert" (insert labelled target)+  let result = insertionTriangulation inserted+  assertValid "rewrite identity result" result+  assertEqual "the point was genuinely inserted" Inserted (insertionDisposition inserted)++  -- Both directions, on every plane. A surviving element keeps exactly its+  -- label; everything else holds the default and nothing else.+  assertPayloadSurvival "directed" directedTable+    [(directedKeyOf result e, directedEdgeData result e) | e <- directedEdges result]+  assertPayloadSurvival "undirected" undirectedTable+    [(undirectedKeyOf result e, undirectedEdgeData result e) | e <- undirectedEdges result]+  assertPayloadSurvival "face" faceTable+    [(faceKeyOf result f, faceData result f) | f <- innerFaces result]++  -- The flip signature. Every flip after an insertion joins the new vertex to+  -- the far corner of a cavity quad, so a new edge is not evidence of one — a+  -- split produces those too. A DESTROYED edge is: splitting a face destroys+  -- nothing and adds exactly three edges and two faces, which is asserted here+  -- so that the arithmetic holds, and under it every base key that is gone from+  -- the result was flipped away. Without this the paragraphs above are a claim+  -- about splits alone.+  assertEqual "the point landed strictly inside a face"+    (numUndirectedEdges base + 3, numFaces base + 2)+    (numUndirectedEdges result, numFaces result)+  let surviving = Set.fromList (map (undirectedKeyOf result) (undirectedEdges result))+      flippedAway = filter (`Set.notMember` surviving) (Map.keys undirectedTable)+  when (null flippedAway) (fail "the insertion caused no flip, so the flip case went unchecked")++  -- The same claim across one transaction that both retires and creates.+  -- Removal swap-compacts, which leaves a retired element's payload sitting in+  -- the slot it vacated; the inserts that follow are handed those slots back.+  -- Nothing else in the suite makes an allocation reissue a used slot.+  let doomed = take 60 corpus+      arrivals = randomPoints 0x5f3a19c2 60+  (_, edited, _) <-+    requireRight "rewrite identity session" $+      withSession labelled (length arrivals) $ do+        mapM_+          (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ())))+          doomed+        mapM_ insertVertex arrivals+  assertValid "rewrite identity session" edited+  assertPayloadSurvival "session directed" directedTable+    [(directedKeyOf edited e, directedEdgeData edited e) | e <- directedEdges edited]+  assertPayloadSurvival "session undirected" undirectedTable+    [(undirectedKeyOf edited e, undirectedEdgeData edited e) | e <- undirectedEdges edited]+  assertPayloadSurvival "session face" faceTable+    [(faceKeyOf edited f, faceData edited f) | f <- innerFaces edited]++directedKeyOf+  :: Triangulation mode vertex directed undirected face+  -> DirectedEdgeId+  -> (Point, Point)+directedKeyOf triangulation edge =+  ( vertexPoint triangulation (origin triangulation edge)+  , vertexPoint triangulation (destination triangulation edge)+  )++undirectedKeyOf+  :: Triangulation mode vertex directed undirected face+  -> UndirectedEdgeId+  -> (Point, Point)+undirectedKeyOf triangulation edge =+  case undirectedEndpoints triangulation edge of+    (from, to) ->+      let (left, right) = (vertexPoint triangulation from, vertexPoint triangulation to)+       in if left <= right then (left, right) else (right, left)++faceKeyOf+  :: Triangulation mode vertex directed undirected face+  -> FaceId+  -> [Point]+faceKeyOf triangulation face = sort (map (vertexPoint triangulation) (faceVertices triangulation face))++labelTable :: Ord key => [key] -> Map.Map key Int+labelTable keys = Map.fromList (zip keys [1 ..])++-- | Every element carries the label its key was given, or the default if its+-- key is new. Both counts are asserted too: a store that kept everything and a+-- store that kept nothing each satisfy one half of this on its own.+assertPayloadSurvival :: (Ord key, Show key) => String -> Map.Map key Int -> [(key, Int)] -> IO ()+assertPayloadSurvival plane labels elements = do+  let kept = length (filter ((`Map.member` labels) . fst) elements)+  when (kept == 0) (fail (plane <> ": the insertion perturbed every element"))+  when (kept == length elements) (fail (plane <> ": the insertion perturbed no element"))+  forM_ elements $ \(key, payload) ->+    assertEqual (plane <> " label at " <> show key) (Map.findWithDefault 0 key labels) payload++testPointLocationAndHints :: IO ()+testPointLocationAndHints = do+  built <- requirePointBuild "location" (randomPoints 0x1234_5678 1200)+  let triangulation = buildTriangulation built+  queries <- traverse (requireQueryPoint "location query") (take 250 (randomPoints 0xdead_beef 250))+  let+      baseline = sum [locationWalkSteps stats | query <- queries, let (_, stats) = locatePointWithHint triangulation Nothing query]+  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint 16 triangulation)+  let hinted = sum+        [ locationWalkSteps stats+        | query <- queries+        , let hint = hierarchyHint hierarchy query+              (_, stats) = locatePointWithHint triangulation hint query+        ]+  unless (hinted <= baseline) $+    fail ("hierarchy hint increased aggregate walking: " <> show (baseline, hinted))+  staleHintQuery <- requireQueryPoint "stale hint query" (Point 0.125 (-0.375))+  let staleVertexHint = VertexHint (VertexId (fromIntegral (numVertices triangulation)))+  assertEqual+    "stale vertex hint falls back to the canonical start face"+    (locatePointWithHint triangulation Nothing staleHintQuery)+    (locatePointWithHint triangulation (Just staleVertexHint) staleHintQuery)+  forM_ (vertices triangulation) $ \vertex -> do+    vertexQuery <- requireQueryPoint "vertex lookup" (vertexPoint triangulation vertex)+    assertEqual "vertex lookup" (OnVertex vertex) (locatePoint triangulation vertexQuery)+  let insertedPoint = Point 0.1234567 (-0.2345678)+  inserted <- requireRight "hierarchy incremental source" (insert triangulation insertedPoint)+  let updatedTriangulation = insertionTriangulation inserted+  updatedHierarchy <-+    requireRight+      "hierarchy incremental update"+      ( updateHierarchyAfterInsertion+          hierarchy+          insertedPoint+          (insertionVertex inserted)+          (insertionDisposition inserted)+      )+  rebuiltHierarchy <- requireRight "hierarchy reference rebuild" (buildHierarchyHint 16 updatedTriangulation)+  assertEqual "incremental hierarchy equals canonical rebuild" rebuiltHierarchy updatedHierarchy++-- The hierarchy replaces its level-to-base correspondence with the arithmetic+-- claim that level-local handle @j@ names base vertex @j * branch@. Query each+-- sampled vertex with its own position: the descent must return that very+-- vertex, at every branch factor and along the whole of level 0. A stride the+-- construction does not actually obey shows up here as a named mismatch.+testHierarchyNestingLaw :: IO ()+testHierarchyNestingLaw = do+  built <- requirePointBuild "hierarchy nesting" (randomPoints 0x0f1e_2d3c 900)+  let triangulation = buildTriangulation built+  forM_ [2, 3, 16] $ \branch -> do+    hierarchy <- requireRight ("hierarchy build at branch " <> show branch) (buildHierarchyHint branch triangulation)+    let sampled = [0, branch .. numVertices triangulation - 1]+    forM_ sampled $ \index -> do+      let vertex = VertexId (fromIntegral index)+      vertexQuery <- requireQueryPoint "sampled hierarchy vertex" (vertexPoint triangulation vertex)+      assertEqual+        ("branch " <> show branch <> " descent onto sampled vertex " <> show index)+        (Just (VertexHint vertex))+        (hierarchyHint hierarchy vertexQuery)++testSibsonInterpolation :: IO ()+testSibsonInterpolation = do+  built <- requirePointBuild "sibson" (gridPoints 9 9)+  let triangulation = buildTriangulation built+      queryPoint = Point 3.25 4.4+      linear vertex = let Point x y = vertexPoint triangulation vertex in 2 * x - 3 * y + 5+      expected = let Point x y = queryPoint in 2 * x - 3 * y + 5+  query <- requireQueryPoint "Sibson query" queryPoint+  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)+  result <- stToIO (naturalNeighborWeights workspace Nothing query)+  let weights = naturalNeighborValues result+  unless (V.length weights >= 3) $ fail "Sibson query did not discover a natural-neighbor cavity"+  assertNear "Sibson partition" 1.0e-11 1 (V.sum (V.map snd weights))+  unless (V.all ((>= (-1.0e-12)) . snd) weights) $ fail "Sibson produced a negative weight"+  (folded, _, foldedStats) <- stToIO (foldNaturalNeighborWeights+    (\total vertex weight -> total + weight * linear vertex)+    0+    workspace+    Nothing+    query)+  assertNear "allocation-free Sibson fold" 2.0e-9 expected folded+  assertEqual "Sibson fold neighbor count" (V.length weights) (interpolationNaturalNeighbors foldedStats)+  (interpolated, _) <- stToIO (interpolateNaturalNeighbor linear workspace Nothing query)+  case interpolated of+    Nothing -> fail "Sibson interpolation rejected an interior query"+    Just value -> assertNear "Sibson affine precision" 2.0e-9 expected value+  let gradients = estimateGradients linear triangulation+  V.forM_ gradients $ \(gx, gy) -> do+    assertNear "planar gradient x" 1.0e-9 2 gx+    assertNear "planar gradient y" 1.0e-9 (-3) gy+  let gradient (VertexId raw) = gradients V.! fromIntegral raw+  (gradientValue, _) <- stToIO (interpolateNaturalNeighborGradient linear gradient 0.5 workspace Nothing query)+  case gradientValue of+    Nothing -> fail "gradient natural-neighbor interpolation rejected an interior query"+    Just value -> assertNear "gradient affine precision" 2.0e-9 expected value+  unless (workspaceBytes workspace > 0) $ fail "Sibson workspace byte accounting is empty"++testVoronoiDual :: IO ()+testVoronoiDual = do+  built <- requirePointBuild "voronoi" [Point 0 0, Point 2 0, Point 0 2, Point 2 2, Point 1 1]+  let triangulation = buildTriangulation built+  assertEqual "Voronoi face count" (numVertices triangulation) (length (voronoiFaces triangulation))+  assertEqual "directed dual edge count" (numDirectedEdges triangulation) (length (directedVoronoiEdges triangulation))+  assertEqual "undirected dual edge count" (numUndirectedEdges triangulation) (length (undirectedVoronoiEdges triangulation))+  forM_ (directedVoronoiEdges triangulation) $ \edge -> do+    assertEqual "dual double reversal" edge (reverseVoronoiEdge (reverseVoronoiEdge edge))+    assertEqual "dual next/previous" edge (voronoiPrevious triangulation (voronoiNext triangulation edge))+    assertEqual+      "dual face/site"+      (origin triangulation (asDelaunayDirectedEdge edge))+      (voronoiFaceSite (voronoiIncidentFace triangulation edge))+    case voronoiEdgeGeometry triangulation edge of+      Nothing -> fail "valid dual edge has no geometry"+      Just _ -> pure ()+  case directedVoronoiEdges triangulation of+    [] -> fail "Voronoi test produced no directed dual edge"+    first : _ -> do+      handle <- requireJust "dynamic Voronoi edge" (VoronoiDynamic.directedVoronoiEdgeHandle triangulation first)+      assertEqual "dynamic dual fix" first (VoronoiDynamic.fixDirectedVoronoiEdge handle)+      assertEqual "dynamic dual reversal" first (VoronoiDynamic.fixDirectedVoronoiEdge (VoronoiDynamic.voronoiEdgeReverseH (VoronoiDynamic.voronoiEdgeReverseH handle)))+      assertEqual "dynamic dual/primal conversion" (asDelaunayDirectedEdge first) (Dynamic.fixDirectedEdge (VoronoiDynamic.voronoiEdgeAsDelaunayH handle))+      let dualFace = VoronoiDynamic.voronoiEdgeFaceH handle+      assertEqual "dynamic dual face site" (origin triangulation (asDelaunayDirectedEdge first)) (Dynamic.fixVertex (VoronoiDynamic.voronoiFaceSiteH dualFace))+      let source = VoronoiDynamic.voronoiEdgeFromH handle+      case VoronoiDynamic.voronoiVertexAsDelaunayFaceH source of+        Just inner -> case VoronoiDynamic.voronoiVertexPositionH source of+          Nothing -> fail "inner dynamic Voronoi vertex has no position"+          Just voronoiPosition -> assertEqual "inner dynamic Voronoi position" (Dynamic.innerFaceCircumcenter inner) (Just voronoiPosition)+        Nothing -> unless (isJust (VoronoiDynamic.voronoiVertexAsOuterEdgeH source)) $+          fail "outer dynamic Voronoi vertex has no defining edge"++testRemoval :: IO ()+testRemoval = do+  built <- requirePointBuild "removal" [Point 0 0, Point 3 0, Point 3 3, Point 0 3, Point 1.5 1.5]+  let triangulation = buildTriangulation built+  assertEqual+    "out-of-range removal obstruction"+    (Left (RemovalVertexOutOfRange (VertexId 5) 5))+    (void (removeVertex triangulation (VertexId 5)))+  removedInterior <- requireRight "interior removal" (removeVertex triangulation (VertexId 4))+  assertEqual "interior removed point" (Point 1.5 1.5) (removalOutcomePoint (removalOutcome removedInterior))+  assertEqual "interior removal count" 4 (numVertices (removalTriangulation removedInterior))+  assertValid "interior removal" (removalTriangulation removedInterior)+  let beforeHullRemoval = removalTriangulation removedInterior+      previousLastVertex = VertexId (fromIntegral (numVertices beforeHullRemoval - 1))+      swappedPoint = vertexPoint beforeHullRemoval previousLastVertex+      swappedData = vertexData beforeHullRemoval previousLastVertex+  removedHull <- requireRight "hull removal" (removeVertex beforeHullRemoval (VertexId 0))+  assertEqual "hull removal count" 3 (numVertices (removalTriangulation removedHull))+  case removalOutcomeSwap (removalOutcome removedHull) of+    Nothing -> fail "hull removal omitted the swap-compacted vertex handle"+    Just (swappedIn, swappedInPoint) -> do+      assertEqual "hull swapped-in handle" (VertexId 0) swappedIn+      assertEqual "hull swapped-in point" swappedPoint (vertexPoint (removalTriangulation removedHull) swappedIn)+      assertEqual "hull swapped-in payload" swappedData (vertexData (removalTriangulation removedHull) swappedIn)+      -- The reported position must be the one the arena now holds, bit for+      -- bit: a caller seeding a search from it is seeding from the mesh.+      assertEqual "hull swapped-in reported position" swappedPoint swappedInPoint+  assertValid "hull removal" (removalTriangulation removedHull)++  let afterHullRemoval = removalTriangulation removedHull+      lastVertex = VertexId (fromIntegral (numVertices afterHullRemoval - 1))+  removedLast <- requireRight "last-vertex removal" (removeVertex afterHullRemoval lastVertex)+  assertEqual+    "removing the last vertex relocates nothing"+    Nothing+    (removalOutcomeSwap (removalOutcome removedLast))+  assertEqual "last-vertex removal count" 2 (numVertices (removalTriangulation removedLast))+  assertValid "last-vertex removal" (removalTriangulation removedLast)++  lineBuild <- requirePointBuild "line removal" [Point 0 0, Point 1 0, Point 2 0, Point 3 0]+  lineMiddle <- requireRight "line middle removal" (removeVertex (buildTriangulation lineBuild) (VertexId 1))+  assertEqual "line middle count" 3 (numVertices (removalTriangulation lineMiddle))+  assertValid "line middle removal" (removalTriangulation lineMiddle)++  degreeThreeBuild <-+    requirePointBuild+      "degree-three removal"+      [Point 0 0, Point 4 0, Point 0 4, Point 1 1]+  let degreeThreeMapping = buildInputVertices degreeThreeBuild+  case if 3 < sizeofPrimArray degreeThreeMapping+        then Just (VertexId (indexPrimArray degreeThreeMapping 3))+        else Nothing of+    Nothing -> fail "degree-three removal input mapping omitted the interior vertex"+    Just centerVertex -> do+      degreeThree <-+        requireRight+          "degree-three interior removal"+          (removeVertex (buildTriangulation degreeThreeBuild) centerVertex)+      assertEqual "degree-three removal count" 3 (numVertices (removalTriangulation degreeThree))+      assertValid "degree-three interior removal" (removalTriangulation degreeThree)++  -- A removal hands its vertex's whole ring to edge/face cleanup, so a+  -- high-degree star is the only shape that exercises the ordered-set path+  -- there; an ordinary mesh keeps degrees near six and never leaves the+  -- insertion sort. Radii are jittered so no four rim points are cocircular.+  let rimCount = 48+      rimPoint index =+        let angle = 2 * pi * fromIntegral index / fromIntegral rimCount+            radius = 1 + 0.001 * fromIntegral (index `mod` 7)+         in Point (radius * cos angle) (radius * sin angle)+  starBuild <-+    requirePointBuild+      "high-degree removal"+      (Point 0 0 : map rimPoint [0 .. rimCount - 1])+  let starTriangulation = buildTriangulation starBuild+      starCentre = VertexId (indexPrimArray (buildInputVertices starBuild) 0)+      centreDegree =+        length+          [ ()+          | edge <- undirectedEdges starTriangulation+          , let (from, to) = undirectedEndpoints starTriangulation edge+          , from == starCentre || to == starCentre+          ]+  assertEqual "high-degree centre ring" rimCount centreDegree+  starRemoved <-+    requireRight "high-degree interior removal" (removeVertex starTriangulation starCentre)+  assertEqual "high-degree removal count" rimCount (numVertices (removalTriangulation starRemoved))+  assertEqual+    "high-degree removed point"+    (Point 0 0)+    (removalOutcomePoint (removalOutcome starRemoved))+  assertValid "high-degree interior removal" (removalTriangulation starRemoved)++  -- A transaction that refuses publishes nothing: the refusal is the whole+  -- answer, so no half-remeshed arena can reach a caller as a triangulation.+  let sessionRefusal = RemovalVertexOutOfRange (VertexId 99) 5+  assertEqual+    "a refused session publishes nothing"+    (Left sessionRefusal)+    (void (withSession triangulation 1 (refuse sessionRefusal :: Session s (Point) () () () ())))++  -- Refusal short-circuits: an edit after it never runs, so the mesh the+  -- transaction abandoned is the mesh it was handed.+  assertEqual+    "a refusal abandons the edits behind it"+    (Left sessionRefusal)+    ( void+        ( withSession+            triangulation+            1+            ( ( do+                  _ <- removeAt (Point 0 0)+                  _ <- refuse sessionRefusal+                  removeAt (Point 1 1)+              ) ::+                Session s (Point) () () () (Maybe (RemovalOutcome (Point)))+            )+        )+    )++  -- The two point-keyed entries must publish one story. A handle-keyed removal+  -- locates nothing, while the coordinate-keyed route resolves the same site+  -- through the exact derived identity section rather than a topological walk.+  -- Both therefore charge no location steps and retire the same vertex.+  handleRemoval <- requireRight "handle removal stats" (removeVertex triangulation (VertexId 4))+  assertEqual+    "a handle-keyed removal locates nothing"+    0+    (statLocationWalkSteps (removalStats handleRemoval))+  locatedRemoval <-+    requireRight "point removal stats" (locateAndRemove triangulation (Point 1.5 1.5))+  pointRemoval <- requireJust "point removal located a vertex" locatedRemoval+  assertEqual+    "a point-keyed removal performs no topological location walk"+    0+    (statLocationWalkSteps (removalStats pointRemoval))+  assertEqual+    "point-keyed and handle-keyed removal publish the same mesh"+    (canonicalEdges (removalTriangulation handleRemoval))+    (canonicalEdges (removalTriangulation pointRemoval))++testConstrainedDelaunay :: IO ()+testConstrainedDelaunay = do+  base <- requirePointBuild "CDT base" [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 1 1, Point 3 3, Point 1 3, Point 3 1]+  let cdt0 = fromDelaunay (buildTriangulation base)+  diagonalBatch <-+    requireRight+      "constraint recovery"+      (recoverConstraints cdt0 (V.singleton (VertexId 0, VertexId 2)))+  (diagonalPath, diagonalAdded) <-+    requireAcceptedConstraint "constraint recovery" diagonalBatch+  let cdt1 = constraintBatchTriangulation diagonalBatch+  when (V.null diagonalPath) $ fail "constraint recovery returned an empty path"+  assertEqual "constraint count" diagonalAdded (numConstraints cdt1)+  assertCdtValid "constraint recovery" cdt1+  conflictBatch <-+    requireRight+      "atomic conflict"+      (recoverConstraints cdt1 (V.singleton (VertexId 1, VertexId 3)))+  case V.toList (constraintBatchOutcomes conflictBatch) of+    [ConstraintRejected _] -> pure ()+    outcomes ->+      fail+        ( "crossing constraint was not rejected atomically: "+            <> show outcomes+        )+  assertEqual+    "crossing rejection preserves topology"+    cdt1+    (constraintBatchTriangulation conflictBatch)+  let constraintProgram =+        V.fromList+          [ (VertexId 0, VertexId 2)+          , (VertexId 1, VertexId 3)+          , (VertexId 4, VertexId 6)+          ]+  wholeProgram <-+    requireRight+      "whole constraint program"+      (recoverConstraints cdt0 constraintProgram)+  (singletonTriangulation, singletonOutcomes) <-+    requireRight+      "singleton constraint program"+      (V.foldM' replayConstraintRequest (cdt0, []) constraintProgram)+  assertEqual+    "batch outcomes equal singleton descent"+    (V.toList (constraintBatchOutcomes wholeProgram))+    (reverse singletonOutcomes)+  assertEqual+    "batch topology equals singleton descent"+    singletonTriangulation+    (constraintBatchTriangulation wholeProgram)+  assertBatchStats "whole constraint program" wholeProgram+  forM_ [0 .. 7 :: Int] $ \batchIndex -> do+    randomizedBuild <-+      requirePointBuild+        ("random constraint batch " <> show batchIndex)+        (randomPoints (0x6a09_e667_f3bc_c909 + fromIntegral batchIndex) 40)+    let randomizedBase = fromDelaunay (buildTriangulation randomizedBuild)+        mapping = buildInputVertices randomizedBuild+        handles = V.generate (sizeofPrimArray mapping) (VertexId . indexPrimArray mapping)+        requests = V.take 18 (V.zip handles (V.reverse handles))+    randomizedBatch <-+      requireRight+        ("random whole constraint batch " <> show batchIndex)+        (recoverConstraints randomizedBase requests)+    (randomizedSingleton, randomizedOutcomes) <-+      requireRight+        ("random singleton constraint batch " <> show batchIndex)+        (V.foldM' replayConstraintRequest (randomizedBase, []) requests)+    assertEqual+      ("random batch outcomes equal singleton descent " <> show batchIndex)+      (V.toList (constraintBatchOutcomes randomizedBatch))+      (reverse randomizedOutcomes)+    assertEqual+      ("random batch topology equals singleton descent " <> show batchIndex)+      randomizedSingleton+      (constraintBatchTriangulation randomizedBatch)+    assertBatchStats+      ("random constraint batch " <> show batchIndex)+      randomizedBatch+    assertCdtValid+      ("random constraint batch " <> show batchIndex)+      (constraintBatchTriangulation randomizedBatch)+  -- Batch admission returns the first typed obstruction. Callers that need a+  -- complete diagnosis ask the explicit corridor query and alone pay for it.+  diagnosisBatch <-+    requireRight+      "conflict diagnosis"+      (recoverConstraints cdt1 (V.singleton (VertexId 6, VertexId 7)))+  case V.toList (constraintBatchOutcomes diagnosisBatch) of+    [ConstraintRejected blocking] -> do+      let diagnosed =+            fmap+              asUndirected+              (getConflictingEdgesBetweenVertices cdt1 (VertexId 6) (VertexId 7))+      case diagnosed of+        [] -> fail "explicit conflict diagnosis named no edge"+        firstBlocking : _ ->+          assertEqual "batch rejection is the first corridor obstruction" firstBlocking blocking+      assertEqual+        "explicit conflict diagnosis is deduplicated"+        (length diagnosed)+        (length (Set.fromList diagnosed))+      forM_ diagnosed $ \edge ->+        unless (isConstraintEdge cdt1 edge) $+          fail ("explicit conflict diagnosis named a non-constraint edge: " <> show edge)+    outcomes ->+      fail+        ( "a constraint crossing the diagonal was not rejected: "+            <> show outcomes+        )++  split <- requireRight "constraint split" (addConstraintAndSplit id cdt1 (VertexId 1) (VertexId 3))+  let splitCdt = constraintRecoveryTriangulation split+  unless (numVertices splitCdt > numVertices cdt1) $ fail "constraint split did not insert an intersection vertex"+  assertCdtValid "constraint split" splitCdt++  -- The batch splitter must agree with singleton descent, including across a+  -- suspension: every vertical below is constrained only inside the batch+  -- itself, so no census against the base can reserve for its crossings and+  -- the chunk's vertex reservation exhausts mid-batch. The driver publishes,+  -- re-reserves against the published mesh, and resumes; the final mesh must+  -- not know any of that happened.+  let bandColumns = [0 .. 7 :: Int]+      bandVertices =+        V.fromList+          ( Point 0 5+              : Point 90 5+              : concat+                  [ [Point x 10, Point x 0]+                  | column <- bandColumns+                  , let x = 10 * fromIntegral column + 5+                  ]+          )+  bandBuild <-+    requireRight+      "split band base"+      (constrainedDelaunayMaximal unitElementDefaults bandVertices V.empty)+  let bandAcceptedBuild = cdtAcceptedBuild bandBuild+      bandMapping = buildInputVertices bandAcceptedBuild+      bandHandle input = VertexId (indexPrimArray bandMapping input)+      bandBase = buildTriangulation bandAcceptedBuild+      bandRequests =+        V.fromList+          ( (bandHandle 0, bandHandle 1)+              : [ (bandHandle (2 * column + 2), bandHandle (2 * column + 3))+                | column <- bandColumns+                ]+          )+      replaySplit+        :: ConstrainedDelaunayTriangulation (Point)+        -> (VertexId, VertexId)+        -> Either (CdtError) (ConstrainedDelaunayTriangulation (Point))+      replaySplit triangulation request =+        constraintRecoveryTriangulation+          <$> uncurry (addConstraintAndSplit id triangulation) request+  bandBatch <-+    requireRight+      "split batch"+      (addConstraintsAndSplit id bandBase bandRequests)+  bandDescent <-+    requireRight+      "split singleton descent"+      (V.foldM' replaySplit bandBase bandRequests)+  assertEqual+    "split batch topology equals singleton descent"+    bandDescent+    (constraintRecoveryTriangulation bandBatch)+  assertEqual+    "split batch added one vertex per crossing"+    (numVertices bandBase + length bandColumns)+    (numVertices (constraintRecoveryTriangulation bandBatch))+  assertCdtValid "split batch" (constraintRecoveryTriangulation bandBatch)++  -- The same law with the reservation exhausting inside one corridor rather+  -- than between corridors: the closing horizontal crosses two constraints+  -- the census can see and fifteen it cannot, so the corridor suspends with+  -- its cursor mid-walk and the resumed transaction continues from the last+  -- split vertex, not from the corridor's start.+  let laceColumns = [0 .. 16 :: Int]+      laceVertices =+        V.fromList+          ( Point 0 5+              : Point 90 5+              : concat+                  [ [Point x 10, Point x 0]+                  | column <- laceColumns+                  , let x = 5 * fromIntegral column + 5+                  ]+          )+      laceBuiltIn = V.fromList [(2, 3), (34, 35)]+  laceBuild <-+    requireRight+      "split lace base"+      (constrainedDelaunayMaximal unitElementDefaults laceVertices laceBuiltIn)+  let laceAcceptedBuild = cdtAcceptedBuild laceBuild+      laceMapping = buildInputVertices laceAcceptedBuild+      laceHandle input = VertexId (indexPrimArray laceMapping input)+      laceBase = buildTriangulation laceAcceptedBuild+      laceRequests =+        V.fromList+          ( [ (laceHandle (2 * column + 2), laceHandle (2 * column + 3))+            | column <- [1 .. 15]+            ]+              <> [(laceHandle 0, laceHandle 1)]+          )+  laceBatch <-+    requireRight+      "split lace batch"+      (addConstraintsAndSplit id laceBase laceRequests)+  laceDescent <-+    requireRight+      "split lace singleton descent"+      (V.foldM' replaySplit laceBase laceRequests)+  assertEqual+    "split lace batch topology equals singleton descent"+    laceDescent+    (constraintRecoveryTriangulation laceBatch)+  assertEqual+    "split lace batch added one vertex per crossing"+    (numVertices laceBase + length laceColumns)+    (numVertices (constraintRecoveryTriangulation laceBatch))+  assertCdtValid "split lace batch" (constraintRecoveryTriangulation laceBatch)++  let verticesInput :: V.Vector (Point)+      verticesInput = V.fromList [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 0 0]+      constraintsInput = V.fromList [(0, 2), (1, 3), (4, 1)]+  bulk <- requireRight "stable CDT bulk load" (constrainedDelaunayMaximal unitElementDefaults verticesInput constraintsInput)+  let bulkAcceptedBuild = cdtAcceptedBuild bulk+      bulkMapping = buildInputVertices bulkAcceptedBuild+  unless (sizeofPrimArray bulkMapping > 4) $ fail "buildInputVertices out of bounds"+  assertEqual "stable duplicate reroute" (VertexId (indexPrimArray bulkMapping 0)) (VertexId (indexPrimArray bulkMapping 4))+  assertEqual "conflict reporting" 1 (V.length (cdtRejectedConstraints bulk))+  assertCdtValid "stable CDT bulk load" (buildTriangulation bulkAcceptedBuild)++testAnnotatedConstrainedUnion :: IO ()+testAnnotatedConstrainedUnion = do+  let leftPoints :: V.Vector (Point)+      leftPoints = V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2]+      rightPoints :: V.Vector (Point)+      rightPoints = V.fromList [Point 2 0, Point 4 0, Point 4 2, Point 2 2]+      thirdPoints :: V.Vector (Point)+      thirdPoints = V.fromList [Point 4 0, Point 6 0, Point 6 2, Point 4 2]+  leftBuild <-+    requireRight "annotated constrained union left" $+      constrainedDelaunay+        unitElementDefaults+        leftPoints+        (V.singleton (0, 2))+  rightBuild <-+    requireRight "annotated constrained union right" $+      constrainedDelaunay+        unitElementDefaults+        rightPoints+        (V.singleton (0, 2))+  thirdBuild <-+    requireRight "annotated constrained union third" $+      constrainedDelaunay+        unitElementDefaults+        thirdPoints+        (V.singleton (0, 2))+  let left =+        mapVertices+          (const (Set.singleton "left"))+          (buildTriangulation leftBuild)+      right =+        mapVertices+          (const (Set.singleton "right"))+          (buildTriangulation rightBuild)+      third =+        mapVertices+          (const (Set.singleton "third"))+          (buildTriangulation thirdBuild)+  joined <-+    requireRight+      "annotated constrained union"+      (unionConstrainedWith Set.union left right)+  let expectedAnnotations =+        Map.fromList+          [ (Point 0 0, Set.singleton "left")+          , (Point 0 2, Set.singleton "left")+          , (Point 2 0, Set.fromList ["left", "right"])+          , (Point 2 2, Set.fromList ["left", "right"])+          , (Point 4 0, Set.singleton "right")+          , (Point 4 2, Set.singleton "right")+          ]+      actualAnnotations =+        Map.fromList+          [ (vertexPoint joined vertex, vertexData joined vertex)+          | vertex <- vertices joined+          ]+      expectedConstraints =+        Set.union+          (Set.fromList (V.toList (constraintSegments left)))+          (Set.fromList (V.toList (constraintSegments right)))+  assertEqual+    "annotated constrained union preserves and combines site payloads"+    expectedAnnotations+    actualAnnotations+  assertEqual+    "annotated constrained union preserves both constraint sections"+    expectedConstraints+    (Set.fromList (V.toList (constraintSegments joined)))+  assertCdtValid "annotated constrained union" joined+  commuted <-+    requireRight+      "annotated constrained union commuted"+      (unionConstrainedWith Set.union right left)+  assertEqual+    "annotated constrained union is commutative under a commutative payload combiner"+    joined+    commuted+  leftAssociated <-+    requireRight+      "annotated constrained union left-associated"+      (unionConstrainedWith Set.union joined third)+  rightPair <-+    requireRight+      "annotated constrained union right pair"+      (unionConstrainedWith Set.union right third)+  rightAssociated <-+    requireRight+      "annotated constrained union right-associated"+      (unionConstrainedWith Set.union left rightPair)+  assertEqual+    "annotated constrained union is associative wherever both descents are admitted"+    leftAssociated+    rightAssociated+  unitJoined <-+    requireRight+      "unit constrained union specialization"+      ( unionConstrained+          (mapVertices (const ()) left)+          (mapVertices (const ()) right)+      )+  assertEqual+    "unit constrained union specializes annotated constrained union"+    unitJoined+    (mapVertices (const ()) joined)++-- | Sequential extension retains the base authority as the transaction root.+-- The semantic result agrees with canonical union on sites and constraint+-- sections, but its receipt proves that only the incoming constraint section+-- was interpreted.+testAsymmetricConstrainedExtension :: IO ()+testAsymmetricConstrainedExtension = do+  let basePoints :: V.Vector (Point)+      basePoints = V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2]+      extensionPoints :: V.Vector (Point)+      extensionPoints = V.fromList [Point 2 0, Point 4 0, Point 4 2, Point 2 2]+  baseBuild <-+    requireRight+      "asymmetric extension base"+      (constrainedDelaunay unitElementDefaults basePoints (V.singleton (0, 2)))+  extensionBuild <-+    requireRight+      "asymmetric extension incoming"+      (constrainedDelaunay unitElementDefaults extensionPoints (V.singleton (0, 2)))+  let base = mapVertices (const (Set.singleton "base")) (buildTriangulation baseBuild)+      extension = mapVertices (const (Set.singleton "extension")) (buildTriangulation extensionBuild)+      expectedAnnotations =+        Map.fromList+          [ (Point 0 0, Set.singleton "base")+          , (Point 0 2, Set.singleton "base")+          , (Point 2 0, Set.fromList ["base", "extension"])+          , (Point 2 2, Set.fromList ["base", "extension"])+          , (Point 4 0, Set.singleton "extension")+          , (Point 4 2, Set.singleton "extension")+          ]+      expectedConstraints =+        Set.union+          (Set.fromList (V.toList (constraintSegments base)))+          (Set.fromList (V.toList (constraintSegments extension)))+      baseAnnotations =+        Map.fromList+          [ (vertexPoint base vertex, vertexData base vertex)+          | vertex <- vertices base+          ]+      extensionAnnotations =+        Map.fromList+          [ (vertexPoint extension vertex, vertexData extension vertex)+          | vertex <- vertices extension+          ]+  baseAnnotationsBefore <- evaluate (force baseAnnotations)+  baseConstraintsBefore <- evaluate (force (constraintSegments base))+  extensionAnnotationsBefore <- evaluate (force extensionAnnotations)+  extensionConstraintsBefore <- evaluate (force (constraintSegments extension))+  extendedResult <-+    requireRight+      "asymmetric constrained extension"+      (extendConstrainedWith Set.union base extension)+  let constraintBatch = constrainedExtensionConstraintBatch extendedResult+      extended = constraintBatchTriangulation constraintBatch+      receipt = constraintBatchStats constraintBatch+      buildReceipt = constrainedExtensionBuildStats extendedResult+      actualAnnotations =+        Map.fromList+          [ (vertexPoint extended vertex, vertexData extended vertex)+          | vertex <- vertices extended+          ]+  assertEqual+    "asymmetric extension preserves base and combines coincident site payloads"+    expectedAnnotations+    actualAnnotations+  assertEqual+    "asymmetric extension preserves resident and incoming constraint sections"+    expectedConstraints+    (Set.fromList (V.toList (constraintSegments extended)))+  assertEqual+    "asymmetric extension does not mutate the frozen base predecessor sites"+    baseAnnotationsBefore+    ( Map.fromList+        [ (vertexPoint base vertex, vertexData base vertex)+        | vertex <- vertices base+        ]+    )+  assertEqual+    "asymmetric extension does not mutate the frozen base predecessor constraints"+    baseConstraintsBefore+    (constraintSegments base)+  assertEqual+    "asymmetric extension does not mutate the frozen incoming predecessor sites"+    extensionAnnotationsBefore+    ( Map.fromList+        [ (vertexPoint extension vertex, vertexData extension vertex)+        | vertex <- vertices extension+        ]+    )+  assertEqual+    "asymmetric extension does not mutate the frozen incoming predecessor constraints"+    extensionConstraintsBefore+    (constraintSegments extension)+  assertEqual+    "asymmetric extension recovers only incoming constraints"+    (V.length (constraintSegments extension))+    (constraintBatchRequests receipt)+  assertEqual+    "asymmetric extension returns one outcome for each incoming constraint"+    (V.length (constraintSegments extension))+    (V.length (constraintBatchOutcomes constraintBatch))+  assertEqual+    "asymmetric extension admits every incoming constraint"+    (V.length (constraintSegments extension))+    (constraintBatchAccepted receipt)+  assertEqual+    "asymmetric extension reports no incoming constraint rejection"+    0+    (constraintBatchRejected receipt)+  assertEqual+    "asymmetric extension charges every incoming site once"+    (V.length extensionPoints)+    (statInputPoints buildReceipt)+  assertEqual+    "asymmetric extension distinguishes occupied incoming sites"+    2+    (statExistingPoints buildReceipt)+  assertEqual+    "asymmetric extension distinguishes newly materialized incoming sites"+    2+    (statUniquePoints buildReceipt)+  assertCdtValid "asymmetric constrained extension" extended++  crossingBuild <-+    requireRight+      "asymmetric extension crossing incoming section"+      (constrainedDelaunay unitElementDefaults basePoints (V.singleton (1, 3)))+  case+      extendConstrainedWith+        Set.union+        base+        (mapVertices (const (Set.singleton "crossing")) (buildTriangulation crossingBuild)) of+    Left (ConstraintUnionConstructionFailed (ConstraintIntersection _)) -> pure ()+    other -> fail ("asymmetric extension did not return the corridor intersection witness: " <> show other)++testLargeAsymmetricConstrainedExtension :: IO ()+testLargeAsymmetricConstrainedExtension = do+  let width = 40 :: Int+      height = 28 :: Int+      basePoints :: V.Vector (Point)+      basePoints =+        V.fromList+          ( [ Point 0 0+            , Point (fromIntegral (width + 1)) 0+            , Point (fromIntegral (width + 1)) (fromIntegral (height + 1))+            , Point 0 (fromIntegral (height + 1))+            ]+              <> [ Point+                     (fromIntegral (column + 1) + fromIntegral ((column * 17 + row * 31) `mod` 13) * 1.0e-3)+                     (fromIntegral (row + 1) + fromIntegral ((column * 23 + row * 19) `mod` 17) * 1.0e-3)+                 | row <- [0 .. height - 1]+                 , column <- [0 .. width - 1]+                 ]+          )+      baseConstraints = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]+      extensionPoints :: V.Vector (Point)+      extensionPoints = V.fromList [Point 48 8, Point 53.2 8.4, Point 50.4 15.8]+      extensionConstraints = V.fromList [(0, 1), (1, 2), (2, 0)]+      conflictPoints :: V.Vector (Point)+      conflictPoints = V.fromList [Point 8 22, Point 30 5, Point 26 7]+  baseBuild <-+    requireRight+      "large asymmetric extension base"+      (constrainedDelaunay unitElementDefaults basePoints baseConstraints)+  extensionBuild <-+    requireRight+      "large asymmetric extension incoming"+      (constrainedDelaunay unitElementDefaults extensionPoints extensionConstraints)+  conflictBuild <-+    requireRight+      "large asymmetric extension conflicting incoming"+      (constrainedDelaunay unitElementDefaults conflictPoints (V.singleton (0, 1)))+  let base = mapVertices (const (Set.singleton "base")) (buildTriangulation baseBuild)+      extension = mapVertices (const (Set.singleton "extension")) (buildTriangulation extensionBuild)+      conflicting = mapVertices (const (Set.singleton "conflict")) (buildTriangulation conflictBuild)+      predecessor+        :: Triangulation 'Constrained (Set.Set String) () () ()+        -> ( Triangulation 'Constrained (Set.Set String) () () ()+           , Map.Map VertexId (Point, Set.Set String)+           , Set.Set (CanonicalSegment)+           )+      predecessor triangulation =+        ( triangulation+        , Map.fromList+            [ (vertex, (vertexPoint triangulation vertex, vertexData triangulation vertex))+            | vertex <- vertices triangulation+            ]+        , Set.fromList (V.toList (constraintSegments triangulation))+        )+      assertPredecessor+        :: String+        -> ( Triangulation 'Constrained (Set.Set String) () () ()+           , Map.Map VertexId (Point, Set.Set String)+           , Set.Set (CanonicalSegment)+           )+        -> Triangulation 'Constrained (Set.Set String) () () ()+        -> IO ()+      assertPredecessor label snapshot triangulation =+        assertEqual label snapshot (predecessor triangulation)+  baseBefore <- evaluate (force (predecessor base))+  extensionBefore <- evaluate (force (predecessor extension))+  conflictingBefore <- evaluate (force (predecessor conflicting))+  let (_, baseVertexSnapshotBefore, baseConstraintSnapshotBefore) = baseBefore+      (_, _, extensionConstraintSnapshotBefore) = extensionBefore+  extensionResult <-+    requireRight+      "large asymmetric constrained extension"+      (extendConstrainedWith Set.union base extension)+  let constraintBatch = constrainedExtensionConstraintBatch extensionResult+      extended = constraintBatchTriangulation constraintBatch+      extendedConstraints = Set.fromList (V.toList (constraintSegments extended))+      extendedVertexSnapshot =+        Map.fromList+          [ (vertex, (vertexPoint extended vertex, vertexData extended vertex))+          | vertex <- vertices extended+          ]+  assertEqual+    "large asymmetric extension retains every base vertex handle, coordinate, and payload"+    baseVertexSnapshotBefore+    (Map.restrictKeys extendedVertexSnapshot (Map.keysSet baseVertexSnapshotBefore))+  assertEqual+    "large asymmetric extension retains its complete base constraint source section"+    baseConstraintSnapshotBefore+    (Set.intersection baseConstraintSnapshotBefore extendedConstraints)+  assertEqual+    "large asymmetric extension retains its complete incoming constraint source section"+    extensionConstraintSnapshotBefore+    (Set.intersection extensionConstraintSnapshotBefore extendedConstraints)+  assertEqual+    "large asymmetric extension preserves exactly both source constraint sections"+    (Set.union baseConstraintSnapshotBefore extensionConstraintSnapshotBefore)+    extendedConstraints+  assertEqual+    "large asymmetric extension leaves the frozen base predecessor physically unchanged"+    baseBefore+    (predecessor base)+  assertPredecessor+    "large asymmetric extension leaves the frozen incoming predecessor physically unchanged"+    extensionBefore+    extension+  assertEqual+    "large asymmetric extension charges only the incoming sites"+    (V.length extensionPoints)+    (statInputPoints (constrainedExtensionBuildStats extensionResult))+  assertEqual+    "large asymmetric extension replays only the incoming constraints"+    (V.length extensionConstraints)+    (constraintBatchRequests (constraintBatchStats constraintBatch))+  assertCdtValid "large asymmetric constrained extension" extended+  case extendConstrainedWith Set.union base conflicting of+    Left (ConstraintUnionConstructionFailed (ConstraintIntersection _)) -> pure ()+    outcome ->+      fail+        ( "large asymmetric extension did not refuse the crossing incoming corridor: "+            <> show outcome+        )+  assertEqual+    "large asymmetric extension refusal leaves the frozen base predecessor physically unchanged"+    baseBefore+    (predecessor base)+  assertPredecessor+    "large asymmetric extension refusal leaves the conflicting predecessor physically unchanged"+    conflictingBefore+    conflicting++-- | A separated constrained seam is a restriction-preserving operation, not+-- the canonical site-set union wearing a cheaper costume. Both closed source+-- sections survive face-for-face; only the corridor contributes new faces.+testSeparatedConstrainedSeam :: IO ()+testSeparatedConstrainedSeam = do+  let leftPoints :: V.Vector (Point)+      leftPoints =+        V.fromList+          [ Point (-4) (-1)+          , Point (-2) (-1)+          , Point (-2) 1+          , Point (-4) 1+          ]+      rightPoints :: V.Vector (Point)+      rightPoints =+        V.fromList+          [ Point 2 (-1)+          , Point 4 (-1)+          , Point 4 1+          , Point 2 1+          ]+      closedContour = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]+  leftBuild <-+    requireRight+      "separated constrained seam left"+      (constrainedDelaunay unitElementDefaults leftPoints closedContour)+  rightBuild <-+    requireRight+      "separated constrained seam right"+      (constrainedDelaunay unitElementDefaults rightPoints closedContour)+  let left =+        Internal.geometryOnlyPublication+          (buildTriangulation leftBuild)+      right =+        Internal.geometryOnlyPublication+          (buildTriangulation rightBuild)+      leftFaceKeys = Set.fromList (fmap (faceKeyOf left) (innerFaces left))+      rightFaceKeys = Set.fromList (fmap (faceKeyOf right) (innerFaces right))+      expectedConstraints =+        Set.union+          (Set.fromList (V.toList (constraintSegments left)))+          (Set.fromList (V.toList (constraintSegments right)))+  seam <-+    requireRight+      "source-preserving separated constrained seam"+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters left right)+  let joined = constrainedSeamResultTriangulation seam+      joinedFaceKeys = Set.fromList (fmap (faceKeyOf joined) (innerFaces joined))+      actualAnnotations =+        Map.fromList+          [ (vertexPoint joined vertex, vertexData joined vertex)+          | vertex <- vertices joined+          ]+      expectedAnnotations =+        Map.union+          ( Map.fromList+              [ (vertexPoint left vertex, vertexData left vertex)+              | vertex <- vertices left+              ]+          )+          ( Map.fromList+              [ (vertexPoint right vertex, vertexData right vertex)+              | vertex <- vertices right+              ]+          )+      actualConstraints = Set.fromList (V.toList (constraintSegments joined))+      certifiedPerimeter =+        fmap+          (\segmentValue -> (segmentStart segmentValue, segmentEnd segmentValue))+          (Set.toAscList (Set.difference actualConstraints expectedConstraints))+  assertCdtValid "source-preserving separated constrained seam" joined+  assertEqual+    "separated seam preserves all source annotations"+    expectedAnnotations+    actualAnnotations+  assertEqual+    "separated seam preserves both source constraint sections"+    True+    (expectedConstraints `Set.isSubsetOf` actualConstraints)+  assertEqual+    "separated seam certifies exactly its two synthetic perimeter bridges"+    [ (Point (-2) (-1), Point 2 (-1))+    , (Point (-2) 1, Point 2 1)+    ]+    certifiedPerimeter+  unless (leftFaceKeys `Set.isSubsetOf` joinedFaceKeys) $+    fail "separated seam removed or retriangulated a left source face"+  unless (rightFaceKeys `Set.isSubsetOf` joinedFaceKeys) $+    fail "separated seam removed or retriangulated a right source face"+  assertEqual+    "separated seam records the stable left face witness"+    (numInnerFaces left)+    (constrainedSeamLeftFaceCount seam)+  assertEqual+    "separated seam records the stable left constraint witness"+    (numConstraints left)+    (constrainedSeamLeftConstraintCount seam)+  assertEqual+    "separated seam has one right face witness per source face"+    (numInnerFaces right)+    (V.length (constrainedSeamRightFaceEvidence seam))+  forM_ (V.toList (constrainedSeamRightFaceEvidence seam)) $ \evidence ->+    assertEqual+      "right face witness names the exact target triangle"+      (faceKeyOf right (constrainedSeamSourceFace evidence))+      (faceKeyOf joined (constrainedSeamTargetFace evidence))+  when (V.null (constrainedSeamJoinFaces seam)) $+    fail "separated seam did not identify any new corridor face"+  assertEqual+    "copied source constraints require no corridor recovery"+    0+    (constraintBatchRequests (constrainedSeamConstraintStats seam))+  unless+    ( all+        ((== Nothing) . constrainedSeamConstraintRecovery)+        (V.toList (constrainedSeamRightConstraintEvidence seam))+    ) $+    fail "separated seam recovered a constraint already represented by its copied source"+  let seamPublication = constrainedSeamPublicationStats seam+  assertEqual+    "separated seam does not enumerate resident unboxed base pages"+    0+    (publicationUnboxedBasePageEnumerations seamPublication)+  assertEqual+    "separated seam does not freeze resident unboxed base pages"+    0+    (publicationUnboxedBasePageFreezes seamPublication)+  assertEqual+    "separated seam does not enumerate resident boxed base pages"+    0+    (publicationBoxedBasePageEnumerations seamPublication)+  unless+    ( publicationUnboxedDirtyBasePages seamPublication+        + publicationUnboxedDirtyAppendedPages seamPublication+        > 0+    ) $+    fail "separated seam publication did not record appended unboxed writes"+  unless (constrainedSeamCachedFrontierPointReads seam > 0) $+    fail "separated seam did not record cached frontier point reads"++  let boundedBridgeParameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 6+          , refineMaxEdgeLength = Just 1+          }+  boundedSeam <-+    requireRight+      "edge-bounded separated constrained seam"+      ( joinSeparatedConstrained+          (\_ _ -> True)+          boundedBridgeParameters+          left+          right+      )+  let boundedJoined = constrainedSeamResultTriangulation boundedSeam+      boundedSyntheticSegments =+        Set.difference+          (Set.fromList (V.toList (constraintSegments boundedJoined)))+          expectedConstraints+  assertCdtValid "edge-bounded separated constrained seam" boundedJoined+  assertEqual+    "edge-bounded seam charges its exact bridge Steiner budget"+    6+    (numVertices boundedJoined - numVertices left - numVertices right)+  assertEqual+    "edge-bounded seam publishes both four-edge bridge paths"+    8+    (Set.size boundedSyntheticSegments)+  unless (all ((<= 1) . canonicalSegmentSquaredLength) boundedSyntheticSegments) $+    fail "edge-bounded seam published an oversized synthetic bridge child"+  assertEqual+    "edge-bounded seam cache agrees with its explicit frontier observation"+    (Internal.prepareSeamFrontierIndex boundedJoined)+    (Internal.triSeamFrontier boundedJoined)+  case+      joinSeparatedConstrained+        (\_ _ -> True)+        boundedBridgeParameters{refineMaxAdditionalVertices = Just 5}+        left+        right of+    Left+      ( ConstraintUnionConstructionFailed+          (CdtBuildError (RefinementSeamBridgeBudgetExceeded 6 5))+        ) -> pure ()+    outcome ->+      fail+        ( "edge-bounded seam did not return its exact budget obstruction: "+            <> either show (const "success") outcome+        )++  reversed <- requireRight "reversed source-preserving separated seam" (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters right left)+  assertCdtValid "reversed source-preserving separated seam" (constrainedSeamResultTriangulation reversed)++  let localParameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 2+          , refineMaxArea = Just 0.5+          , refineKeepConstraintEdges = True+          }+      allJoinedFaces = Set.fromList (innerFaces joined)+  locallyRefined <-+    requireRight+      "seam refinement preserves cached hull frontier"+      (refineWithinDomain (const ()) localParameters allJoinedFaces Set.empty joined)+  let refinedJoined = refinedTriangulation (refinementDomainResult locallyRefined)+  unless (refinementComplete (refinementDomainResult locallyRefined)) $+    fail "seam refinement fixture did not drain its local worklist"+  assertEqual+    "local refinement retains the exact cached hull frontier"+    (Internal.triSeamFrontier joined)+    (Internal.triSeamFrontier refinedJoined)++  thirdBuild <-+    requireRight+      "repeated separated constrained seam source"+      (constrainedDelaunay unitElementDefaults (V.fromList+        [ Point 8 (-1)+        , Point 10 (-1)+        , Point 10 1+        , Point 8 1+        ]) closedContour)+  let third =+        Internal.geometryOnlyPublication+          (buildTriangulation thirdBuild)+  repeated <-+    requireRight+      "repeated separated seam after local refinement"+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters refinedJoined third)+  assertCdtValid+    "repeated separated seam after local refinement"+    (constrainedSeamResultTriangulation repeated)+  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters left left of+    Left ConstraintUnionNotSeparated -> pure ()+    other -> fail ("non-separated constrained seam was not refused: " <> show other)++-- North/south additions use the orientation-preserving Y chart rather than+-- rotating or copying the resident mesh.  The sequence then alternates axes,+-- so a stale one-axis frontier cache cannot accidentally make the third join+-- succeed.+testAxisSeparatedConstrainedSeams :: IO ()+testAxisSeparatedConstrainedSeams = do+  let closedContour :: Int -> V.Vector (Int, Int)+      closedContour pointCount =+        V.fromList+          [ (index, (index + 1) `mod` pointCount)+          | index <- [0 .. pointCount - 1]+          ]+      buildPublished+        :: String+        -> V.Vector Point+        -> IO (Triangulation 'Constrained () () () ())+      buildPublished label points = do+        built <-+          requireRight+            label+            (constrainedDelaunay+               unitElementDefaults+               points+               (closedContour (V.length points)))+        pure (Internal.geometryOnlyPublication (buildTriangulation built))+      centralPoints =+        V.fromList+          [ Point (-1) (-1)+          , Point 1 (-1)+          , Point 1 1+          , Point (-1) 1+          ]+      northPoints =+        V.fromList+          [ Point (-1) 3+          , Point 1 3+          , Point 1 5+          , Point (-1) 5+          ]+      southPoints =+        V.fromList+          [ Point (-1) (-5)+          , Point 1 (-5)+          , Point 1 (-3)+          , Point (-1) (-3)+          ]+      eastPoints =+        V.fromList+          [ Point 3 (-1)+          , Point 5 (-1)+          , Point 5 1+          , Point 3 1+          ]+      overlapPoints =+        V.fromList+          [ Point 0 0+          , Point 2 0+          , Point 2 2+          , Point 0 2+          ]+      irregularNorthPoints =+        V.fromList+          [ Point (-2) 7+          , Point 0 7+          , Point 2 7+          , Point 2.5 9+          , Point 0 11+          , Point (-2.5) 9+          ]+  central <- buildPublished "axis seam central source" centralPoints+  north <- buildPublished "axis seam north source" northPoints+  south <- buildPublished "axis seam south source" southPoints+  east <- buildPublished "axis seam east source" eastPoints+  overlap <- buildPublished "axis seam overlapping source" overlapPoints++  let northParameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 2+          , refineMaxEdgeLength = Just 1+          }+  northJoined <-+    requireRight+      "edge-bounded north source-preserving separated seam"+      (joinSeparatedConstrained (\_ _ -> True) northParameters central north)+  let northTopology = constrainedSeamResultTriangulation northJoined+      northPublication = constrainedSeamPublicationStats northJoined+      northSyntheticSegments =+        Set.difference+          (Set.fromList (V.toList (constraintSegments northTopology)))+          ( Set.union+              (Set.fromList (V.toList (constraintSegments central)))+              (Set.fromList (V.toList (constraintSegments north)))+          )+  assertCdtValid+    "north source-preserving separated seam"+    northTopology+  assertEqual+    "north seam charges two bridge Steiner vertices"+    2+    (numVertices northTopology - numVertices central - numVertices north)+  assertEqual+    "north seam certifies two bounded two-edge bridge paths"+    4+    (Set.size northSyntheticSegments)+  unless (all ((<= 1) . canonicalSegmentSquaredLength) northSyntheticSegments) $+    fail "north seam published an oversized synthetic bridge child"+  assertEqual+    "north seam cache agrees with an explicit frontier observation"+    (Internal.prepareSeamFrontierIndex northTopology)+    (Internal.triSeamFrontier northTopology)+  assertEqual+    "north seam does not enumerate resident unboxed pages"+    0+    (publicationUnboxedBasePageEnumerations northPublication)+  assertEqual+    "north seam does not freeze resident unboxed pages"+    0+    (publicationUnboxedBasePageFreezes northPublication)+  assertEqual+    "north seam does not enumerate resident boxed pages"+    0+    (publicationBoxedBasePageEnumerations northPublication)+  unless+    ( publicationUnboxedDirtyBasePages northPublication+        + publicationUnboxedDirtyAppendedPages northPublication+        > 0+    ) $+    fail "north seam did not record appended unboxed writes"++  reversed <-+    requireRight+      "reversed south/north source-preserving separated seam"+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters south central)+  assertCdtValid+    "reversed south/north source-preserving separated seam"+    (constrainedSeamResultTriangulation reversed)++  mixed <-+    requireRight+      "mixed-axis east extension after north seam"+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters+         (constrainedSeamResultTriangulation northJoined)+         east)+  assertCdtValid+    "mixed-axis east extension after north seam"+    (constrainedSeamResultTriangulation mixed)+  assertEqual+    "mixed-axis seam retains prior perimeter and certifies two new bridges"+    ( numConstraints (constrainedSeamResultTriangulation northJoined)+        + numConstraints east+        + 2+    )+    (numConstraints (constrainedSeamResultTriangulation mixed))++  repeated <-+    requireRight+      "repeated mixed-axis south extension"+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters+         (constrainedSeamResultTriangulation mixed)+         south)+  assertCdtValid+    "repeated mixed-axis south extension"+    (constrainedSeamResultTriangulation repeated)+  assertEqual+    "repeated mixed-axis seam cache agrees with an explicit frontier observation"+    (Internal.prepareSeamFrontierIndex (constrainedSeamResultTriangulation repeated))+    (Internal.triSeamFrontier (constrainedSeamResultTriangulation repeated))++  irregularNorth <-+    buildPublished+      "axis seam irregular north source"+      irregularNorthPoints+  irregularJoined <-+    requireRight+      "irregular collinear north extension"+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters+         (constrainedSeamResultTriangulation repeated)+         irregularNorth)+  assertCdtValid+    "irregular collinear north extension"+    (constrainedSeamResultTriangulation irregularJoined)+  assertEqual+    "irregular collinear north cache agrees with an explicit frontier observation"+    (Internal.prepareSeamFrontierIndex (constrainedSeamResultTriangulation irregularJoined))+    (Internal.triSeamFrontier (constrainedSeamResultTriangulation irregularJoined))++  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters central overlap of+    Left ConstraintUnionNotSeparated -> pure ()+    outcome ->+      fail+        ( "overlapping sources were not refused by either-axis admission: "+            <> either show (const "success") outcome+        )++-- A separated source frontier may become an interior diagonal when the seam+-- closes. If that diagonal is unconstrained and locally illegal, preserving+-- the source and publishing a Delaunay join are incompatible obligations. The+-- constrained twin is lawful: its contour explicitly authorizes that fixed+-- diagonal to remain.+testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite :: IO ()+testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite = do+  let leftPoints =+        V.fromList+          [ Point (-2) 0+          , Point 0 0+          , Point (-1) 10+          , Point (-3) 5+          ]+      rightPoints =+        V.fromList+          [ Point 1 5+          , Point 2 4+          , Point 3 5+          , Point 2 6+          ]+      closedQuadrilateral = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]+      buildSource+        :: String+        -> V.Vector (Int, Int)+        -> V.Vector Point+        -> IO (BuildResult 'Constrained Point () () ())+      buildSource label constraints points =+        requireRight label (constrainedDelaunay unitElementDefaults points constraints)+      publish+        :: BuildResult 'Constrained Point () () ()+        -> Triangulation 'Constrained () () () ()+      publish = Internal.geometryOnlyPublication . buildTriangulation+  unconstrainedLeft <- publish <$> buildSource "incompatible seam left" V.empty leftPoints+  unconstrainedRight <- publish <$> buildSource "incompatible seam right" V.empty rightPoints+  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters unconstrainedLeft unconstrainedRight of+    Left+      ( ConstraintUnionConstructionFailed+          (CdtBuildError (SeamSourceEdgeRequiresFlip _))+        ) -> pure ()+    outcome ->+      fail+        ( "separated seam did not refuse an unconstrained pinned source rewrite: "+            <> either show (const "success") outcome+        )+  selectiveJoin <-+    requireRight+      "selective seam rewrites only unselected exterior faces"+      (joinSeparatedConstrained (\_ _ -> False) defaultRefinementParameters unconstrainedLeft unconstrainedRight)+  let selectivelyJoined = constrainedSeamResultTriangulation selectiveJoin+      sourceFaceKeys =+        Set.union+          (Set.fromList (fmap (faceKeyOf unconstrainedLeft) (innerFaces unconstrainedLeft)))+          (Set.fromList (fmap (faceKeyOf unconstrainedRight) (innerFaces unconstrainedRight)))+      selectiveFaceKeys =+        Set.fromList (fmap (faceKeyOf selectivelyJoined) (innerFaces selectivelyJoined))+  assertCdtValid "selective unconstrained seam" selectivelyJoined+  when (sourceFaceKeys `Set.isSubsetOf` selectiveFaceKeys) $+    fail "selective seam did not rewrite the incompatible exterior source face"+  when (V.null (constrainedSeamJoinFaces selectiveJoin)) $+    fail "selective seam did not publish the exact rewritten J component"+  unless (V.null (constrainedSeamRightFaceEvidence selectiveJoin)) $+    fail "selective seam fabricated transport evidence for unselected incoming faces"+  constrainedLeft <-+    publish+      <$> buildSource+        "compatible constrained seam left"+        closedQuadrilateral+        leftPoints+  constrainedRight <-+    publish+      <$> buildSource+        "compatible constrained seam right"+        closedQuadrilateral+        rightPoints+  let protectedLeftFaces = Set.fromList (boundedRegionFaces constrainedLeft)+      protectedRightFaces = Set.fromList (boundedRegionFaces constrainedRight)+      preserveConstrainedSource side faceHandle =+        case side of+          SeamResident -> Set.member faceHandle protectedLeftFaces+          SeamIncoming -> Set.member faceHandle protectedRightFaces+      protectedFaceKeys =+        Set.union+          (Set.fromList (fmap (faceKeyOf constrainedLeft) (Set.toList protectedLeftFaces)))+          (Set.fromList (fmap (faceKeyOf constrainedRight) (Set.toList protectedRightFaces)))+  constrainedJoin <-+    requireRight+      "compatible selected constrained seam"+      (joinSeparatedConstrained preserveConstrainedSource defaultRefinementParameters constrainedLeft constrainedRight)+  let constrainedJoined = constrainedSeamResultTriangulation constrainedJoin+      joinedFaceKeys =+        Set.fromList (fmap (faceKeyOf constrainedJoined) (innerFaces constrainedJoined))+      evidencedIncomingFaces =+        Set.fromList+          ( fmap+              constrainedSeamSourceFace+              (V.toList (constrainedSeamRightFaceEvidence constrainedJoin))+          )+  assertCdtValid+    "compatible selected constrained seam"+    constrainedJoined+  unless (protectedFaceKeys `Set.isSubsetOf` joinedFaceKeys) $+    fail "selected constrained source face did not survive exactly"+  assertEqual+    "selected seam evidences exactly the protected incoming faces"+    protectedRightFaces+    evidencedIncomingFaces++-- An obtuse unconstrained source has a hull edge inside the opposite vertex's+-- diametral circle.  That edge is queued by the refinement seed even though+-- it is not a source constraint.  The checked local interpreter must refuse+-- the encroachment split at its authoritative resolver, retain the cached+-- frontier, and remain joinable afterwards.+testLocalRefinementRejectsEncroachedHullSplit :: IO ()+testLocalRefinementRejectsEncroachedHullSplit = do+  built <-+    requireRight+      "encroached hull source"+      ( constrainedDelaunay+          unitElementDefaults+          (V.fromList [Point 0 0, Point 2 0, Point 0.1 0.1])+          V.empty+      )+  let source =+        Internal.geometryOnlyPublication+          (buildTriangulation built)+      permitted = Set.fromList (innerFaces source)+      parameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 1+          , refineMaxArea = Just 0.001+          , refineKeepConstraintEdges = True+          }+      hullEdges =+        [ edge+        | edge <- undirectedEdges source+        , let (forward, backward) = directedPair edge+        , incidentFace source forward == outerFace || incidentFace source backward == outerFace+        ]+      encroachedHull edge =+        let (fromVertex, toVertex) = Dcel.undirectedEndpoints source edge+            fromPoint = vertexPoint source fromVertex+            toPoint = vertexPoint source toVertex+            oppositeFaceEdge =+              case directedPair edge of+                (forward, backward)+                  | incidentFace source forward == outerFace -> backward+                  | otherwise -> forward+            oppositePoint =+              let innerFace = incidentFace source oppositeFaceEdge+               in if innerFace == outerFace+                    then Nothing+                    else+                      case Dcel.faceVertices source innerFace of+                        verticesInFace ->+                          case filter (/= fromVertex) (filter (/= toVertex) verticesInFace) of+                            [vertex] -> Just (vertexPoint source vertex)+                            _ -> Nothing+         in maybe False (inDiametralCircle fromPoint toPoint) oppositePoint+  unless (any encroachedHull hullEdges) $+    fail "encroached hull fixture did not produce an encroached hull pair"+  refined <-+    requireRight+      "encroached hull local refinement"+      (refineWithinDomain (const ()) parameters permitted Set.empty source)+  let refinementResult = refinementDomainResult refined+      result = refinedTriangulation refinementResult+  unless (refinementComplete refinementResult) $+    fail "encroached hull local refinement exhausted without a fixpoint"+  assertEqual "encroached hull split count" (numVertices source) (numVertices result)+  assertEqual+    "encroached hull refinement retains cached frontier"+    (Internal.triSeamFrontier source)+    (Internal.triSeamFrontier result)++  secondBuild <-+    requireRight+      "encroached hull join source"+      ( constrainedDelaunay+          unitElementDefaults+          (V.fromList [Point 4 0, Point 6 0, Point 6 1, Point 4 1])+          V.empty+      )+  let second =+        Internal.geometryOnlyPublication+          (buildTriangulation secondBuild)+  joined <-+    requireRight+      "encroached hull repeated join"+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters result second)+  assertCdtValid+    "encroached hull repeated join"+    (constrainedSeamResultTriangulation joined)++testConstrainedRefinement :: IO ()+testConstrainedRefinement = do+  cdtBuild <- requireRight "bounded domain" $ constrainedDelaunay+    unitElementDefaults+    (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8, Point 4 2, Point 4 6])+    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])+  let cdt = buildTriangulation cdtBuild+      parameters = defaultRefinementParameters+        { refineMaxAdditionalVertices = Just 80+        , refineMaxArea = Just 3+        , refineMaxRadiusEdgeRatio = Just 1.4+        , refineExcludeOuterFaces = True+        , refineKeepConstraintEdges = False+        }+  refined <- requireRight "constrained refinement" (refine id parameters cdt)+  let result = refinedTriangulation refined+  canonicalResult <- requireRight "canonical constrained refinement" (canonicalize result)+  unless (refinementAddedVertices refined > 0) $ fail "constrained refinement inserted no Steiner points"+  assertCdtValid "constrained refinement" result+  unless (numConstraints result >= numConstraints cdt) $+    fail "constraint splitting lost the constrained boundary"+  assertEqual+    "canonical publication preserves constraint segments"+    (constraintSegments result)+    (constraintSegments canonicalResult)++  -- Refinement maintains the outer-region classification incrementally, from+  -- the touched patch alone. That is a claim about what an insertion cannot+  -- reach, so it is gated against an independent flood over the finished mesh+  -- rather than trusted. The domain here is deliberately narrower than its+  -- convex hull, so the excluded set is non-empty and the two can disagree.+  notchBuild <- requireRight "notched domain" $ constrainedDelaunay+    unitElementDefaults+    (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8, Point 13 4, Point 4 4])+    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])+  let notch = buildTriangulation notchBuild+      notchMaximumEdgeLength = 6.2+      notchParameters = defaultRefinementParameters+        { refineMaxAdditionalVertices = Just 120+        , refineMaxArea = Just 1.5+        , refineMaxEdgeLength = Just notchMaximumEdgeLength+        , refineExcludeOuterFaces = True+        , refineKeepConstraintEdges = False+        }+  notchRefined <- requireRight "notched refinement" (refine id notchParameters notch)+  let notchResult = refinedTriangulation notchRefined+      maintained = sort (V.toList (refinementExcludedFaces notchRefined))+      independent = sort (outerRegionFaces notchResult)+      excludedEdgeLengthsSquared =+        [ squaredDistanceWide+            (vertexPoint notchResult fromVertex)+            (vertexPoint notchResult toVertex)+        | face <- maintained+        , directed <- faceDirectedEdges notchResult face+        , let (fromVertex, toVertex) =+                undirectedEndpoints notchResult (asUndirected directed)+        ]+  unless (refinementAddedVertices notchRefined > 0) $ fail "notched refinement inserted no Steiner points"+  unless (refinementComplete notchRefined) $ fail "notched maximum-edge refinement did not complete"+  when (null independent) $ fail "notched domain produced no outer region: the gate is vacuous"+  unless+    (any (> notchMaximumEdgeLength * notchMaximumEdgeLength) excludedEdgeLengthsSquared)+    (fail "notched fixture has no excluded maximum-edge violation")+  assertEqual "incremental exclusion agrees with an independent flood" independent maintained+  assertCdtValid "notched refinement" notchResult++  annulusBuild <- requireRight "annular domain" $ constrainedDelaunay+    unitElementDefaults+    ( V.fromList+        [ Point 0 0+        , Point 12 0+        , Point 12 12+        , Point 0 12+        , Point 4 4+        , Point 8 4+        , Point 8 8+        , Point 4 8+        ]+    )+    ( V.fromList+        [ (0, 1)+        , (1, 2)+        , (2, 3)+        , (3, 0)+        , (4, 5)+        , (5, 6)+        , (6, 7)+        , (7, 4)+        ]+    )+  let annulus = buildTriangulation annulusBuild+      annulusParameters :: Int -> Maybe Double -> RefinementParameters+      annulusParameters budget maximumArea =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just budget+          , refineMaxArea = maximumArea+          , refineExcludeOuterFaces = True+          , refineKeepConstraintEdges = False+          }+  annulusUnchanged <-+    requireRight+      "budget-zero annular refinement"+      (refine id (annulusParameters 0 Nothing) annulus)+  let initialAnnulusOutside = sort (outerRegionFaces annulus)+      budgetZeroOutside =+        sort (V.toList (refinementExcludedFaces annulusUnchanged))+  assertEqual "annulus has one two-crossing hole" 2 (length initialAnnulusOutside)+  assertEqual+    "budget-zero refinement uses the authoritative annulus classification"+    initialAnnulusOutside+    budgetZeroOutside++  annulusRefined <-+    requireRight+      "positive-budget annular refinement"+      (refine id (annulusParameters 40 (Just 4)) annulus)+  let refinedAnnulus = refinedTriangulation annulusRefined+      maintainedAnnulus =+        sort (V.toList (refinementExcludedFaces annulusRefined))+      independentAnnulus = sort (outerRegionFaces refinedAnnulus)+  unless (refinementAddedVertices annulusRefined > 0) $+    fail "annular refinement inserted no Steiner points"+  assertEqual+    "incremental annulus exclusion agrees with authoritative barrier depth"+    independentAnnulus+    maintainedAnnulus+  assertCdtValid "annular constrained refinement" refinedAnnulus++  -- A tiny constraint can share a mesh with coordinates of vastly different+  -- magnitude. Encroachment is discovered by a local cavity walk, so there is+  -- no broad phase for the range to overflow; the pin is that the scaled+  -- circumcenter and diametral predicates still produce exactly one vertex.+  wideGridBuild <- requireRight "wide-grid constrained domain" $ constrainedDelaunay+    unitElementDefaults+    ( V.fromList+        [ Point 0 0+        , Point 2.0e-43 0+        , Point 1.0e60 0+        , Point 0 1.0e60+        ] :: V.Vector (Point)+    )+    (V.singleton (0, 1))+  wideGridRefined <-+    requireRight+      "wide-grid constrained refinement"+      ( refine+          id+          defaultRefinementParameters+            { refineMaxAdditionalVertices = Just 1+            , refineMaxArea = Just 1.0e119+            , refineKeepConstraintEdges = False+            }+          (buildTriangulation wideGridBuild)+      )+  assertEqual "wide-grid refinement count" 1 (refinementAddedVertices wideGridRefined)+  assertCdtValid "wide-grid constrained refinement" (refinedTriangulation wideGridRefined)++-- | A local domain is a closed section, not a hopeful initial queue. Its+-- interface is exact, its protected faces survive point-for-point, and the+-- receipt contains no visit to the protected side.+testCheckedLocalRefinement :: IO ()+testCheckedLocalRefinement = do+  built <-+    requireRight+      "checked local refinement source"+      ( constrainedDelaunay+          unitElementDefaults+          ( V.fromList+              [ Point 0 0+              , Point 4.1 0+              , Point 8 0.2+              , Point 0.1 4+              , Point 4 4.2+              , Point 8.1 4+              ]+          )+          (V.singleton (2, 5))+      )+  let source = buildTriangulation built+      permitted =+        Set.fromList+          [ face+          | face <- innerFaces source+          , faceCentroidX source face < 4.05+          ]+      interface =+        Set.fromList+          [ edge+          | edge <- undirectedEdges source+          , let (forward, backward) = directedPair edge+                forwardFace = incidentFace source forward+                backwardFace = incidentFace source backward+          , forwardFace /= outerFace+          , backwardFace /= outerFace+          , Set.member forwardFace permitted /= Set.member backwardFace permitted+          ]+      protected = filter (`Set.notMember` permitted) (innerFaces source)+      protectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint source) (faceVertices source face)))) protected+      calmParameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 3+          , refineMaxRadiusEdgeRatio = Nothing+          , refineKeepConstraintEdges = True+          }+      crossingParameters = calmParameters{refineMaxArea = Just 1}+  unless (not (Set.null permitted) && not (null protected) && not (Set.null interface)) $+    fail "checked local refinement fixture did not form a nontrivial cover"+  case refineWithinDomain id calmParameters{refineMaxAdditionalVertices = Nothing} permitted interface source of+    Left RefinementDomainRequiresFiniteVertexBudget -> pure ()+    outcome -> fail ("checked local refinement accepted an unbounded local budget: " <> either show (const "success") outcome)+  case Set.minView interface of+    Nothing -> fail "checked local refinement fixture has no interface"+    Just (_, incomplete) ->+      case refineWithinDomain id calmParameters permitted incomplete source of+        Left (RefinementDomainInterfaceMissing _) -> pure ()+        Left obstruction -> fail ("checked local refinement returned the wrong incomplete-interface obstruction: " <> show obstruction)+        Right _ -> fail "checked local refinement accepted an incomplete interface"+  refined <-+    requireRight+      "checked local refinement"+      (refineWithinDomain id calmParameters permitted interface source)+  let localResult = refinementDomainResult refined+      target = refinedTriangulation localResult+      targetProtectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint target) (faceVertices target face)))) protected+      receipt = refinementDomainReceipt refined+  assertEqual "checked local protected face restriction" protectedSignatures targetProtectedSignatures+  assertEqual+    "checked local final permitted lineage"+    permitted+    (Set.fromList (V.toList (refinementFinalPermittedFaces receipt)))+  assertEqual "checked local protected visit receipt" V.empty (refinementVisitedProtectedFaces receipt)+  assertEqual "checked local boundary crossing receipt" 0 (refinementAttemptedBoundaryCrossings receipt)+  let closureStats = refinementValidationClosureStats receipt+  unless+    ( validationClosureFaces closureStats > 0+        && validationClosureDirectedEdges closureStats > 0+        && validationClosureVertices closureStats > 0+    ) $+    fail "checked local refinement did not record its selected validation closure"+  assertEqual+    "checked local refinement does not enumerate resident unboxed base pages"+    0+    (publicationUnboxedBasePageEnumerations (refinementPublicationStats receipt))+  assertEqual+    "checked local protected constraint restriction"+    (constraintSegments source)+    (constraintSegments target)+  assertValid "checked local refinement" target+  case refineWithinDomain id crossingParameters permitted interface source of+    Left (RefinementDomainWouldCrossInterface _ _) -> pure ()+    Left obstruction -> fail ("checked local refinement returned the wrong crossing obstruction: " <> show obstruction)+    Right _ -> fail "checked local refinement silently crossed its immutable interface"+  case refineWithinDomain id calmParameters{refinePreserveConvexHull = False} permitted interface source of+    Left RefinementDomainRequiresConvexHullPreservation -> pure ()+    outcome -> fail ("checked local refinement accepted hull mutation: " <> either show (const "success") outcome)+  case refineWithinDomain id calmParameters{refineKeepConstraintEdges = False} permitted interface source of+    Left RefinementDomainRequiresConstraintPreservation -> pure ()+    outcome -> fail ("checked local refinement accepted constraint mutation: " <> either show (const "success") outcome)+  case refineWithinDomain id calmParameters{refineExcludeOuterFaces = True} permitted interface source of+    Left RefinementDomainForbidsOuterFaceExclusion -> pure ()+    outcome -> fail ("checked local refinement accepted outer-face exclusion: " <> either show (const "success") outcome)+  wholeRefined <-+    requireRight+      "checked whole-section refinement"+      ( refineWithinDomain+          id+          crossingParameters+          (Set.fromList (innerFaces source))+          Set.empty+          source+      )+  let wholeResult = refinementDomainResult wholeRefined+      wholeReceipt = refinementDomainReceipt wholeRefined+  unless (refinementAddedVertices wholeResult > 0) $+    fail "checked whole-section refinement did not improve its admitted section"+  unless (not (V.null (refinementCreatedFaces wholeReceipt))) $+    fail "checked whole-section refinement omitted semantically rewritten face slots"+  unless+    ( V.any+        (\(FaceId raw) -> toInteger raw < toInteger (numFaces source))+        (refinementCreatedFaces wholeReceipt)+    ) $+    fail "checked whole-section refinement omitted recycled face slots"+  assertEqual+    "checked whole-section final permitted lineage"+    (Set.fromList (innerFaces (refinedTriangulation wholeResult)))+    (Set.fromList (V.toList (refinementFinalPermittedFaces wholeReceipt)))+  assertValid "checked whole-section refinement" (refinedTriangulation wholeResult)+ where+  faceCentroidX+    :: Triangulation mode vertex directed undirected face+    -> FaceId+    -> Double+  faceCentroidX triangulation face =+    case fmap (vertexPoint triangulation) (faceVertices triangulation face) of+      [] -> 0+      points ->+        sum [x | Point x _ <- points] / fromIntegral (length points)++-- | A maximum edge bound is a local geometric law, not a rendering preference:+-- a long, thin face is split along its longest edge even when its area is below+-- the minimum-area short-circuit. A budget-limited pass remains resumable;+-- once the longest edge is frozen at the local interface, a completed pass+-- returns the exact typed quality obstruction instead of crossing it.+testMaximumEdgeLengthQuality :: IO ()+testMaximumEdgeLengthQuality = do+  built <-+    requireRight+      "maximum-edge long-thin source"+      ( constrainedDelaunay+          unitElementDefaults+          ( V.fromList+              [ Point 0 0+              , Point 10 0+              , Point 10 1+              , Point 0 1+              ]+          )+          (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])+      )+  let source = buildTriangulation built+      allPermitted = Set.fromList (innerFaces source)+      parameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 1+          , refineMinArea = Just 6+          , refineMaxEdgeLength = Just 10.01+          , refineMaxRadiusEdgeRatio = Nothing+          , refineKeepConstraintEdges = True+          }+  partial <-+    requireRight+      "maximum-edge budget-limited refinement"+      (refineWithinDomain id parameters allPermitted Set.empty source)+  let partialResult = refinementDomainResult partial+      partialTarget = refinedTriangulation partialResult+      partialReceipt = refinementDomainReceipt partial+      resumedPermitted = Set.fromList (V.toList (refinementFinalPermittedFaces partialReceipt))+  unless (refinementAddedVertices partialResult > 0) $+    fail "maximum-edge refinement did not split the long edge before the area floor"+  when (refinementComplete partialResult) $+    fail "maximum-edge budget-limited refinement reported completion"+  assertValid "maximum-edge budget-limited refinement" partialTarget+  resumed <-+    requireRight+      "maximum-edge resumed refinement"+      ( refineWithinDomain+          id+          parameters{refineMaxAdditionalVertices = Just 8}+          resumedPermitted+          Set.empty+          partialTarget+      )+  let resumedResult = refinementDomainResult resumed+      resumedReceipt = refinementDomainReceipt resumed+  unless (refinementComplete resumedResult) $+    fail "maximum-edge resumed refinement did not drain its local worklist"+  assertMaximumEdgeLength+    "maximum-edge resumed refinement"+    10.01+    (refinedTriangulation resumedResult)+    (V.toList (refinementFinalPermittedFaces resumedReceipt))++  case+      refine+        id+        parameters+          { refineMaxAdditionalVertices = Just 8+          , refineMaxEdgeLength = Just 9+          }+        source of+    Left (RefinementOversizedEdge _ edge actual bound)+      | isConstraintEdge source edge && actual > bound -> pure ()+    Left obstruction ->+      fail ("maximum-edge global audit returned the wrong obstruction: " <> show obstruction)+    Right _ -> fail "maximum-edge global refinement accepted an oversized kept constraint"++  case innerFaces source of+    firstFace : _ -> do+      let permitted = Set.singleton firstFace+          interface =+            Set.fromList+              [ edge+              | edge <- undirectedEdges source+              , let (forward, backward) = directedPair edge+                    forwardFace = incidentFace source forward+                    backwardFace = incidentFace source backward+              , forwardFace /= outerFace+              , backwardFace /= outerFace+              , Set.member forwardFace permitted /= Set.member backwardFace permitted+              ]+      let blocked =+            refineWithinDomain+              id+              parameters{refineMaxAdditionalVertices = Just 8}+              permitted+              interface+              source+      case blocked of+        Left (RefinementOversizedEdge face edge actual bound)+          | Set.member face permitted+              && Set.member edge interface+              && actual > bound -> pure ()+        Left obstruction ->+          fail ("maximum-edge interface returned the wrong obstruction: " <> show obstruction)+        Right _ -> fail "maximum-edge interface was silently crossed or accepted"+    [] -> fail "maximum-edge source has no inner face"++-- | A circumcenter can be inside the admitted face while its legalization+-- cavity also meets the immutable interface. That is a true crossing demand,+-- not the boundary-limited locator read discharged by the local interpreter.+testLocalRefinementRejectsTrueInterfaceCrossing :: IO ()+testLocalRefinementRejectsTrueInterfaceCrossing = do+  built <-+    requireRight+      "deterministic true interface crossing source"+      ( constrainedDelaunay+          unitElementDefaults+          ( V.fromList+              [ Point 0 0+              , Point 2 0+              , Point 1 (sqrt 3)+              , Point (-1) (sqrt 3)+              ]+          )+          V.empty+      )+  let source = buildTriangulation built+      equilateralFacePoints = sort [Point 0 0, Point 2 0, Point 1 (sqrt 3)]+      permittedFace =+        List.find+          ( \face ->+              sort (fmap (vertexPoint source) (faceVertices source face))+                == equilateralFacePoints+          )+          (innerFaces source)+  assertEqual "deterministic crossing source has two inner faces" 2 (numInnerFaces source)+  case permittedFace of+    Nothing -> fail "deterministic crossing source omitted its equilateral face"+    Just face -> do+      let permitted = Set.singleton face+          interface =+            Set.fromList+              [ edge+              | edge <- undirectedEdges source+              , let (forward, backward) = directedPair edge+                    forwardFace = incidentFace source forward+                    backwardFace = incidentFace source backward+              , forwardFace /= outerFace+              , backwardFace /= outerFace+              , Set.member forwardFace permitted /= Set.member backwardFace permitted+              ]+          parameters =+            defaultRefinementParameters+              { refineMaxAdditionalVertices = Just 1+              , refineMaxArea = Just 0.5+              , refineMaxRadiusEdgeRatio = Nothing+              , refineKeepConstraintEdges = True+              }+      assertEqual+        "deterministic crossing source has one interface edge"+        1+        (Set.size interface)+      case refineWithinDomain id parameters permitted interface source of+        Left (RefinementDomainWouldCrossInterface _ _) -> pure ()+        Left obstruction ->+          fail+            ( "deterministic true interface crossing returned the wrong obstruction: "+                <> show obstruction+            )+        Right _ ->+          fail "deterministic true interface crossing was silently accepted"++testRepeatedBoundaryAdjacentRefinement :: IO ()+testRepeatedBoundaryAdjacentRefinement = do+  built <-+    requireRight+      "repeated boundary-adjacent constrained source"+      ( constrainedDelaunay+          unitElementDefaults+          ( V.fromList+              [ Point 0 0+              , Point 4 0+              , Point 8 0+              , Point 12 0+              , Point 0 4+              , Point 4 4+              , Point 8 4+              , Point 12 4+              ]+          )+          (V.singleton (2, 6))+      )+  let source = buildTriangulation built+      (sourcePermitted, sourceInterface) = leftSection source+      fullParameters =+        defaultRefinementParameters+          { refineMaxAdditionalVertices = Just 20+          , refineMaxArea = Just 1+          , refineMaxRadiusEdgeRatio = Nothing+          , refineKeepConstraintEdges = True+          }+  fullRefinement <-+    requireRight+      "full boundary-adjacent refinement"+      (refineWithinDomain id fullParameters sourcePermitted sourceInterface source)+  let fullResult = refinementDomainResult fullRefinement+      fullTarget = refinedTriangulation fullResult+      fullReceipt = refinementDomainReceipt fullRefinement+  unless (refinementAddedVertices fullResult > 0) $+    fail "full boundary-adjacent refinement did not refine its admitted section"+  unless (refinementInterfaceBoundaryReads fullReceipt > 0) $+    fail "boundary-adjacent refinement did not report its immutable interface read"+  assertEqual+    "boundary-adjacent crossing attempts"+    0+    (refinementAttemptedBoundaryCrossings fullReceipt)+  assertEqual+    "full boundary-adjacent constraint restriction"+    (constraintSegments source)+    (constraintSegments fullTarget)+  assertEqual+    "completed boundary-adjacent refinement is idempotent"+    True+    (refinementComplete fullResult)++  let progressParameters = fullParameters{refineMaxAdditionalVertices = Just 1}+  progressFirstRefinement <-+    requireRight+      "first bounded boundary-adjacent refinement"+      (refineWithinDomain id progressParameters sourcePermitted sourceInterface source)+  let progressFirstResult = refinementDomainResult progressFirstRefinement+      progressFirstTarget = refinedTriangulation progressFirstResult+      progressFirstReceipt = refinementDomainReceipt progressFirstRefinement+      progressPermitted =+        Set.fromList (V.toList (refinementFinalPermittedFaces progressFirstReceipt))+  unless (refinementAddedVertices progressFirstResult > 0) $+    fail "first bounded boundary-adjacent refinement did not refine its admitted section"+  when (refinementComplete progressFirstResult) $+    fail "first bounded boundary-adjacent refinement unexpectedly reached a fixpoint"+  progressSecondRefinement <-+    requireRight+      "repeated boundary-adjacent refinement"+      ( refineWithinDomain+          id+          progressParameters+          progressPermitted+          sourceInterface+          progressFirstTarget+      )+  let progressSecondResult = refinementDomainResult progressSecondRefinement+      progressSecondTarget = refinedTriangulation progressSecondResult+      progressSecondReceipt = refinementDomainReceipt progressSecondRefinement+  unless (refinementAddedVertices progressSecondResult > 0) $+    fail "repeated boundary-adjacent refinement did not retain its dynamic join-face support"+  assertEqual+    "repeated boundary-adjacent protected visits"+    V.empty+    (refinementVisitedProtectedFaces progressSecondReceipt)+  assertEqual+    "repeated boundary-adjacent constraint restriction"+    (constraintSegments progressFirstTarget)+    (constraintSegments progressSecondTarget)+  assertValid "repeated boundary-adjacent refinement" progressSecondTarget+ where+  leftSection+    :: Triangulation mode vertex directed undirected face+    -> (Set.Set FaceId, Set.Set UndirectedEdgeId)+  leftSection triangulation =+    let permitted =+          Set.fromList+            [ face+            | face <- innerFaces triangulation+            , faceCentroidX triangulation face < 8+            ]+        interface =+          Set.fromList+            [ edge+            | edge <- undirectedEdges triangulation+            , let (forward, backward) = directedPair edge+                  forwardFace = incidentFace triangulation forward+                  backwardFace = incidentFace triangulation backward+            , forwardFace /= outerFace+            , backwardFace /= outerFace+            , Set.member forwardFace permitted /= Set.member backwardFace permitted+            ]+     in (permitted, interface)+  faceCentroidX+    :: Triangulation mode vertex directed undirected face+    -> FaceId+    -> Double+  faceCentroidX triangulation face =+    case fmap (vertexPoint triangulation) (faceVertices triangulation face) of+      [] -> 0+      points ->+        sum [x | Point x _ <- points] / fromIntegral (length points)++testFaceComponentsAndBoundaries :: IO ()+testFaceComponentsAndBoundaries = do+  emptyBuild <- requirePointBuild "empty region components" []+  collinearBuild <-+    requirePointBuild+      "collinear region components"+      [Point 0 0, Point 1 0, Point 2 0]+  assertEqual+    "empty mesh has no face components"+    ([] :: [(Bool, FaceComponent)])+    (faceComponents (buildTriangulation emptyBuild) (const True))+  assertEqual+    "collinear mesh has no face components"+    ([] :: [(Bool, FaceComponent)])+    (faceComponents (buildTriangulation collinearBuild) (const True))++  triangle <-+    regionMeshFromPoints+      "single triangle region"+      [Point 0 0, Point 2 0, Point 0 2]+  triangleBoundary <-+    requireComponentBoundary "single triangle" True triangle (const True)+  assertBoundaryShape "single triangle" triangle 3 [] triangleBoundary++  square <-+    regionMeshFromPoints+      "uniform square region"+      [Point 0 0, Point 2 0, Point 2 2, Point 0 2]+  squareComponent <-+    requireLabelledComponent+      "uniform square component"+      True+      (faceComponents square (const True))+  assertEqual+    "component provenance mismatch is typed before DCEL lookup"+    (Left (BoundaryComponentFaceOutOfRange (FaceId 2) 2))+    (componentBoundary triangle squareComponent)+  assertEqual+    "component loop provenance mismatch is typed before DCEL lookup"+    (Left (BoundaryComponentFaceOutOfRange (FaceId 2) 2))+    (componentBoundaryLoops triangle squareComponent)+  squareBoundary <- requireRight "uniform square boundary" (componentBoundary square squareComponent)+  squareLoops <-+    requireRight+      "uniform square oriented boundary loops"+      (componentBoundaryLoops square squareComponent)+  repeatedSquareBoundary <-+    requireRight "repeated uniform square boundary" (componentBoundary square squareComponent)+  assertEqual "boundary extraction is deterministic" squareBoundary repeatedSquareBoundary+  assertEqual+    "ordinary oriented loops agree with strict boundary"+    (regionBoundaryOuterLoop squareBoundary :| regionBoundaryHoleLoops squareBoundary)+    squareLoops+  assertEqual+    "uniform square drops its Delaunay diagonal"+    (Set.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])+    (loopPointSet square (regionBoundaryOuterLoop squareBoundary))+  assertBoundaryShape "uniform square" square 4 [] squareBoundary++  let splitComponents =+        faceComponents square (regionFaceSatisfies square (\point -> pointX point < 1))+  assertEqual "split square component count" 2 (length splitComponents)+  traverse_+    (\(_, component) -> do+       boundary <- requireRight "split square boundary" (componentBoundary square component)+       assertBoundaryShape "split square component" square 3 [] boundary)+    splitComponents++  collinearHull <-+    regionMeshFromPoints+      "collinear hull simplification"+      [ Point 0 0+      , Point 1 0+      , Point 2 0+      , Point 2 2+      , Point 0 2+      , Point 1 1+      ]+  collinearBoundary <-+    requireComponentBoundary "collinear hull" True collinearHull (const True)+  assertEqual+    "exact simplification drops a redundant hull site"+    (Set.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])+    (loopPointSet collinearHull (regionBoundaryOuterLoop collinearBoundary))+  assertEqual+    "resident boundary retains every hull edge endpoint"+    (Set.fromList [Point 0 0, Point 1 0, Point 2 0, Point 2 2, Point 0 2])+    (residentLoopPointSet collinearHull (regionBoundaryOuterLoop collinearBoundary))+  assertEqual+    "resident boundary follows exact DCEL edges"+    True+    (residentBoundaryIsEdgeCycle collinearHull (regionBoundaryOuterLoop collinearBoundary))++  lShape <- regionMesh "concave L region" 2 2+  let inL = regionFaceSatisfies lShape (\(Point x y) -> not (x > 1 && y > 1))+  lBoundary <- requireComponentBoundary "concave L" True lShape inL+  assertEqual+    "concave L boundary"+    ( Set.fromList+        [ Point 0 0+        , Point 2 0+        , Point 2 1+        , Point 1 1+        , Point 1 2+        , Point 0 2+        ]+    )+    (loopPointSet lShape (regionBoundaryOuterLoop lBoundary))+  assertBoundaryShape "concave L" lShape 6 [] lBoundary++  annulus <- regionMesh "annulus region" 3 3+  let outsideCenter =+        regionFaceSatisfies annulus (\(Point x y) -> not (x > 1 && x < 2 && y > 1 && y < 2))+  annulusBoundary <-+    requireComponentBoundary "annulus outer" True annulus outsideCenter+  assertBoundaryShape "annulus" annulus 4 [4] annulusBoundary++  twoHoles <- regionMesh "ordered hole regions" 5 3+  let outsideTwoCells =+        regionFaceSatisfies twoHoles $ \(Point x y) ->+          let cell = (floor x :: Int, floor y :: Int)+           in cell /= (1, 1) && cell /= (3, 1)+  twoHoleBoundary <-+    requireComponentBoundary "two-hole outer" True twoHoles outsideTwoCells+  assertBoundaryShape "two-hole" twoHoles 4 [4, 4] twoHoleBoundary++  disconnected <- regionMesh "disconnected equal labels" 3 1+  let outsideMiddle =+        regionFaceSatisfies disconnected (\(Point x _) -> x < 1 || x > 2)+      disconnectedComponents = faceComponents disconnected outsideMiddle+      equalLabelComponents =+        [component | (True, component) <- disconnectedComponents]+  assertEqual "equal labels remain two disconnected components" 2 (length equalLabelComponents)+  assertEqual+    "face component order is deterministic"+    disconnectedComponents+    (faceComponents disconnected outsideMiddle)++  pinched <- regionMesh "pinched region" 3 3+  let pinchedSelection =+        regionFaceSatisfies pinched $ \(Point x y) ->+          let cell = (floor x :: Int, floor y :: Int)+           in cell /= (0, 0) && cell /= (1, 1)+  pinchedComponent <-+    requireLabelledComponent+      "pinched selected component"+      True+      (faceComponents pinched pinchedSelection)+  pinchedLoops <-+    requireRight+      "pinched oriented boundary loops"+      (componentBoundaryLoops pinched pinchedComponent)+  let pinchedLoopList = toList pinchedLoops+  assertEqual "pinched simple loop count" 2 (length pinchedLoopList)+  assertEqual+    "pinched simple loop vertex counts"+    [4, 6]+    (sort (fmap (length . boundaryLoopVertices) pinchedLoopList))+  assertEqual+    "pinched loop orientations"+    [BoundaryCounterClockwise, BoundaryClockwise]+    (sort (fmap boundaryLoopOrientation pinchedLoopList))+  traverse_+    (\loop -> do+       let loopVertexIds = toList (boundaryLoopVertices loop)+       assertEqual+         "pinched loop has no repeated vertex"+         (length loopVertexIds)+         (Set.size (Set.fromList loopVertexIds))+       assertLoopWinding+         "pinched loop winding agrees with orientation"+         (case boundaryLoopOrientation loop of+            BoundaryCounterClockwise -> GT+            BoundaryClockwise -> LT)+         pinched+         loop)+    pinchedLoopList+  case componentBoundary pinched pinchedComponent of+    Left (BoundaryPinch vertex firstEdge secondEdge) -> do+      assertEqual "pinch vertex" (Point 1 1) (vertexPoint pinched vertex)+      when (firstEdge == secondEdge) $+        fail ("pinch repeated one outgoing edge: " <> show firstEdge)+    other -> fail ("pinched boundary produced " <> show other)++testAlphaFaceFiltration :: IO ()+testAlphaFaceFiltration = do+  triangle <-+    regionMeshFromPoints+      "alpha right triangle"+      [Point 0 0, Point 2 0, Point 0 2]+  face <- case innerFaces triangle of+    [singleFace] -> pure singleFace+    faces -> fail ("alpha fixture faces: " <> show faces)+  exactThreshold <- requireRight "exact alpha threshold" (mkRadiusSquared 2)+  lowerThreshold <- requireRight "lower alpha threshold" (mkRadiusSquared 1.999)+  zeroThreshold <- requireRight "zero alpha threshold" (mkRadiusSquared 0)+  assertEqual "zero alpha threshold excludes face" False+    (alphaShapeContainsFace zeroThreshold triangle face)+  assertEqual "closed alpha threshold includes equality" True (alphaShapeContainsFace exactThreshold triangle face)+  assertEqual "lower alpha threshold excludes face" False (alphaShapeContainsFace lowerThreshold triangle face)+  assertEqual "outer face is absent from alpha filtration" False (alphaShapeContainsFace exactThreshold triangle outerFace)++  let roundedPoints =+        ( Point (-20) (-20)+        , Point (-19) (-13)+        , Point (-2) (-19)+        )+      (roundedFirst, roundedSecond, roundedThird) = roundedPoints+      roundedRadiusSquared = 84.5+  roundedTriangle <-+    regionMeshFromPoints+      "rounded alpha equality"+      [roundedFirst, roundedSecond, roundedThird]+  roundedFace <- case innerFaces roundedTriangle of+    [singleFace] -> pure singleFace+    faces -> fail ("rounded alpha fixture faces: " <> show faces)+  roundedThreshold <-+    requireRight+      "rounded exact alpha threshold"+      (mkRadiusSquared roundedRadiusSquared)+  roundedLowerThreshold <-+    requireRight+      "rounded lower alpha threshold"+      (mkRadiusSquared (roundedRadiusSquared - encodeFloat 1 (-46)))+  assertEqual+    "closed alpha equality is exact"+    True+    (alphaShapeContainsFace roundedThreshold roundedTriangle roundedFace)+  assertEqual+    "one binary64 step below exact alpha equality is excluded"+    False+    (alphaShapeContainsFace roundedLowerThreshold roundedTriangle roundedFace)++  alphaBoundary <-+    requireComponentBoundary+      "alpha"+      True+      triangle+      (alphaShapeContainsFace exactThreshold triangle)+  assertBoundaryShape "alpha component" triangle 3 [] alphaBoundary++  annulus <-+    regionMeshFromPoints+      "alpha annulus"+      (ringPoints 4 32 <> ringPoints 2 16 <> ringPoints 3 24)+  annulusThreshold <-+    requireRight "alpha annulus threshold" (mkRadiusSquared 0.4)+  annulusBoundary <-+    requireComponentBoundary+      "alpha annulus"+      True+      annulus+      (alphaShapeContainsFace annulusThreshold annulus)+  assertEqual+    "alpha annulus hole count"+    1+    (length (regionBoundaryHoleLoops annulusBoundary))+  assertLoopWinding+    "alpha annulus outer winding"+    GT+    annulus+    (regionBoundaryOuterLoop annulusBoundary)+  traverse_+    (assertLoopWinding "alpha annulus hole winding" LT annulus)+    (regionBoundaryHoleLoops annulusBoundary)+  traverse_+    (\(label, value, failure) ->+       assertEqual label (Left failure) (mkRadiusSquared value))+    [ ("negative alpha threshold refusal", -1, NegativeRadiusSquared (-1))+    , ("NaN alpha threshold refusal", 0 / 0, NonFiniteRadiusSquared ValueNaN)+    , ("positive-infinite alpha threshold refusal", 1 / 0, NonFiniteRadiusSquared ValuePositiveInfinity)+    , ("negative-infinite alpha threshold refusal", (-1) / 0, NonFiniteRadiusSquared ValueNegativeInfinity)+    ]++ringPoints :: Double -> Int -> [Point]+ringPoints radius count =+  [ Point (radius * cos angle) (radius * sin angle)+  | index <- [0 .. count - 1]+  , let angle = 2 * pi * fromIntegral index / fromIntegral count+  ]++regionMesh :: String -> Int -> Int -> IO NativeMesh+regionMesh label widthInCells heightInCells =+  regionMeshFromPoints+    label+    [ Point (fromIntegral x) (fromIntegral y)+    | y <- [0 .. heightInCells]+    , x <- [0 .. widthInCells]+    ]++regionMeshFromPoints :: String -> [Point] -> IO NativeMesh+regionMeshFromPoints label points =+  buildTriangulation <$> requirePointBuild label points++regionFaceCentroid+  :: Triangulation mode vertex directed undirected face+  -> FaceId+  -> Maybe Point+regionFaceCentroid triangulation face =+  (\(first, second, third) ->+     centroid+       (vertexPoint triangulation first)+       (vertexPoint triangulation second)+       (vertexPoint triangulation third))+    <$> Dcel.innerFaceVertices triangulation face++regionFaceSatisfies+  :: Triangulation mode vertex directed undirected face+  -> (Point -> Bool)+  -> FaceId+  -> Bool+regionFaceSatisfies triangulation predicate =+  maybe False predicate . regionFaceCentroid triangulation++requireLabelledComponent+  :: (Eq label, Show label)+  => String+  -> label+  -> [(label, FaceComponent)]+  -> IO FaceComponent+requireLabelledComponent label expected components =+  case [component | (actual, component) <- components, actual == expected] of+    [component] -> pure component+    matches ->+      fail+        ( label+            <> ": expected one component for label "+            <> show expected+            <> ", got "+            <> show (length matches)+        )++requireComponentBoundary+  :: (Eq label, Show label)+  => String+  -> label+  -> Triangulation mode vertex directed undirected face+  -> (FaceId -> label)+  -> IO RegionBoundary+requireComponentBoundary label expected triangulation labelFace = do+  component <-+    requireLabelledComponent+      (label <> " component")+      expected+      (faceComponents triangulation labelFace)+  requireRight (label <> " boundary") (componentBoundary triangulation component)++loopPointSet+  :: Triangulation mode vertex directed undirected face+  -> BoundaryLoop+  -> Set.Set Point+loopPointSet triangulation =+  Set.fromList+    . fmap (vertexPoint triangulation)+    . toList+    . boundaryLoopVertices++residentLoopPointSet+  :: Triangulation mode vertex directed undirected face+  -> BoundaryLoop+  -> Set.Set Point+residentLoopPointSet triangulation =+  Set.fromList+    . fmap (vertexPoint triangulation)+    . toList+    . boundaryLoopResidentVertices++residentBoundaryIsEdgeCycle+  :: Triangulation mode vertex directed undirected face+  -> BoundaryLoop+  -> Bool+residentBoundaryIsEdgeCycle triangulation loop =+  let first :| remaining = boundaryLoopResidentVertices loop+      adjacentPairs = zip (first : remaining) (remaining <> [first])+   in all+        (\(fromVertex, toVertex) ->+           any+             ((== toVertex) . destination triangulation)+             (vertexOutgoingEdges triangulation fromVertex))+        adjacentPairs++assertLoopWinding+  :: String+  -> Ordering+  -> Triangulation mode vertex directed undirected face+  -> BoundaryLoop+  -> IO ()+assertLoopWinding label expected triangulation loop =+  let first :| remaining = fmap (vertexPoint triangulation) (boundaryLoopVertices loop)+      points = first : remaining+      twiceArea =+        sum+          ( zipWith+              (\(Point ax ay) (Point bx by) -> ax * by - ay * bx)+              points+              (remaining <> [first])+          )+   in assertEqual label expected (compare twiceArea 0)++assertBoundaryShape+  :: String+  -> Triangulation mode vertex directed undirected face+  -> Int+  -> [Int]+  -> RegionBoundary+  -> IO ()+assertBoundaryShape label triangulation outerVertexCount holeVertexCounts boundary = do+  assertEqual+    (label <> " outer orientation")+    BoundaryCounterClockwise+    (boundaryLoopOrientation (regionBoundaryOuterLoop boundary))+  assertEqual (label <> " outer vertex count") outerVertexCount+    (length (boundaryLoopVertices (regionBoundaryOuterLoop boundary)))+  assertLoopWinding (label <> " outer winding") GT triangulation+    (regionBoundaryOuterLoop boundary)+  let holes = regionBoundaryHoleLoops boundary+  assertEqual (label <> " hole count") (length holeVertexCounts) (length holes)+  traverse_+    (\(vertexCount, hole) -> do+       assertEqual+         (label <> " hole orientation")+         BoundaryClockwise+         (boundaryLoopOrientation hole)+       assertEqual (label <> " hole vertex count") vertexCount+         (length (boundaryLoopVertices hole))+       assertLoopWinding (label <> " hole winding") LT triangulation hole)+    (zip holeVertexCounts holes)++testTraversal :: IO ()+testTraversal = do+  built <- requirePointBuild "traversal" [Point (-3) 0, Point (-1) (-2), Point (-1) 2, Point 1 (-2), Point 1 2, Point 3 0]+  forwardStart <- requireQueryPoint "forward traversal start" (Point (-4) 0)+  forwardEnd <- requireQueryPoint "forward traversal end" (Point 4 0)+  let triangulation = buildTriangulation built+      forward = lineIntersections triangulation forwardStart forwardEnd+      backward = lineIntersections triangulation forwardEnd forwardStart+  when (null forward) $ fail "ordered line traversal crossed nothing"+  assertEqual+    "reversing the segment reverses the crossings"+    (map crossingIdentity forward)+    (reverse (map crossingIdentity backward))+  circleEdges <- Set.fromList <$> requireRight "circle edge query" (edgesInCircle triangulation (Point 0 0) 4)+  let bruteCircle = Set.fromList+        [ edge+        | edge <- undirectedEdges triangulation+        , let (a, b) = undirectedEndpoints triangulation edge+        , segmentDistanceSquared (vertexPoint triangulation a) (vertexPoint triangulation b) (Point 0 0) <= 4+        ]+  assertEqual "circle edge flood" bruteCircle circleEdges+  assertEqual+    "negative circle metric refusal"+    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))+    (circleMetric (Point 0 0 :: Point) (-1))+  assertEqual+    "negative circle edge-query refusal"+    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))+    (edgesInCircle triangulation (Point 0 0) (-1))+  assertEqual+    "negative circle vertex-query refusal"+    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))+    (verticesInCircle triangulation (Point 0 0) (-1))+  assertEqual+    "NaN circle metric refusal"+    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNaN)))+    (circleMetric (Point 0 0 :: Point) (0 / 0))+  assertEqual+    "NaN circle center-x refusal"+    (Left (InvalidCircleCenter (InvalidPointX CoordinateNaN)))+    (circleMetric (Point (0 / 0) 0 :: Point) 1)+  assertEqual+    "positive-infinite circle center-y refusal"+    (Left (InvalidCircleCenter (InvalidPointY CoordinateInfinite)))+    (edgesInCircle triangulation (Point 0 (1 / 0)) 1)+  assertEqual+    "negative-infinite circle center-x refusal"+    (Left (InvalidCircleCenter (InvalidPointX CoordinateInfinite)))+    (verticesInCircle triangulation (Point ((-1) / 0) 0) 1)+  assertEqual+    "infinite circle edge-query refusal"+    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValuePositiveInfinity)))+    (edgesInCircle triangulation (Point 0 0) (1 / 0))+  assertEqual+    "negative-infinite circle vertex-query refusal"+    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNegativeInfinity)))+    (verticesInCircle triangulation (Point 0 0) ((-1) / 0))+  rectangleVertices <-+    Set.fromList+      <$> requireRight+        "rectangle query"+        (verticesInRectangle triangulation (Point (-1.1) (-2.1)) (Point 1.1 2.1))+  let bruteVertices = Set.fromList+        [ vertex+        | vertex <- vertices triangulation+        , let Point x y = vertexPoint triangulation vertex+        , x >= (-1.1), x <= 1.1, y >= (-2.1), y <= 2.1+        ]+  assertEqual "rectangle vertex flood" bruteVertices rectangleVertices++testRandomizedConstruction :: IO ()+testRandomizedConstruction = do+  forM_ [0 .. 31] $ \index -> do+    let points = randomPoints (0x9e37_79b9 + fromIntegral index) (40 + index * 7)+    built <- requirePointBuild ("random build " <> show index) points+    let triangulation = buildTriangulation built+    assertValid ("random build " <> show index) triangulation+    assertEqual "random input mapping" (length points) (sizeofPrimArray (buildInputVertices built))++testErrors :: IO ()+testErrors = do+  assertEqual+    "NaN query x refusal"+    (Left (InvalidPointX CoordinateNaN))+    (mkQueryPoint (Point (0 / 0) 0))+  assertEqual+    "infinite query y refusal"+    (Left (InvalidPointY CoordinateInfinite))+    (mkQueryPoint (Point 0 (1 / 0)))+  assertEqual+    "negative-infinite query x refusal"+    (Left (InvalidPointX CoordinateInfinite))+    (mkQueryPoint (Point ((-1) / 0) 0))+  case delaunay unitElementDefaults (V.singleton (Point (0 / 0) 0 :: Point)) of+    Left (InvalidCoordinate (Just 0) _ CoordinateNaN) -> pure ()+    Left other -> fail ("NaN insertion result: " <> show other)+    Right built ->+      fail+        ( "NaN insertion result: built a triangulation of "+            <> show (numVertices (buildTriangulation built))+            <> " vertices"+        )+  expectBuildFailure+    "NaN minimum angle"+    (RefinementMinimumAngleNotFinite ValueNaN)+    (withMinimumAngle (0 / 0 :: Double) defaultRefinementParameters)+  expectBuildFailure+    "infinite minimum angle"+    (RefinementMinimumAngleNotFinite ValuePositiveInfinity)+    (withMinimumAngle (1 / 0 :: Double) defaultRefinementParameters)+  expectBuildFailure+    "out-of-range minimum angle"+    (RefinementMinimumAngleOutOfRange 61)+    (withMinimumAngle (61 :: Double) defaultRefinementParameters)+  expectBuildFailure+    "minimum angle whose derived ratio overflows"+    (RefinementMinimumAngleDerivedRatioNotFinite ValuePositiveInfinity)+    (withMinimumAngle (encodeFloat 1 (-1074) :: Double) defaultRefinementParameters)+  square <- requirePointBuild "area validation square" [Point 0 0, Point 1 0, Point 1 1, Point 0 1]+  let refineWith parameters =+        refine id parameters (buildTriangulation square)+      refineWithArea area =+        refineWith defaultRefinementParameters{refineMaxArea = Just area}+  expectBuildFailure+    "negative refinement vertex budget"+    (RefinementMaximumAdditionalVerticesNegative (-1))+    (refineWith defaultRefinementParameters{refineMaxAdditionalVertices = Just (-1)})+  expectBuildFailure+    "infinite minimum area"+    (RefinementMinimumAreaNotFinite ValuePositiveInfinity)+    (refineWith defaultRefinementParameters{refineMinArea = Just (1 / 0)})+  expectBuildFailure+    "negative minimum area"+    (RefinementMinimumAreaNegative (-1))+    (refineWith defaultRefinementParameters{refineMinArea = Just (-1)})+  expectBuildFailure+    "NaN maximum area"+    (RefinementMaximumAreaNotFinite ValueNaN)+    (refineWithArea (0 / 0))+  expectBuildFailure+    "infinite maximum area"+    (RefinementMaximumAreaNotFinite ValuePositiveInfinity)+    (refineWithArea (1 / 0))+  expectBuildFailure+    "zero maximum area"+    (RefinementMaximumAreaNotPositive 0)+    (refineWithArea 0)+  expectBuildFailure+    "negative maximum area"+    (RefinementMaximumAreaNotPositive (-1))+    (refineWithArea (-1))+  expectBuildFailure+    "infinite maximum radius/edge ratio"+    (RefinementMaximumRadiusEdgeRatioNotFinite ValuePositiveInfinity)+    (refineWith defaultRefinementParameters{refineMaxRadiusEdgeRatio = Just (1 / 0)})+  expectBuildFailure+    "non-positive maximum radius/edge ratio"+    (RefinementMaximumRadiusEdgeRatioNotPositive 0)+    (refineWith defaultRefinementParameters{refineMaxRadiusEdgeRatio = Just 0})+  expectBuildFailure+    "infinite maximum edge length"+    (RefinementMaximumEdgeLengthNotFinite ValuePositiveInfinity)+    (refineWith defaultRefinementParameters{refineMaxEdgeLength = Just (1 / 0)})+  expectBuildFailure+    "non-positive maximum edge length"+    (RefinementMaximumEdgeLengthNotPositive 0)+    (refineWith defaultRefinementParameters{refineMaxEdgeLength = Just 0})+  expectBuildFailure+    "minimum area above maximum area"+    (RefinementMinimumAreaExceedsMaximum 2 1)+    ( refineWith+        defaultRefinementParameters+          { refineMinArea = Just 2+          , refineMaxArea = Just 1+          }+    )+  case refineWithArea 0.25 of+    Right _ -> pure ()+    Left failure -> fail ("positive maximum area rejected: " <> show failure)++expectBuildFailure :: String -> BuildError -> Either BuildError value -> IO ()+expectBuildFailure label expected outcome =+  case outcome of+    Left actual -> assertEqual label expected actual+    Right _ -> fail (label <> ": expected " <> show expected <> ", got success")+++canonicalEdges+  :: Triangulation mode vertex directed undirected face+  -> Set.Set (Point, Point)+canonicalEdges triangulation =+  Set.fromList+    [ ordered (vertexPoint triangulation (origin triangulation edge)) (vertexPoint triangulation (destination triangulation edge))+    | undirected <- undirectedEdges triangulation+    , let edge = normalizedDirected undirected+    ]+ where+  ordered :: Ord value => value -> value -> (value, value)+  ordered left right = if left <= right then (left, right) else (right, left)++requireJust :: String -> Maybe value -> IO value+requireJust _ (Just value) = pure value+requireJust label Nothing = fail (label <> ": expected Just")++requireAcceptedConstraint+  :: String+  -> ConstraintBatchResult vertex directed undirected face+  -> IO (V.Vector DirectedEdgeId, Int)+requireAcceptedConstraint label batch =+  case V.toList (constraintBatchOutcomes batch) of+    [ConstraintAccepted path added] -> pure (path, added)+    outcomes ->+      fail+        ( label+            <> ": expected one accepted constraint, got "+            <> show outcomes+        )++assertBatchStats+  :: String+  -> ConstraintBatchResult vertex directed undirected face+  -> IO ()+assertBatchStats label batch = do+  let stats = constraintBatchStats batch+      (accepted, rejected) =+        V.foldl'+          (\(!acceptedCount, !rejectedCount) outcome ->+            case outcome of+              ConstraintAccepted _ _ -> (acceptedCount + 1, rejectedCount)+              ConstraintRejected _ -> (acceptedCount, rejectedCount + 1)+          )+          (0, 0)+          (constraintBatchOutcomes batch)+  assertEqual+    (label <> " request count")+    (V.length (constraintBatchOutcomes batch))+    (constraintBatchRequests stats)+  assertEqual+    (label <> " accepted count")+    accepted+    (constraintBatchAccepted stats)+  assertEqual+    (label <> " rejected count")+    rejected+    (constraintBatchRejected stats)++replayConstraintRequest+  :: (ConstrainedDelaunayTriangulation (Point), [ConstraintOutcome])+  -> (VertexId, VertexId)+  -> Either+      (CdtError)+      (ConstrainedDelaunayTriangulation (Point), [ConstraintOutcome])+replayConstraintRequest (current, outcomes) request = do+  singleton <- recoverConstraints current (V.singleton request)+  case V.toList (constraintBatchOutcomes singleton) of+    [outcome] ->+      Right+        ( constraintBatchTriangulation singleton+        , outcome : outcomes+        )+    cardinality ->+      Left+        ( ConstraintBatchCardinalityMismatch+            1+            (length cardinality)+        )++requirePointBuild :: String -> [Point] -> IO (BuildResult 'Unconstrained (Point) () () ())+requirePointBuild label points = requireRight label (delaunay unitElementDefaults (V.fromList points))++assertCdtValid :: String -> Triangulation 'Constrained vertex () () () -> IO ()+assertCdtValid label triangulation =+  case validateTriangulation triangulation of+    [] -> pure ()+    violations -> fail (label <> " CDT violations: " <> show violations)++assertMaximumEdgeLength+  :: String+  -> Double+  -> Triangulation mode vertex directed undirected face+  -> [FaceId]+  -> IO ()+assertMaximumEdgeLength label maximumLength triangulation faces =+  unless (null violations) $+    fail (label <> " oversized edges: " <> show (take 1 violations))+ where+  !maximumSquaredLength = maximumLength * maximumLength+  violations =+    [ (face, edge, squaredLength)+    | face <- faces+    , directed <- faceDirectedEdges triangulation face+    , let edge = asUndirected directed+          (fromVertex, toVertex) = undirectedEndpoints triangulation edge+          squaredLength =+            squaredDistanceWide+              (vertexPoint triangulation fromVertex)+              (vertexPoint triangulation toVertex)+    , squaredLength > maximumSquaredLength+    ]++canonicalSegmentSquaredLength :: CanonicalSegment -> Double+canonicalSegmentSquaredLength segmentValue =+  squaredDistanceWide+    (segmentStart segmentValue)+    (segmentEnd segmentValue)  assertNear :: String -> Double -> Double -> Double -> IO () assertNear label tolerance expected actual =
+ test/persistence-rose/Main.hs view
@@ -0,0 +1,196 @@+module Main (main) where++import Data.ByteString qualified as ByteString+import Data.List (isInfixOf, isPrefixOf, isSuffixOf)+import Data.Word (Word8)+import Moonlight.Triangulation.Exhibit.PersistenceRose+  ( AlphaEclipseReceipt (..),+    AlphaEclipseScene,+    PersistenceRoseAnimationReceipt (..),+    PersistenceRoseReceipt (..),+    PersistenceRoseScene,+    alphaEclipseReceipt,+    buildAlphaEclipseScene,+    buildPersistenceRoseScene,+    persistenceRoseFrameSpecs,+    persistenceRoseReceipt,+    renderPersistenceRoseFrame,+    renderPersistenceRosePoster,+    renderAlphaEclipse,+    stampPersistenceRoseGif,+    verifyPersistenceRoseGif,+  )+import Test.Tasty (TestTree, defaultMain, testGroup)+import Test.Tasty.HUnit+  ( Assertion,+    assertBool,+    assertFailure,+    testCase,+    (@?=),+  )++main :: IO ()+main = defaultMain persistenceRoseTests++persistenceRoseTests :: TestTree+persistenceRoseTests =+  testGroup+    "persistence rose"+    [ testCase "scene builds from canonical owners" testSceneBuilds,+      testCase "hero holes agree with persistent beta one" testHeroAgreement,+      testCase "Sibson field has no fallback" testSibsonField,+      testCase "poster is deterministic SVG" testPosterDeterminism,+      testCase "frame sweep closes exactly" testFrameSweepClosure,+      testCase "alpha eclipse is the exact resident square interval" testAlphaEclipseSemantics,+      testCase "alpha eclipse is deterministic SVG" testAlphaEclipseDeterminism,+      testCase "GIF provenance binds the canonical frame section" testGifProvenance+    ]++withScene :: (PersistenceRoseScene -> Assertion) -> Assertion+withScene = withResult buildPersistenceRoseScene++withAlphaEclipse :: (AlphaEclipseScene -> Assertion) -> Assertion+withAlphaEclipse = withResult buildAlphaEclipseScene++withResult :: Show failure => Either failure value -> (value -> Assertion) -> Assertion+withResult result assertion = either (assertFailure . show) assertion result++testSceneBuilds :: Assertion+testSceneBuilds =+  withScene $ \scene -> do+    let receipt = persistenceRoseReceipt scene+    assertBool "the deterministic cover should contain at least one hundred sites" (roseSiteCount receipt >= 100)+    assertBool "Delaunay edges should be nontrivial" (roseEdgeCount receipt > roseSiteCount receipt)+    assertBool "bounded faces should be nontrivial" (roseFaceCount receipt > 0)+    assertBool "the alpha filtration should have many exact critical births" (roseCriticalBirthCount receipt > 20)+    assertBool "the exact barcode should contain finite H1 intervals" (roseFinitePersistentHoleCount receipt > 0)+    assertBool "the Voronoi dual should contain finite segments" (roseVoronoiSegmentCount receipt > 0)+    assertBool "the exact overlay and Minkowski mask should remain nonempty" (roseMaskComponentCount receipt > 0)++testHeroAgreement :: Assertion+testHeroAgreement =+  withScene $ \scene -> do+    let receipt = persistenceRoseReceipt scene+    assertBool "the hero state should display at least seven independent holes" (roseHeroBettiOne receipt >= 7)+    roseHeroGeometricHoleCount receipt @?= roseHeroBettiOne receipt++testSibsonField :: Assertion+testSibsonField =+  withScene $ \scene -> do+    let receipt = persistenceRoseReceipt scene+    assertBool "the field should contain a dense interior sample" (roseNaturalNeighborSampleCount receipt > 300)+    roseNaturalNeighborFallbackCount receipt @?= 0++testPosterDeterminism :: Assertion+testPosterDeterminism =+  withScene $ \scene -> do+    let firstPoster = renderPersistenceRosePoster scene+        secondPoster = renderPersistenceRosePoster scene+    firstPoster @?= secondPoster+    assertBool "poster should be an SVG document" ("<svg" `isPrefixOf` firstPoster && "</svg>\n" `isSuffixOf` firstPoster)+    assertBool+      "poster should explain beta one semantically"+      ("7 independent one-dimensional holes" `isInfixOf` firstPoster)+    assertBool+      "poster should identify the scale-local topology"+      ("TOPOLOGICAL HOLES AT THIS SCALE" `isInfixOf` firstPoster)+    assertBool+      "poster should omit the old alpha subtitle"+      (not ("EXACT ALPHA / PERSISTENT H1" `isInfixOf` firstPoster))+    assertBool+      "poster should omit the capability footer"+      (not ("VORONOI DUAL · SIBSON FIELD · EXACT OVERLAY" `isInfixOf` firstPoster))+    assertBool+      "poster should omit the receipt footer"+      (not ("glued bounded complement components at the hero state" `isInfixOf` firstPoster))++testFrameSweepClosure :: Assertion+testFrameSweepClosure =+  withScene $ \scene ->+    case persistenceRoseFrameSpecs 49 of+      Left frameError -> assertFailure (show frameError)+      Right frames ->+        case endpoints frames of+          Nothing -> assertFailure "a valid frame count produced no frames"+          Just (firstFrame, finalFrame) -> do+            let firstSvg = renderPersistenceRoseFrame scene firstFrame+            firstSvg @?= renderPersistenceRoseFrame scene finalFrame+            assertBool+              "animation should omit the old descent subtitle"+              (not ("ALPHA DESCENT / GLUING" `isInfixOf` firstSvg))++testAlphaEclipseSemantics :: Assertion+testAlphaEclipseSemantics =+  withAlphaEclipse $ \scene -> do+    let receipt = alphaEclipseReceipt scene+    eclipseSiteCount receipt @?= 4+    eclipseEdgeCount receipt @?= 5+    eclipseFaceCount receipt @?= 2+    (eclipseHoleBirthNumerator receipt, eclipseHoleBirthDenominator receipt) @?= (1, 1)+    (eclipseHoleDeathNumerator receipt, eclipseHoleDeathDenominator receipt) @?= (2, 1)+    (eclipseBettiBeforeBirth receipt, eclipseBettiAtBirth receipt, eclipseBettiAtDeath receipt)+      @?= (0, 1, 0)++testAlphaEclipseDeterminism :: Assertion+testAlphaEclipseDeterminism =+  withAlphaEclipse $ \scene -> do+    let firstSvg = renderAlphaEclipse scene+        secondSvg = renderAlphaEclipse scene+    firstSvg @?= secondSvg+    assertBool "eclipse should be an SVG document" ("<svg" `isPrefixOf` firstSvg && "</svg>\n" `isSuffixOf` firstSvg)+    assertBool "eclipse should name the exact finite interval" ("birth α² = 1" `isInfixOf` firstSvg && "death α² = 2" `isInfixOf` firstSvg)+    assertBool "eclipse should identify one fixed resident DCEL" ("ONE FIXED DELAUNAY DCEL" `isInfixOf` firstSvg)++testGifProvenance :: Assertion+testGifProvenance =+  withScene $ \scene ->+    case stampPersistenceRoseGif scene syntheticCanonicalGif of+      Left gifError -> assertFailure (show gifError)+      Right (stampedReceipt, stamped) -> do+        roseAnimationFrameCount stampedReceipt @?= 65+        case stampPersistenceRoseGif scene stamped of+          Left gifError -> assertFailure (show gifError)+          Right (restampedReceipt, restamped) -> do+            restampedReceipt @?= stampedReceipt+            restamped @?= stamped+        case verifyPersistenceRoseGif scene stamped of+          Left gifError -> assertFailure (show gifError)+          Right receipt -> do+            roseAnimationFrameCount receipt @?= 65+            roseAnimationFramesPerSecond receipt @?= 8+            (roseAnimationWidth receipt, roseAnimationHeight receipt) @?= (960, 540)++syntheticCanonicalGif :: ByteString.ByteString+syntheticCanonicalGif =+  ByteString.pack+    ( [ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61,+        0xc0, 0x03,+        0x1c, 0x02,+        0x00, 0x00, 0x00+      ]+        <> concatMap syntheticGifFrame [0 .. 64]+        <> [0x3b]+    )++syntheticGifFrame :: Int -> [Word8]+syntheticGifFrame frameIndex =+  [ 0x21, 0xf9, 0x04, 0x00,+    if even frameIndex then 0x0d else 0x0c,+    0x00, 0x00, 0x00,+    0x2c,+    0x00, 0x00,+    0x00, 0x00,+    0x01, 0x00,+    0x01, 0x00,+    0x00,+    0x02,+    0x02, 0x44, 0x01,+    0x00+  ]++endpoints :: [value] -> Maybe (value, value)+endpoints values =+  case values of+    [] -> Nothing+    firstValue : remainingValues ->+      Just (firstValue, foldl' (\_ nextValue -> nextValue) firstValue remainingValues)