moonlight-triangulation: Delaunay meshes and exact planar-region algebra.

[ geometry, library, math, mit ] [ Propose Tags ] [ Report a vulnerability ]

Delaunay and constrained Delaunay triangulation as a lawful finite-set algebra, together with exact rational planar regions, labelled common refinement, intrinsic valuations, and polygonal Minkowski morphology. A mesh is a value of its site set, so union, intersection and difference return triangulations and refinement composes after them rather than replacing them. One structure-of-arrays half-edge mesh carries the constrained and unconstrained layers, split into public sublibraries by role: exact binary64 predicates and paged storage; the finite DCEL with its handle, iterator, location and validation surface; circle-sweep bulk load, incremental insertion, removal, conflict-strip constraint recovery and Ruppert refinement; the Voronoi dual with natural-neighbour interpolation and Delaunay hierarchy hints; a bounded-concurrency interpreter for the join tournament; and a versioned binary serialization surface. Failure is values: every refusal names its witness.


[Skip to Readme]

library moonlight-triangulation

library moonlight-triangulation:dual

library moonlight-triangulation:serialize

Modules

[Index]

library moonlight-triangulation:parallel

Modules

[Index]

library moonlight-triangulation:build

library moonlight-triangulation:dcel

Modules

[Index]

library moonlight-triangulation:core

Flags

Manual Flags

NameDescriptionDefault
warnings-as-errors

Treat compiler warnings as errors in package validation.

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

Versions [RSS] 0.1.0.0, 1.0.0.0, 1.0.1.0, 1.2.0.0, 1.2.0.1
Change log CHANGELOG.md
Dependencies async (>=2.2 && <2.3), base (>=4.19 && <5), binary (>=0.8 && <0.9), bytestring (>=0.12 && <0.13), containers (>=0.8 && <0.9), deepseq (>=1.5 && <1.6), moonlight-triangulation, primitive (>=0.9 && <0.10), transformers (>=0.6 && <0.7), vector (>=0.13 && <0.14), vector-algorithms (>=0.9 && <0.10) [details]
Tested with ghc ==9.8.4, ghc ==9.10.3, ghc ==9.12.4, ghc ==9.14.1
License MIT
Copyright (c) 2026 Fable, Blue Rose
Author Fable, Blue Rose
Maintainer rosaliafialkova@gmail.com
Uploaded by bluerose at 2026-08-15T13:52:43Z
Category Geometry, Math
Home page https://github.com/PaleRoses/moonlight
Bug tracker https://github.com/PaleRoses/moonlight/issues
Source repo head: git clone https://github.com/PaleRoses/moonlight.git(moonlight-triangulation)
Distributions
Downloads 10 total (10 in the last 30 days)
Rating 2.0 (votes: 1) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for moonlight-triangulation-1.2.0.1

[back to package description]

moonlight-triangulation

Part of Moonlight, the sheaf-theoretic computation layer beneath Melusine and Pale Meridian.

moonlight-triangulation carries Delaunay and constrained Delaunay triangulations as a lawful finite-set algebra under canonical observation: a mesh represents its site set, a join returns a valid Delaunay representative, and the result is a triangulation again — so the operations close, compose, and fold. Operations return typed obstructions where the finite arena cannot represent a result.

Delaunay triangulation, constrained Delaunay (CDT), exact rational planar regions and labelled overlay, intrinsic valuations, polygonal Minkowski morphology, the Voronoi dual, natural-neighbour interpolation, Ruppert refinement, walk point location, convex hull, exact Shewchuk predicates, incremental insertion and removal, and versioned binary serialization.

Operations

