moonlight-triangulation: Delaunay triangulations as a lawful finite-set algebra.

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

Delaunay and constrained Delaunay triangulation as a lawful finite-set algebra: 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
fuzz-targets

Build the external coverage-guided fuzz entrypoints.

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

  • No Candidates
Versions [RSS] 0.1.0.0
Change log CHANGELOG.md
Dependencies async (>=2.2 && <2.3), base (>=4.20 && <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.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-11T13:16:23Z
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
Executables moonlight-triangulation-fuzz-refinement, moonlight-triangulation-fuzz-constraints, moonlight-triangulation-fuzz-decode
Downloads 0 total (0 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for moonlight-triangulation-0.1.0.0

[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), the Voronoi dual, natural-neighbour interpolation, Ruppert refinement, walk point location, convex hull, exact Shewchuk predicates, incremental insertion and removal, 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.

Choose the operation

What you need Use Do not substitute
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 joinSeparatedConstrainedWith 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.
Require construction-independent numbering canonicalize at the observation boundary Do not canonicalize every intermediate value; it is intentionally global work.

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.

On the retained one-million-site / five-thousand-site witnesses, raw difference fell from 4.997 s to 0.169 s. Near-full intersection now takes 0.116 s from a cold persistent-index context and 0.100 s after that cache has already been forced, down from 3.475 s before indexed descent. A five-thousand-site symmetricDifference result fell from 4.022 s / 2.19 GB allocated to 0.121 s / 244 MB. Exact siteRelation now takes 0.083 s cold and 0.115 s after forcing the persistent cache, down from 3.417 s before indexed descent. Singleton insertion remains 5.326 ms from 48.037 ms. These are raw publication measurements; paying for canonicalize afterward is deliberately reported separately rather than smuggled into the local-operation claim.

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.
  • Coordinates are binary64 throughout the public surface. vertexPoints and innerFaceVertexTriples project dense vectors directly from the authoritative DCEL; callers do not reconstruct a sibling mesh DTO.
  • One half-edge mesh underneath. Cdt is a mode index on it, not a second structure.

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 Double ())
      build coords = case delaunay unitElementDefaults (fromList [Point x y | (x, y) <- coords]) of
        Left err -> fail (show err)
        Right result -> pure (mapVertices (const ()) (buildTriangulation result))
  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.

Payloads annotate geometry through mapVertices; the input Points arrive as their own vertex payload. The example deliberately erases them with mapVertices (const ()), while 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, the mesh itself carries one. Every Delaunay face has a circumradius: faces inside a sampled region sit near the local pitch, while faces spanning concavities and voids circumscribe them and blow up. Keeping the faces below a threshold derived from the data — a multiple of the median circumradius, so no authored constant — is the alpha-complex, and its boundary falls out as the edges with exactly one kept side. Those edges are already edges of the triangulation, so feeding them to constrainedDelaunay as index pairs recovers without conflicts, and the parity machinery above takes over from there.

module Main where

import Data.List (sort, span)
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 <- case delaunay unitElementDefaults (fromList [Point x y | (x, y) <- pts]) of
    Left err -> fail (show err)
    Right result -> pure (mapVertices (const () :: Point -> ()) (buildTriangulation result))
  let corners f =
        [(x, y) | v <- faceVertices mesh f, let Point x y = vertexPoint mesh v]
      circumradius (ax, ay) (bx, by) (cx, cy) =
        let dab = sqrt ((bx - ax) ** 2 + (by - ay) ** 2)
            dbc = sqrt ((cx - bx) ** 2 + (cy - by) ** 2)
            dca = sqrt ((ax - cx) ** 2 + (ay - cy) ** 2)
            area2 = abs ((bx - ax) * (cy - ay) - (cx - ax) * (by - ay))
        in if area2 == 0 then 1 / 0 else dab * dbc * dca / (2 * area2)
      radii =
        [ (k, circumradius p q s)
        | k <- [0 .. numFaces mesh - 1]
        , [p, q, s] <- [corners (FaceId (fromIntegral k))]
        ]
      sorted = sort (map snd radii)
      medianR = case drop (length sorted `div` 2) sorted of
        m : _ -> m
        [] -> 1
      kept = [FaceId (fromIntegral k) | (k, r) <- radii, r <= 1.35 * medianR]
      norm (p, q) = if p <= q then (p, q) else (q, p)
      edges f = case corners f of
        [p, q, s] -> [norm (p, q), norm (q, s), norm (s, p)]
        _ -> []
      runs xs = case xs of
        [] -> []
        x : rest -> let (same, more) = span (== x) rest in (x, 1 + length same) : runs more
      boundary = [e | (e, n) <- runs (sort (concatMap edges kept)), n == 1]
  print (length radii, length kept, length boundary)

Output: (110,96,48) — 110 finite faces, 96 in the alpha-complex, and the derived boundary is exactly the 48 ring edges: both loops recovered from the point set alone. 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.

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, and determinants are evaluated over Integer. The floating approximation is trusted only inside Shewchuk's error bounds — (3 + 16u)u for orientation, (10 + 96u)u for incircle — and falls through to the 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, 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, 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 and barrier fill.
  • .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.