Operation Role What it returns
union join A valid Delaunay representative of both site sets; overlapping annotations glue through JoinSemilattice.
unions balanced fold The same join over a list, associated for the tournament rather than the left.
siteRelation support order Exact equality, proper-subset, disjointness or partial-overlap classification.
intersection geometry-only meet The sites both meshes hold.
intersectionWith annotated meet Shared sites with a left-then-right annotation combiner.
difference relative complement The left's sites and annotations, less the right's support.
symmetricDifference exclusive or The sites and annotations carried by exactly one mesh.
canonicalize physical normal form Construction-independent dense numbering, derived explicitly when required.
refine quality Steiner insertion, composed after any of the above — never a second kind of mesh.
constrainedDelaunay boundary Sites plus segments, after which interiority is computable.
overlayLayers exact common refinement One provenance-bearing arrangement whose bounded cells carry both source labels.
overlayClosedUnion / overlayClosedIntersection / overlayRegularizedDifference planar Boolean selection A closed exact cell set retaining point- and edge-only results; difference is regularized from selected faces.
overlaySelectedRegion polygon publication Selected bounded faces with internal arrangement edges dissolved through the existing boundary walker.
cellValuations / regionValuations intrinsic measurement Exact Euler characteristic and rational area plus symbolic radical length with certified bounds.
minkowskiSum polygonal convolution Exact convex or general polygonal Minkowski addition, with work recorded in a receipt.
erodeBy / openWith / closeWith regularized morphology Full-dimensional polygonal erosion and its opening/closing compositions for an origin-anchored convex kernel.

Choose the operation

What you need Use Do not substitute
Build a geometry-only mesh from coordinates delaunayGeometry Do not manufacture unit annotations and erase them afterward.
Build from separate coordinates and annotations delaunayFromCoordinates Do not fabricate a HasPosition instance for an annotation that does not own geometry.
Combine two unconstrained meshes union Do not concatenate vertices and rebuild manually; union owns overlap annotations and the measured schedule.
Combine many unconstrained meshes unions Do not left-fold union; unions owns balanced association.
Classify coordinate support without constructing a mesh siteRelation Do not compare vertex counts or resident Eq; neither answers support order.
Keep coordinates present in both geometry-only meshes intersection Use intersectionWith instead when annotations must survive or be recomputed.
Keep shared coordinates and choose their annotation intersectionWith combine For a left annotation-preserving mask, use intersectionWith const left mask.
Remove a coordinate mask from a mesh difference source mask Do not remove by stale VertexId; removal compacts arenas while coordinates remain stable.
Keep coordinates present in exactly one mesh symmetricDifference Do not spell it as two differences plus union; the direct operation owns the persistent toggle schedule.
Build the first constrained mesh constrainedDelaunay Do not build unconstrained and treat a rendered outline as topology.
Canonically combine arbitrary constrained meshes unionConstrainedWith or geometry-only unionConstrained Do not use unconstrained union; constrained union owns complete conflict witnesses and constraint recovery.
Join two strictly x-separated constrained meshes joinSeparatedConstrained Separation means annotations are copied, never combined; use the general constrained union when separation is not proved.
Append one constrained section to an authoritative constrained base extendConstrainedWith Do not use symmetric constrained union when base identity and already-solved constraints must remain resident.
Insert one site persistently BulkLoad.insert or BulkLoad.insertAt Do not open a manual transaction; the singleton entry owns the dense/copy-on-write crossover.
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.
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.
Preserve a point- or edge-only Boolean result overlayClosedUnion, overlayClosedIntersection, or overlayRegularizedDifference Do not publish immediately as PlanarRegion; that carrier is deliberately full-dimensional.
Publish a selected polygonal region overlaySelectedRegion predicate overlay Do not collect arrangement cells independently; selection must precede boundary descent so internal seams disappear.
Measure a closed exact selection or region cellValuations or regionValuations Do not coerce irrational perimeter to an allegedly exact Double; inspect its radical expression and certified interval.
Expand a polygon by another polygon minkowskiSum or polygonOffset Do not add resident mesh vertices pairwise and rebuild; morphology acts on the represented continuum.
Erode, open, or close a polygonal region erodeBy, openWith, or closeWith Do not infer a hidden universe or invent lower-dimensional polygons; the result is regularized to representable 2D area.
Keep Delaunay faces below an alpha threshold alphaShapeContainsFace threshold mesh This is the filled-face filtration, not palette logic and not a claim to expose every simplex of a full alpha-complex carrier.
Require construction-independent numbering canonicalize at the observation boundary Do not canonicalize every intermediate value; it is intentionally global work.

Foreign bindings

The moonlight-triangulation-c foreign-library component exposes opaque immutable geometry meshes, batch insertion, finite-set operations, and dense vertex and triangle projections through one versioned C ABI. Python, TypeScript, and Rust bindings live beneath bindings/ and descend through that same ABI; none restates the triangulation engine or introduces a second mesh representation.

union a a is a; commutativity and associativity hold after explicit canonicalize; and a join adds no sites: the result carries |A| + |B| − |A ∩ B| of them. Structural Eq remains exact resident equality for caches and serialization rather than secretly rebuilding the mesh.

Persistent publication schedules

The algebraic result is independent of the execution schedule. These are the currently measured publication choices; canonicalize remains the explicit global observation when construction-independent numbering is required.

Operation context Publication schedule
siteRelation left right Index the smaller support in one transient exact open-addressed section, scan the other operand, then discard the index; no support maps or published cache state.
difference mesh empty, symmetricDifference mesh empty, symmetricDifference empty mesh Return the surviving representative verbatim.
difference left right with size right <= (size left - size right) / 128 Remove the right support through one local copy-on-write session; return left verbatim when the supports are disjoint. Larger masks rebuild.
Geometry-only intersection left right Return an existing operand for equality or subset, return the empty mesh for disjoint supports, and locally remove the smaller complement when it is at most overlap / 128. Other partial overlaps rebuild.
intersectionWith Rebuild, because the annotation combiner may rewrite every surviving payload even when topology changes locally.
symmetricDifference left right with small <= (large - small) / 128 Toggle the smaller operand through one local session. Comparable operands and small-output, large-input cases rebuild.
Comparable symmetricDifference left right Partition through one transient exact index and a matched bitset, then rebuild only the exclusive output section.
BulkLoad.insert / BulkLoad.insertAt Dense publication below 10,000 resident sites; copy-on-write publication at 10,000 and above. A sequence still belongs in one Session.
extendConstrainedWith Copy-on-write only for a base of at least 200,000 sites, at most 128 incoming sites, exactly one incoming segment, and a pre-thaw corridor with no resident intersection. Every unmeasured or resident-corridor case stays dense.
refineWithinDomain Dense publication. The local transaction candidate preserved semantics but did not improve wall time, so it was removed.

Benchmark highlights

Fresh source build on 2026-08-12. The comparison binaries passed exact canonical-output agreement before seven interleaved fresh-process timing rounds; brackets are the observed range. Lower is better.

Operation Workload Moonlight Spade 2.15.1 Result
Removal remove 125k / 500k sites 522.067 ms [514.481–539.392] 16.576 s [15.304–17.308] 31.75× faster
Removal remove 25k / 100k sites 72.164 ms [67.604–78.116] 360.899 ms [264.639–406.783] 5.00× faster median
Natural-neighbour interpolation 2k queries / 400k sites 192.792 ms [178.064–201.805] 233.688 ms [214.695–250.986] 1.21× faster
Incremental insertion 500k sites 33.403 s [32.592–36.813] 30.506 s [28.809–31.688] within 1.09×

Spade exposes no public triangulation set algebra corresponding to these operations. Their independently witnessed Moonlight publication costs are:

Operation Workload Moonlight
siteRelation 1m sites vs 995k retained 96.759 ms
intersection retain 995k / 1m sites 109.513 ms
symmetricDifference publish the 5k-site exclusive result 119.298 ms
difference remove 5k / 1m sites 129.820 ms
union add 5k / 5m sites 130.295 ms

These are raw publication measurements. canonicalize remains a separate, explicit observation when construction-independent numbering is required.

union :: JoinSemilattice annotation
      => Triangulation 'Unconstrained annotation () () ()
      -> Triangulation 'Unconstrained annotation () () ()
      -> Either BuildError (Triangulation 'Unconstrained annotation () () ())

unions :: JoinSemilattice annotation
       => [Triangulation 'Unconstrained annotation () () ()]
       -> Either BuildError (Triangulation 'Unconstrained annotation () () ())

siteRelation
      :: Triangulation leftMode leftAnnotation leftDirected leftUndirected leftFace
      -> Triangulation rightMode rightAnnotation rightDirected rightUndirected rightFace
      -> SiteRelation

intersection
      :: Triangulation 'Unconstrained () () () ()
      -> Triangulation 'Unconstrained () () () ()
      -> Either BuildError (Triangulation 'Unconstrained () () () ())

intersectionWith
      :: (leftAnnotation -> rightAnnotation -> annotation)
      -> Triangulation 'Unconstrained leftAnnotation () () ()
      -> Triangulation 'Unconstrained rightAnnotation () () ()
      -> Either BuildError (Triangulation 'Unconstrained annotation () () ())

difference
      :: Triangulation 'Unconstrained leftAnnotation () () ()
      -> Triangulation 'Unconstrained rightAnnotation () () ()
      -> Either BuildError (Triangulation 'Unconstrained leftAnnotation () () ())

symmetricDifference
      :: Triangulation 'Unconstrained annotation () () ()
      -> Triangulation 'Unconstrained annotation () () ()
      -> Either BuildError (Triangulation 'Unconstrained annotation () () ())

Constraints

  • No Semigroup, no Monoid, no <>. A join the finite arena cannot represent is a Left; a partial class instance would lie about totality.
  • union and unions glue overlapping annotations through JoinSemilattice. intersectionWith supplies the corresponding explicit overlap combiner. difference preserves the left annotation and symmetricDifference preserves the annotation of whichever exclusive site survives. Plain intersection remains the geometry-only specialization. Annotation-preserving restriction is derived rather than stored as a second operator: intersectionWith const left mask.
  • refine composes after an operation. It is not a mode, a flag, or a second triangulation type.
  • Mutation lives in ST behind Moonlight.Triangulation.Internal.Mutable and never escapes.
  • serialize is absent from the facade by design; import Moonlight.Triangulation.Serialization to reach it. Its canonical decoder requires both a DecodingBudget and explicit TrustedPayloadDecoders; wire format 6 validates one structural count prefix before decoding defaults, payloads, or allocating a section, and preserves the vertex payload plane's optional fill. The budget covers the input envelope and every module-owned container. trustedBinaryPayloadDecoders records the separate caller decision that the selected executable Binary decoders have an acceptable internal resource policy. Version 5 is intentionally unsupported.
  • Resident triangulation coordinates are binary64. Exact planar-region APIs use normalized rational ExactPoints; an admitted OverlayResult seals those authoritative coordinates beside one certified binary64 DCEL projection. vertexPoints and innerFaceVertexTriples still project the resident DCEL directly; callers do not reconstruct a sibling mesh DTO.
  • One half-edge mesh underneath. Cdt is a mode index on it, not a second structure.

The Haskell facade carries the exact region, overlay, valuation, and morphology algebra. ExactCellSet preserves closed point, edge, and face selections; PlanarRegion is its full-dimensional polygonal publication. The C ABI and Python, TypeScript, and Rust bindings carry the immutable binary64 mesh surface.

Use

A triangulation is a value of its site set: construction returns Either with typed obstructions, union returns the same typed refusal when the finite arena cannot represent its result, and mesh quality is a composition over the result rather than a second operation. This program compiles against the facade alone; fromList is GHC.Exts, so nothing beyond base and moonlight-triangulation is in scope.

module Main where

import GHC.Exts (fromList)
import Moonlight.Triangulation

main :: IO ()
main = do
  let build :: [(Double, Double)] -> IO (DelaunayTriangulation ())
      build coords =
        either (fail . show) pure (delaunayGeometry (fromList [Point x y | (x, y) <- coords]))
  a <- build [(x, y) | x <- [0 .. 9], y <- [0 .. 9]]
  b <- build [(x + 6, y) | x <- [0 .. 9], y <- [0 .. 9]]

  -- valid Delaunay representative of the union of sites
  joined <- either (fail . show) pure (union a b)

  -- meet and differences return typed obstructions, never partial results
  met <- either (fail . show) pure (intersection a b)

  -- mesh quality is a composition, not a second operation
  parameters <- either (fail . show) pure (withMinimumAngle 14 defaultRefinementParameters)
  result <- either (fail . show) pure (refine (const ()) parameters joined)
  let healed = refinedTriangulation result

  print (numVertices joined, numVertices met, numVertices healed, refinementComplete result)

Output: (160,40,160,True) — the union carries 160 sites, the meet 40, and refinement inserts nothing because every triangle already clears the 14° bar; refinementComplete states that sufficiency. Where operands leave concavities, the same composition places Steiner sites exactly at the slivers it dissolves.

delaunayGeometry constructs the unit-annotated carrier directly. Payloads can instead enter with their coordinates through delaunay, or independently through delaunayFromCoordinates; annotation-preserving restriction is available through intersectionWith, difference, and symmetricDifference. Constraints enter through constrainedDelaunay; bulk incremental work names the machine-room module directly (Moonlight.Triangulation.BulkLoad for insertMany, Moonlight.Triangulation.Session for the owned editing transaction — withSession: thaw once, insert and remove freely, publish once).

Interior without hull-fill

A point set has no boundary, so delaunay necessarily meshes the convex hull: concavities and holes are spanned by faces that belong to the hull, not to any intended region. The region becomes real the moment its boundary is authored: constrainedDelaunay takes the sites plus constraint segments as input-index pairs, and interiority is then computable — facesAtEvenBarrierDepth runs a 0–1 BFS from the outer face and returns every face at even barrier depth, which is exactly the outside (depth 0) plus anything nested behind a second loop. The interior is the complement. refine consumes the same parity through refineExcludeOuterFaces, so Steiner sites respect the boundary too.

module Main where

import GHC.Exts (fromList)
import Moonlight.Triangulation

ring :: Double -> Int -> [(Double, Double)]
ring radius n =
  [ (radius * cos t, radius * sin t)
  | k <- [0 .. n - 1]
  , let t = 2 * pi * fromIntegral k / fromIntegral n
  ]

main :: IO ()
main = do
  let outer = ring 4 32
      inner = ring 2 16
      middle = ring 3 24
      pts = fromList [Point x y | (x, y) <- outer <> inner <> middle]
      loop base count = [(base + k, base + (k + 1) `mod` count) | k <- [0 .. count - 1]]
      constraints = fromList (loop 0 32 <> loop 32 16)
  annulus <- case constrainedDelaunay unitElementDefaults pts constraints of
    Left err -> fail (show err)
    Right result -> pure (buildTriangulation result)
  let outside =
        [ fromIntegral raw :: Int
        | FaceId raw <- facesAtEvenBarrierDepth annulus (isConstraintEdge annulus)
        ]
      interior =
        [ FaceId (fromIntegral k)
        | k <- [0 .. numFaces annulus - 1]
        , not (k `elem` outside)
        ]
  print (numFaces annulus, length interior)

Output: (111,97) — the annulus band is the 97 interior faces; the 14 excluded faces are the hole's hull-fill and the outer region. Rendering the interior list draws the ring with a genuine void: no crop, no edge-length heuristic, the engine's own verdict. faceVertices walks each interior face for display, and a nested loop flips parity again, so islands inside holes come back automatically.

Deriving the boundary

When no boundary is known, classify the Delaunay faces and let the DCEL descend them. faceComponents evaluates the caller's label once per bounded face and keeps disconnected regions distinct even when their labels compare equal. componentBoundary then returns one counter-clockwise outer loop and clockwise hole loops, dropping a boundary vertex only when the library's exact orientation predicate proves it collinear and the point lies on the closed neighbour segment. A point pinch is a typed BoundaryPinch, never an invented polygon.

For alpha-shape work the supplied label is simply alphaShapeContainsFace threshold mesh. mkRadiusSquared admits the finite non-negative threshold and exact dyadic comparison makes membership closed at equality. This is the face, or 2-simplex, filtration; it does not manufacture a second mesh or pretend to own palette, colour-space, or rendering policy.

module Main where

import GHC.Exts (fromList)
import Moonlight.Triangulation

ring :: Double -> Int -> [(Double, Double)]
ring radius n =
  [ (radius * cos t, radius * sin t)
  | k <- [0 .. n - 1]
  , let t = 2 * pi * fromIntegral k / fromIntegral n
  ]

main :: IO ()
main = do
  let pts = ring 4 32 <> ring 2 16 <> ring 3 24
  mesh <-
    either
      (fail . show)
      pure
      (delaunayGeometry (fromList [Point x y | (x, y) <- pts]))
  threshold <- either (fail . show) pure (mkRadiusSquared 0.4)
  let components = faceComponents mesh (alphaShapeContainsFace threshold mesh)
      kept = [(label, component) | (label, component) <- components, label]
  boundaries <-
    traverse
      (either (fail . show) pure . componentBoundary mesh . snd)
      kept
  print
    [ ( length (boundaryLoopVertices (regionBoundaryOuterLoop boundary))
      , length (regionBoundaryHoleLoops boundary)
      )
    | boundary <- boundaries
    ]

The numeric threshold remains application policy; the circumradius calculation, face adjacency, loop extraction, winding, holes, and exact simplification do not. The derived boundary walks through data sites, so it is as jagged as the sampling; an authored boundary stays smooth at any pitch and wins where the generator is known.

Exact overlay, valuations, and morphology

Author polygonal regions with rational coordinates, refine labelled layers once, and select every Boolean from that common arrangement. Closed selection retains zero- and one-dimensional intersections; polygon publication dissolves all arrangement edges internal to the selected faces.

Valuations read either carrier. Euler characteristic and area remain exact, while Euclidean length is a normalized radical expression with certified binary64 bounds. Minkowski addition, offset, erosion, opening, and closing then compose over the same admitted PlanarRegion.

module Main where

import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Map.Strict as Map
import Moonlight.Triangulation

admit :: Show obstruction => Either obstruction value -> IO value
admit = either (fail . show) pure

rectangleLoop :: Integer -> Integer -> Integer -> Integer -> IO ExactLoop
rectangleLoop minimumX minimumY maximumX maximumY =
  admit
    ( exactLoop
        ( exactPoint (fromInteger minimumX) (fromInteger minimumY)
            :| [ exactPoint (fromInteger maximumX) (fromInteger minimumY)
               , exactPoint (fromInteger maximumX) (fromInteger maximumY)
               , exactPoint (fromInteger minimumX) (fromInteger maximumY)
               ]
        )
    )

rectangleRegion :: Integer -> Integer -> Integer -> Integer -> IO PlanarRegion
rectangleRegion minimumX minimumY maximumX maximumY = do
  outer <- rectangleLoop minimumX minimumY maximumX maximumY
  component <- admit (polygonComponent outer [])
  admit (planarRegion [component])

insideLayer :: PlanarRegion -> IO (PlanarLayer Bool)
insideLayer region = admit (planarLayer False (Map.singleton True region))

cellSummary :: ExactCellSet -> IO (Int, Integer, Integer, CertifiedInterval)
cellSummary cells = do
  values <- admit (cellValuations cells)
  perimeter <- admit (cellSetPerimeter cells)
  let area = exactAreaValue (valuationArea values)
  pure
    ( eulerCharacteristicValue (valuationEuler values)
    , exactRationalNumerator area
    , exactRationalDenominator area
    , exactLengthBounds perimeter
    )

regionArea :: PlanarRegion -> IO (Integer, Integer)
regionArea region = do
  values <- admit (regionValuations region)
  let area = exactAreaValue (valuationArea values)
  pure (exactRationalNumerator area, exactRationalDenominator area)

main :: IO ()
main = do
  left <- rectangleRegion 0 0 4 4
  right <- rectangleRegion 2 1 6 3
  leftLayer <- insideLayer left
  rightLayer <- insideLayer right
  overlay <- admit (overlayLayers leftLayer rightLayer)

  unionCells <- admit (overlayClosedUnion id id overlay)
  intersectionCells <- admit (overlayClosedIntersection id id overlay)
  differenceCells <- admit (overlayRegularizedDifference id id overlay)
  traverse cellSummary [unionCells, intersectionCells, differenceCells]
    >>= print

  kernelLoop <- rectangleLoop (-1) (-1) 1 1
  kernel <- admit (convexPolygon (exactLoopPoints kernelLoop)) >>= admit . structuringElement
  (sumRegion, _) <- admit (minkowskiSum left right)
  (offsetRegion, _) <- admit (polygonOffset kernel left)
  (insetRegion, _) <- admit (polygonInset kernel left)
  (openedRegion, _) <- admit (openWith kernel left)
  (closedRegion, _) <- admit (closeWith kernel left)
  traverse regionArea [sumRegion, offsetRegion, insetRegion, openedRegion, closedRegion]
    >>= print

The Boolean summaries are union (χ = 1, area = 20, perimeter = 20), intersection (1, 4, 8), and regularized difference (1, 12, 20). The morphology areas are (48, 36, 4, 16, 16): Minkowski sum, one-unit square offset, inset, opening, and closing respectively.

The growing city

A triangulation is a value so that a large one can be extended without being rebuilt or locked: a city mesh a district is added to, and then another.

union rebuilds last. planPair classifies the pair first — an empty or repeated operand returns the other verbatim, separable operands merge along their seam, a subset inserts into its superset, skewed sizes insert the smaller into the larger.

insertionIsCheaper addition base = addition <= 64 || addition <= base `quot` 8

A district is small against a city, so it inserts through a local copy-on-write transaction. Publication scales with the insertion work and pages dirtied rather than copying and renumbering the whole city.

The mesh being read is never the mesh being written. A render or pathfinding thread queries the published value while the next is built: no lock, no defensive copy, no interval in which the world and the mesh disagree. Mutation offers only the choice between stalling in place and answering from a clone that does not yet know what was built, and both are visible from the frame.

The same fact makes a failed union a Left over an untouched operand, reverting an edit a selection, and a preview a second value rather than a copy.

Moonlight.Triangulation.Parallel reads the operation the other way: regions triangulated apart, folded up a balanced tournament, each node's sides concurrent.

Representation

Structure-of-arrays over paged, copy-on-write storage. Half-edge twins are index complements (e xor 1), so traversal is arithmetic rather than indirection. Local insertion transactions copy only the pages they touch; mutation is confined to ST behind Moonlight.Triangulation.Internal.Mutable and never escapes.

Predicates

Moonlight.Triangulation.Internal.Dyadic carries the exact layer: mantissas are decoded and aligned to a common exponent, with bounded predicates evaluated in machine words and larger cases over Integer. Floating approximations are trusted only inside proved error bounds and fall through to exact evaluation otherwise.

Public sublibraries

Depend on the sublibrary you actually use, not on the facade. The footprint is the interface: each row states what the token costs you, and a build that pulls more than the row says is a bug in this table or in the cabal.

Sublibrary Surface
core Exact-arithmetic scalars (Scalar, LineSideInfo) over paged, copy-on-write storage (Internal.Dyadic, .Paged, .BoxedPaged, .PageDirectory, .Growable, .PackedIndex, .FaceQueue). Depends only on base/containers/deepseq/vector — no other sublibrary.
dcel The finite half-edge mesh and its whole read surface: Types, Math, exact geometry, closed CellSet, Region, Valuation, Interop, Dcel, Payload, JoinSemilattice, Handles with its iterator family, PointLocation, Validation, FloodFillIterator, IntersectionIterator. Adds primitive and vector-algorithms over core.
build Everything that constructs: BulkLoad, Session, Removal, typed SetAlgebra, Cdt constraint recovery, Overlay, Minkowski, and Refinement. Adds no external dependency over core and dcel.
parallel Concurrent evaluation of the pure union plan. Adds async at this effect boundary rather than below it.
serialize Serialization — the versioned binary envelope, and the only sublibrary that costs you binary, bytestring, and transformers. Deliberately absent from the facade.
dual The Voronoi dual and what reads it: Voronoi, Voronoi.Handles, Interpolation (natural-neighbour), HintGenerator (Delaunay hierarchy hints). Sits over core, dcel and build.
facade (moonlight-triangulation) Moonlight.Triangulation alone: the equational surface, one export list stating a theory. It re-exports selected names from core/dcel/build/dual — and not serialize. A caller who wants more than the theory names the machine-room module directly.

Surface

  • Moonlight.Triangulation — the apex facade.
  • .BulkLoad — circle-sweep construction and incremental insertion.
  • .Cdt — constraint recovery by conflict strip, with requeue on re-intersection.
  • .PointLocation, .HintGenerator — walk location and Delaunay hierarchy hints.
  • .IntersectionIterator, .FloodFillIterator — ordered line traversal, barrier fill, labelled face components, boundary loops, and alpha-face filtration.
  • .Exact, .CellSet, .Region, .Valuation — exact rational geometry, closed cell selections, admitted polygonal regions, and intrinsic measures.
  • .Overlay, .Minkowski — labelled common refinement, planar Booleans, polygonal convolution, offset, erosion, opening, and closing.
  • .Voronoi, .Interpolation — dual cells and natural-neighbour interpolation.
  • .Refinement — Ruppert-style angle and area refinement.
  • .Validation — structural and Delaunay-property audits.
  • .Serialization (sublibrary serialize) — versioned binary envelope.