packages feed

moonlight-triangulation 1.0.0.0 → 1.0.1.0

raw patch · 29 files changed

+2184/−18 lines, 29 filesdep ~moonlight-triangulationnew-component:flib:moonlight-triangulation-c

Dependency ranges changed: moonlight-triangulation

Files

CHANGELOG.md view
@@ -6,6 +6,14 @@ The serialization format carries its own version tag, independent of the package version; any change to it is recorded here explicitly. +## 1.0.1.0++* Add the geometry-only `delaunayGeometry` entrance and re-export+  `delaunayFromCoordinates` with its duplicate-payload policy from the main+  facade.+* Add one versioned C ABI over immutable geometry meshes, with typed+  obstruction witnesses and thin Python, TypeScript, and Rust bindings.+ ## 1.0.0.0  * Specialize the public geometry surface to binary64 and remove the ornamental
README.md view
@@ -34,6 +34,8 @@  | 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. |@@ -52,6 +54,15 @@ | 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. | +## 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/`](bindings/README.md)+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@@ -162,10 +173,9 @@  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))+  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]] @@ -188,10 +198,10 @@ `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 `Point`s 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+`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 —@@ -281,9 +291,11 @@ 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))+  mesh <-+    either+      (fail . show)+      pure+      (delaunayGeometry (fromList [Point x y | (x, y) <- pts]))   let corners f =         [(x, y) | v <- faceVertices mesh f, let Point x y = vertexPoint mesh v]       circumradius (ax, ay) (bx, by) (cx, cy) =
bench/join/Moonlight/Triangulation/JoinBench.hs view
@@ -428,7 +428,7 @@ dropAdjacentDuplicates rest = rest  geometryMesh :: [Point] -> IO Mesh-geometryMesh points = mapVertices (const ()) <$> siteMesh points+geometryMesh points = requireRight (delaunayGeometry (V.fromList points))  siteMesh :: [Point] -> IO SiteMesh siteMesh points =
+ bindings/README.md view
@@ -0,0 +1,55 @@+# Foreign bindings++`moonlight-triangulation.cabal` owns a private Haskell `ffi` sublibrary and the+`moonlight-triangulation-c` foreign library. Python, TypeScript, and Rust are+leaf bindings over that sole C ABI; none restates the engine or mesh.++From `compiler`, build and locate the shared library:++```bash+cabal build moonlight-triangulation:flib:moonlight-triangulation-c --enable-shared --project-file=cabal.project.triangulation-dev+cabal list-bin moonlight-triangulation:flib:moonlight-triangulation-c --enable-shared --project-file=cabal.project.triangulation-dev+```++Set `MOONLIGHT_TRIANGULATION_LIBRARY` to that `.dylib`, `.so`, or `.dll`; Rust+linking also reads its directory from `MOONLIGHT_TRIANGULATION_LIB_DIR`.++The ABI accepts interleaved binary64 coordinates and publishes opaque immutable+mesh handles. Set operations and batch insertion return new handles, leaving+inputs valid until freed. Dense vertices and triangles follow vertex-handle and+bounded-face-handle order. Every status-returning call accepts an optional+`ml_obstruction` distinguishing API misuse, capacity refusal, geometric+obstruction, and runtime failure, with retained index, value, coordinate, and+message witnesses. `UINT64_MAX` means no input index applies.++Batch related edits into one `insert_many` call; each call publishes a new mesh,+so repeated singleton calls pay repeated publication costs.++`ml_runtime_initialize` is idempotent and process-lifetime. There is no shutdown+call because GHC cannot reliably restart after its outermost `hs_exit`.++## Python++```python+from moonlight_triangulation import Moonlight+mesh = Moonlight().delaunay([(0, 0), (1, 0), (0, 1)])+print(mesh.vertices, mesh.triangles)+mesh.close()+```++## TypeScript++```typescript+import { Moonlight } from "@moonlight/triangulation";+const mesh = new Moonlight().delaunay([[0, 0], [1, 0], [0, 1]]);+console.log(mesh.vertices(), mesh.triangles());+mesh.close();+```++## Rust++```rust+use moonlight_triangulation::Moonlight;+let mesh = Moonlight::initialize()?.delaunay(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]])?;+println!("{:?} {:?}", mesh.vertices()?, mesh.triangles()?);+```
+ bindings/python/pyproject.toml view
@@ -0,0 +1,13 @@+[build-system]+requires = ["hatchling>=1.27,<2"]+build-backend = "hatchling.build"++[project]+name = "moonlight-triangulation"+version = "1.0.0"+description = "Python bindings for Moonlight's immutable Delaunay mesh algebra."+requires-python = ">=3.11"+license = { text = "MIT" }++[tool.hatch.build.targets.wheel]+packages = ["src/moonlight_triangulation"]
+ bindings/python/src/moonlight_triangulation/__init__.py view
@@ -0,0 +1,247 @@+from __future__ import annotations++import ctypes+import os+import weakref+from collections.abc import Callable, Sequence+from pathlib import Path+from typing import Final+++class _Obstruction(ctypes.Structure):+    _fields_ = [+        ("code", ctypes.c_uint32),+        ("coordinate_error", ctypes.c_uint32),+        ("input_index", ctypes.c_uint64),+        ("first_index", ctypes.c_uint64),+        ("second_index", ctypes.c_uint64),+        ("first_value", ctypes.c_double),+        ("second_value", ctypes.c_double),+        ("point_x", ctypes.c_double),+        ("point_y", ctypes.c_double),+        ("message", ctypes.c_char * 256),+    ]+++class MoonlightError(RuntimeError):+    def __init__(self, status: int, obstruction: _Obstruction) -> None:+        self.status = status+        self.code = obstruction.code+        self.coordinate_error = obstruction.coordinate_error+        self.input_index = obstruction.input_index+        self.first_index = obstruction.first_index+        self.second_index = obstruction.second_index+        self.first_value = obstruction.first_value+        self.second_value = obstruction.second_value+        self.point = (obstruction.point_x, obstruction.point_y)+        message = bytes(obstruction.message).split(b"\0", 1)[0].decode("utf-8", errors="replace")+        super().__init__(message or f"Moonlight ABI failure {status}:{self.code}")+++class _NativeApi:+    _OK: Final = 0++    def __init__(self, library_path: Path) -> None:+        library = ctypes.CDLL(str(library_path))+        mesh = ctypes.c_void_p+        mesh_output = ctypes.POINTER(mesh)+        obstruction = ctypes.POINTER(_Obstruction)+        count_output = ctypes.POINTER(ctypes.c_size_t)+        _configure(library, "ml_abi_version", ())+        _configure(library, "ml_runtime_initialize", ())+        _configure(library, "ml_delaunay_f64", (ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, mesh_output, obstruction))+        _configure(library, "ml_mesh_insert_many_f64", (mesh, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, mesh_output, obstruction))+        _configure(library, "ml_mesh_union", (mesh, mesh, mesh_output, obstruction))+        _configure(library, "ml_mesh_intersection", (mesh, mesh, mesh_output, obstruction))+        _configure(library, "ml_mesh_difference", (mesh, mesh, mesh_output, obstruction))+        _configure(library, "ml_mesh_symmetric_difference", (mesh, mesh, mesh_output, obstruction))+        _configure(library, "ml_mesh_vertex_count", (mesh, count_output, obstruction))+        _configure(library, "ml_mesh_triangle_count", (mesh, count_output, obstruction))+        _configure(library, "ml_mesh_copy_vertices_f64", (mesh, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, count_output, obstruction))+        _configure(library, "ml_mesh_copy_triangles_u32", (mesh, ctypes.POINTER(ctypes.c_uint32), ctypes.c_size_t, count_output, obstruction))+        _configure(library, "ml_mesh_free", (mesh,), None)++        status = int(library.ml_runtime_initialize())+        if status != self._OK:+            raise RuntimeError(f"Moonlight runtime initialization failed with status {status}")+        abi_version = int(library.ml_abi_version())+        if abi_version != 1:+            raise RuntimeError(f"unsupported Moonlight ABI version {abi_version}")+        self.library = library++    def check(self, status: int, obstruction: _Obstruction) -> None:+        if status != self._OK:+            raise MoonlightError(status, obstruction)+++Point = tuple[float, float]+Triangle = tuple[int, int, int]+_BinaryNativeOperation = Callable[[ctypes.c_void_p, ctypes.c_void_p, object, object], int]+++class Moonlight:+    def __init__(self, library_path: str | os.PathLike[str] | None = None) -> None:+        configured_path = library_path or os.environ.get("MOONLIGHT_TRIANGULATION_LIBRARY")+        if configured_path is None:+            raise ValueError("set MOONLIGHT_TRIANGULATION_LIBRARY or pass library_path")+        self._native = _NativeApi(Path(configured_path))++    def delaunay(self, points: Sequence[Point]) -> Mesh:+        coordinates, pointer = _coordinate_buffer(points)+        output = ctypes.c_void_p()+        obstruction = _Obstruction()+        status = int(+            self._native.library.ml_delaunay_f64(+                pointer,+                len(points),+                ctypes.byref(output),+                ctypes.byref(obstruction),+            )+        )+        self._native.check(status, obstruction)+        return Mesh(self._native, _required_handle(output))+++class Mesh:+    __slots__ = ("_native", "_handle", "_finalizer", "__weakref__")++    def __init__(self, native: _NativeApi, handle: ctypes.c_void_p) -> None:+        self._native = native+        self._handle = handle+        self._finalizer = weakref.finalize(self, native.library.ml_mesh_free, handle)++    def close(self) -> None:+        self._finalizer()+        self._handle = ctypes.c_void_p()++    def __enter__(self) -> Mesh:+        return self++    def __exit__(self, _type: object, _value: object, _traceback: object) -> None:+        self.close()++    @property+    def vertex_count(self) -> int:+        return self._count(self._native.library.ml_mesh_vertex_count)++    @property+    def triangle_count(self) -> int:+        return self._count(self._native.library.ml_mesh_triangle_count)++    @property+    def vertices(self) -> tuple[Point, ...]:+        count = self.vertex_count+        output = (ctypes.c_double * (count * 2))()+        written = ctypes.c_size_t()+        obstruction = _Obstruction()+        status = int(+            self._native.library.ml_mesh_copy_vertices_f64(+                self._live_handle(),+                output,+                count,+                ctypes.byref(written),+                ctypes.byref(obstruction),+            )+        )+        self._native.check(status, obstruction)+        return tuple((float(output[index * 2]), float(output[index * 2 + 1])) for index in range(written.value))++    @property+    def triangles(self) -> tuple[Triangle, ...]:+        count = self.triangle_count+        output = (ctypes.c_uint32 * (count * 3))()+        written = ctypes.c_size_t()+        obstruction = _Obstruction()+        status = int(+            self._native.library.ml_mesh_copy_triangles_u32(+                self._live_handle(),+                output,+                count,+                ctypes.byref(written),+                ctypes.byref(obstruction),+            )+        )+        self._native.check(status, obstruction)+        return tuple(+            (int(output[index * 3]), int(output[index * 3 + 1]), int(output[index * 3 + 2]))+            for index in range(written.value)+        )++    def insert_many(self, points: Sequence[Point]) -> Mesh:+        coordinates, pointer = _coordinate_buffer(points)+        output = ctypes.c_void_p()+        obstruction = _Obstruction()+        status = int(+            self._native.library.ml_mesh_insert_many_f64(+                self._live_handle(),+                pointer,+                len(points),+                ctypes.byref(output),+                ctypes.byref(obstruction),+            )+        )+        self._native.check(status, obstruction)+        return Mesh(self._native, _required_handle(output))++    def union(self, other: Mesh) -> Mesh:+        return self._binary(other, self._native.library.ml_mesh_union)++    def intersection(self, other: Mesh) -> Mesh:+        return self._binary(other, self._native.library.ml_mesh_intersection)++    def difference(self, other: Mesh) -> Mesh:+        return self._binary(other, self._native.library.ml_mesh_difference)++    def symmetric_difference(self, other: Mesh) -> Mesh:+        return self._binary(other, self._native.library.ml_mesh_symmetric_difference)++    def _binary(self, other: Mesh, operation: _BinaryNativeOperation) -> Mesh:+        if self._native is not other._native:+            raise ValueError("both meshes must belong to the same Moonlight runtime")+        output = ctypes.c_void_p()+        obstruction = _Obstruction()+        status = int(+            operation(+                self._live_handle(),+                other._live_handle(),+                ctypes.byref(output),+                ctypes.byref(obstruction),+            )+        )+        self._native.check(status, obstruction)+        return Mesh(self._native, _required_handle(output))++    def _count(self, operation: Callable[[ctypes.c_void_p, object, object], int]) -> int:+        output = ctypes.c_size_t()+        obstruction = _Obstruction()+        status = int(operation(self._live_handle(), ctypes.byref(output), ctypes.byref(obstruction)))+        self._native.check(status, obstruction)+        return int(output.value)++    def _live_handle(self) -> ctypes.c_void_p:+        if not self._finalizer.alive:+            raise RuntimeError("mesh is closed")+        return self._handle+++def _coordinate_buffer(points: Sequence[Point]) -> tuple[object, object]:+    values = tuple(component for x, y in points for component in (x, y))+    if not values:+        return (), None+    coordinates = (ctypes.c_double * len(values))(*values)+    return coordinates, coordinates+++def _required_handle(handle: ctypes.c_void_p) -> ctypes.c_void_p:+    if not handle.value:+        raise RuntimeError("Moonlight returned success without a mesh handle")+    return handle+++def _configure(library: ctypes.CDLL, name: str, parameters: Sequence[object], result: object = ctypes.c_uint32) -> None:+    function = getattr(library, name)+    setattr(function, "argtypes", list(parameters))+    setattr(function, "restype", result)+++__all__ = ["Mesh", "Moonlight", "MoonlightError", "Point", "Triangle"]
+ bindings/python/tests/test_binding.py view
@@ -0,0 +1,54 @@+from __future__ import annotations++import math+import os+import unittest+from typing import ClassVar++from moonlight_triangulation import Moonlight, MoonlightError+++class MoonlightBindingTest(unittest.TestCase):+    engine: ClassVar[Moonlight]++    @classmethod+    def setUpClass(cls) -> None:+        cls.engine = Moonlight(os.environ["MOONLIGHT_TRIANGULATION_LIBRARY"])++    def test_immutable_mesh_algebra_and_dense_projection(self) -> None:+        left = self.engine.delaunay([(0, 0), (2, 0), (0, 2), (2, 2)])+        right = self.engine.delaunay([(2, 0), (4, 0), (2, 2), (4, 2)])+        union = left.union(right)+        intersection = left.intersection(right)+        difference = left.difference(right)+        symmetric = left.symmetric_difference(right)+        extended = left.insert_many([(1, 1), (3, 1)])+        self.addCleanup(left.close)+        self.addCleanup(right.close)+        self.addCleanup(union.close)+        self.addCleanup(intersection.close)+        self.addCleanup(difference.close)+        self.addCleanup(symmetric.close)+        self.addCleanup(extended.close)++        self.assertEqual(left.vertex_count, 4)+        self.assertEqual(union.vertex_count, 6)+        self.assertEqual(intersection.vertex_count, 2)+        self.assertEqual(difference.vertex_count, 2)+        self.assertEqual(symmetric.vertex_count, 4)+        self.assertEqual(extended.vertex_count, 6)+        self.assertEqual(len(left.vertices), left.vertex_count)+        self.assertEqual(len(left.triangles), left.triangle_count)+        self.assertTrue(all(max(triangle) < left.vertex_count for triangle in left.triangles))++    def test_invalid_coordinate_preserves_typed_witness(self) -> None:+        with self.assertRaises(MoonlightError) as raised:+            self.engine.delaunay([(0, 0), (math.nan, 1), (1, 0)])+        self.assertEqual(raised.exception.status, 4)+        self.assertEqual(raised.exception.code, 1)+        self.assertEqual(raised.exception.coordinate_error, 1)+        self.assertEqual(raised.exception.input_index, 1)+++if __name__ == "__main__":+    unittest.main()
+ bindings/rust/Cargo.toml view
@@ -0,0 +1,14 @@+[package]+name = "moonlight-triangulation"+version = "1.0.0"+edition = "2024"+license = "MIT"+description = "Rust bindings for Moonlight's immutable Delaunay mesh algebra."+repository = "https://github.com/PaleRoses/moonlight.git"+build = "build.rs"+links = "moonlight-triangulation-c"++[lib]+path = "src/lib.rs"++[workspace]
+ bindings/rust/build.rs view
@@ -0,0 +1,11 @@+use std::env;++fn main() {+    println!("cargo:rerun-if-env-changed=MOONLIGHT_TRIANGULATION_LIB_DIR");+    if let Some(directory) = env::var_os("MOONLIGHT_TRIANGULATION_LIB_DIR") {+        println!(+            "cargo:rustc-link-search=native={}",+            directory.to_string_lossy()+        );+    }+}
+ bindings/rust/src/lib.rs view
@@ -0,0 +1,373 @@+use std::error::Error;+use std::ffi::{c_char, c_double, c_uint};+use std::fmt::{Display, Formatter};+use std::marker::PhantomData;+use std::ptr::NonNull;+use std::rc::Rc;++const STATUS_OK: u32 = 0;+const ABI_VERSION: u32 = 1;++#[repr(C)]+struct NativeMesh {+    _private: [u8; 0],+}++#[repr(C)]+struct NativeObstruction {+    code: u32,+    coordinate_error: u32,+    input_index: u64,+    first_index: u64,+    second_index: u64,+    first_value: f64,+    second_value: f64,+    point_x: f64,+    point_y: f64,+    message: [c_char; 256],+}++impl Default for NativeObstruction {+    fn default() -> Self {+        Self {+            code: 0,+            coordinate_error: 0,+            input_index: u64::MAX,+            first_index: 0,+            second_index: 0,+            first_value: 0.0,+            second_value: 0.0,+            point_x: 0.0,+            point_y: 0.0,+            message: [0; 256],+        }+    }+}++#[link(name = "moonlight-triangulation-c")]+unsafe extern "C" {+    fn ml_abi_version() -> c_uint;+    fn ml_runtime_initialize() -> c_uint;+    fn ml_delaunay_f64(+        coordinates: *const c_double,+        point_count: usize,+        result: *mut *mut NativeMesh,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_insert_many_f64(+        mesh: *const NativeMesh,+        coordinates: *const c_double,+        point_count: usize,+        result: *mut *mut NativeMesh,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_union(+        left: *const NativeMesh,+        right: *const NativeMesh,+        result: *mut *mut NativeMesh,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_intersection(+        left: *const NativeMesh,+        right: *const NativeMesh,+        result: *mut *mut NativeMesh,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_difference(+        left: *const NativeMesh,+        right: *const NativeMesh,+        result: *mut *mut NativeMesh,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_symmetric_difference(+        left: *const NativeMesh,+        right: *const NativeMesh,+        result: *mut *mut NativeMesh,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_vertex_count(+        mesh: *const NativeMesh,+        count: *mut usize,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_triangle_count(+        mesh: *const NativeMesh,+        count: *mut usize,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_copy_vertices_f64(+        mesh: *const NativeMesh,+        coordinates: *mut c_double,+        point_capacity: usize,+        points_written: *mut usize,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_copy_triangles_u32(+        mesh: *const NativeMesh,+        triangles: *mut u32,+        triangle_capacity: usize,+        triangles_written: *mut usize,+        obstruction: *mut NativeObstruction,+    ) -> c_uint;+    fn ml_mesh_free(mesh: *mut NativeMesh);+}++type BinaryMeshOperation = unsafe extern "C" fn(+    *const NativeMesh,+    *const NativeMesh,+    *mut *mut NativeMesh,+    *mut NativeObstruction,+) -> c_uint;++#[derive(Debug, Clone, PartialEq)]+pub struct MoonlightError {+    pub status: u32,+    pub code: u32,+    pub coordinate_error: u32,+    pub input_index: u64,+    pub first_index: u64,+    pub second_index: u64,+    pub first_value: f64,+    pub second_value: f64,+    pub point: [f64; 2],+    pub message: String,+}++impl Display for MoonlightError {+    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {+        write!(formatter, "{}", self.message)+    }+}++impl Error for MoonlightError {}++pub struct Moonlight;++impl Moonlight {+    pub fn initialize() -> Result<Self, MoonlightError> {+        let status = unsafe { ml_runtime_initialize() };+        if status != STATUS_OK {+            return Err(synthetic_error(+                status,+                "Moonlight runtime initialization failed",+                0,+                0,+            ));+        }+        let version = unsafe { ml_abi_version() };+        if version != ABI_VERSION {+            return Err(synthetic_error(+                5,+                format!("unsupported Moonlight ABI version {version}"),+                u64::from(version),+                u64::from(ABI_VERSION),+            ));+        }+        Ok(Self)+    }++    pub fn delaunay(&self, points: &[[f64; 2]]) -> Result<Mesh, MoonlightError> {+        let coordinates = flatten_points(points);+        create_mesh(|output, obstruction| unsafe {+            ml_delaunay_f64(coordinates.as_ptr(), points.len(), output, obstruction)+        })+    }+}++pub struct Mesh {+    handle: NonNull<NativeMesh>,+    _thread_affinity: PhantomData<Rc<()>>,+}++impl Mesh {+    pub fn vertex_count(&self) -> Result<usize, MoonlightError> {+        self.count(ml_mesh_vertex_count)+    }++    pub fn triangle_count(&self) -> Result<usize, MoonlightError> {+        self.count(ml_mesh_triangle_count)+    }++    pub fn vertices(&self) -> Result<Vec<[f64; 2]>, MoonlightError> {+        let count = self.vertex_count()?;+        let mut coordinates = vec![0.0; count * 2];+        let mut written = 0;+        let mut obstruction = NativeObstruction::default();+        let status = unsafe {+            ml_mesh_copy_vertices_f64(+                self.handle.as_ptr(),+                coordinates.as_mut_ptr(),+                count,+                &mut written,+                &mut obstruction,+            )+        };+        status_result(status, obstruction)?;+        coordinates.truncate(written * 2);+        Ok(coordinates+            .chunks_exact(2)+            .map(|point| [point[0], point[1]])+            .collect())+    }++    pub fn triangles(&self) -> Result<Vec<[u32; 3]>, MoonlightError> {+        let count = self.triangle_count()?;+        let mut triangles = vec![0; count * 3];+        let mut written = 0;+        let mut obstruction = NativeObstruction::default();+        let status = unsafe {+            ml_mesh_copy_triangles_u32(+                self.handle.as_ptr(),+                triangles.as_mut_ptr(),+                count,+                &mut written,+                &mut obstruction,+            )+        };+        status_result(status, obstruction)?;+        triangles.truncate(written * 3);+        Ok(triangles+            .chunks_exact(3)+            .map(|triangle| [triangle[0], triangle[1], triangle[2]])+            .collect())+    }++    pub fn insert_many(&self, points: &[[f64; 2]]) -> Result<Self, MoonlightError> {+        let coordinates = flatten_points(points);+        create_mesh(|output, obstruction| unsafe {+            ml_mesh_insert_many_f64(+                self.handle.as_ptr(),+                coordinates.as_ptr(),+                points.len(),+                output,+                obstruction,+            )+        })+    }++    pub fn union(&self, other: &Self) -> Result<Self, MoonlightError> {+        self.binary(other, ml_mesh_union)+    }++    pub fn intersection(&self, other: &Self) -> Result<Self, MoonlightError> {+        self.binary(other, ml_mesh_intersection)+    }++    pub fn difference(&self, other: &Self) -> Result<Self, MoonlightError> {+        self.binary(other, ml_mesh_difference)+    }++    pub fn symmetric_difference(&self, other: &Self) -> Result<Self, MoonlightError> {+        self.binary(other, ml_mesh_symmetric_difference)+    }++    fn binary(&self, other: &Self, operation: BinaryMeshOperation) -> Result<Self, MoonlightError> {+        create_mesh(|output, obstruction| unsafe {+            operation(+                self.handle.as_ptr(),+                other.handle.as_ptr(),+                output,+                obstruction,+            )+        })+    }++    fn count(+        &self,+        operation: unsafe extern "C" fn(+            *const NativeMesh,+            *mut usize,+            *mut NativeObstruction,+        ) -> c_uint,+    ) -> Result<usize, MoonlightError> {+        let mut count = 0;+        let mut obstruction = NativeObstruction::default();+        let status = unsafe { operation(self.handle.as_ptr(), &mut count, &mut obstruction) };+        status_result(status, obstruction)?;+        Ok(count)+    }+}++impl Drop for Mesh {+    fn drop(&mut self) {+        unsafe { ml_mesh_free(self.handle.as_ptr()) }+    }+}++fn flatten_points(points: &[[f64; 2]]) -> Vec<f64> {+    points.iter().flat_map(|[x, y]| [*x, *y]).collect()+}++#[inline]+fn create_mesh(+    operation: impl FnOnce(*mut *mut NativeMesh, *mut NativeObstruction) -> u32,+) -> Result<Mesh, MoonlightError> {+    let mut output = std::ptr::null_mut();+    let mut obstruction = NativeObstruction::default();+    let status = operation(&mut output, &mut obstruction);+    status_result(status, obstruction)?;+    NonNull::new(output)+        .map(|handle| Mesh {+            handle,+            _thread_affinity: PhantomData,+        })+        .ok_or_else(success_without_handle_error)+}++fn status_result(status: u32, obstruction: NativeObstruction) -> Result<(), MoonlightError> {+    if status == STATUS_OK {+        Ok(())+    } else {+        Err(MoonlightError::from_native(status, obstruction))+    }+}++impl MoonlightError {+    fn from_native(status: u32, obstruction: NativeObstruction) -> Self {+        let message_end = obstruction+            .message+            .iter()+            .position(|character| *character == 0)+            .unwrap_or(obstruction.message.len());+        let message_bytes = obstruction.message[..message_end]+            .iter()+            .map(|character| *character as u8)+            .collect::<Vec<_>>();+        Self {+            status,+            code: obstruction.code,+            coordinate_error: obstruction.coordinate_error,+            input_index: obstruction.input_index,+            first_index: obstruction.first_index,+            second_index: obstruction.second_index,+            first_value: obstruction.first_value,+            second_value: obstruction.second_value,+            point: [obstruction.point_x, obstruction.point_y],+            message: String::from_utf8_lossy(&message_bytes).into_owned(),+        }+    }+}++fn success_without_handle_error() -> MoonlightError {+    synthetic_error(5, "Moonlight returned success without a mesh handle", 0, 0)+}++fn synthetic_error(+    status: u32,+    message: impl Into<String>,+    first_index: u64,+    second_index: u64,+) -> MoonlightError {+    MoonlightError {+        status,+        code: 103,+        coordinate_error: 0,+        input_index: u64::MAX,+        first_index,+        second_index,+        first_value: 0.0,+        second_value: 0.0,+        point: [0.0, 0.0],+        message: message.into(),+    }+}
+ bindings/rust/tests/binding.rs view
@@ -0,0 +1,49 @@+use moonlight_triangulation::Moonlight;++#[test]+fn immutable_mesh_algebra_and_dense_projection() {+    let engine = Moonlight::initialize().expect("runtime");+    let left = engine+        .delaunay(&[[0.0, 0.0], [2.0, 0.0], [0.0, 2.0], [2.0, 2.0]])+        .expect("left mesh");+    let right = engine+        .delaunay(&[[2.0, 0.0], [4.0, 0.0], [2.0, 2.0], [4.0, 2.0]])+        .expect("right mesh");+    let union = left.union(&right).expect("union");+    let intersection = left.intersection(&right).expect("intersection");+    let difference = left.difference(&right).expect("difference");+    let symmetric = left+        .symmetric_difference(&right)+        .expect("symmetric difference");+    let extended = left+        .insert_many(&[[1.0, 1.0], [3.0, 1.0]])+        .expect("batch insertion");++    assert_eq!(left.vertex_count().expect("left count"), 4);+    assert_eq!(union.vertex_count().expect("union count"), 6);+    assert_eq!(intersection.vertex_count().expect("intersection count"), 2);+    assert_eq!(difference.vertex_count().expect("difference count"), 2);+    assert_eq!(symmetric.vertex_count().expect("symmetric count"), 4);+    assert_eq!(extended.vertex_count().expect("extended count"), 6);+    assert_eq!(+        left.vertices().expect("vertices").len(),+        left.vertex_count().expect("vertex count")+    );+    assert_eq!(+        left.triangles().expect("triangles").len(),+        left.triangle_count().expect("triangle count")+    );+}++#[test]+fn invalid_coordinate_preserves_typed_witness() {+    let engine = Moonlight::initialize().expect("runtime");+    let failure = engine+        .delaunay(&[[0.0, 0.0], [f64::NAN, 1.0], [1.0, 0.0]])+        .err()+        .expect("invalid coordinate refusal");+    assert_eq!(failure.status, 4);+    assert_eq!(failure.code, 1);+    assert_eq!(failure.coordinate_error, 1);+    assert_eq!(failure.input_index, 1);+}
+ bindings/typescript/package.json view
@@ -0,0 +1,33 @@+{+  "name": "@moonlight/triangulation",+  "version": "1.0.0",+  "description": "TypeScript bindings for Moonlight's immutable Delaunay mesh algebra.",+  "type": "module",+  "main": "./dist/src/index.js",+  "types": "./dist/src/index.d.ts",+  "exports": {+    ".": {+      "types": "./dist/src/index.d.ts",+      "import": "./dist/src/index.js"+    }+  },+  "files": [+    "dist/src"+  ],+  "scripts": {+    "build": "tsc -p tsconfig.json",+    "test": "node --test dist/test/binding.test.js"+  },+  "engines": {+    "node": ">=20"+  },+  "dependencies": {+    "koffi": "3.1.4"+  },+  "devDependencies": {+    "@types/node": "26.2.0",+    "typescript": "7.0.2"+  },+  "license": "MIT",+  "packageManager": "pnpm@11.21.0"+}
+ bindings/typescript/pnpm-workspace.yaml view
@@ -0,0 +1,5 @@+packages:+  - .++allowBuilds:+  koffi: true
+ bindings/typescript/src/index.ts view
@@ -0,0 +1,280 @@+import koffi from "koffi";++const STATUS_OK = 0;+const ABI_VERSION = 1;++export type Point = readonly [x: number, y: number];+export type Triangle = readonly [first: number, second: number, third: number];++type MeshHandle = object;+type NativeInteger = number | bigint;++interface NativeObstruction {+  code?: number;+  coordinate_error?: number;+  input_index?: NativeInteger;+  first_index?: NativeInteger;+  second_index?: NativeInteger;+  first_value?: number;+  second_value?: number;+  point_x?: number;+  point_y?: number;+  message?: string | readonly number[];+}++type BuildOperation = (coordinates: Float64Array, pointCount: number, output: Array<MeshHandle | null>, obstruction: NativeObstruction) => number;+type InsertOperation = (mesh: MeshHandle, coordinates: Float64Array, pointCount: number, output: Array<MeshHandle | null>, obstruction: NativeObstruction) => number;+type BinaryOperation = (left: MeshHandle, right: MeshHandle, output: Array<MeshHandle | null>, obstruction: NativeObstruction) => number;+type CountOperation = (mesh: MeshHandle, output: number[], obstruction: NativeObstruction) => number;++interface NativeApi {+  readonly abiVersion: () => number;+  readonly runtimeInitialize: () => number;+  readonly delaunay: BuildOperation;+  readonly insertMany: InsertOperation;+  readonly union: BinaryOperation;+  readonly intersection: BinaryOperation;+  readonly difference: BinaryOperation;+  readonly symmetricDifference: BinaryOperation;+  readonly vertexCount: CountOperation;+  readonly triangleCount: CountOperation;+  readonly copyVertices: (mesh: MeshHandle, coordinates: Float64Array, capacity: number, written: number[], obstruction: NativeObstruction) => number;+  readonly copyTriangles: (mesh: MeshHandle, triangles: Uint32Array, capacity: number, written: number[], obstruction: NativeObstruction) => number;+  readonly free: (mesh: MeshHandle) => void;+}++interface FinalizerState {+  readonly free: (mesh: MeshHandle) => void;+  readonly handle: MeshHandle;+}++const meshFinalizer = new FinalizationRegistry<FinalizerState>(({ free, handle }) => free(handle));++export class MoonlightError extends Error {+  readonly status: number;+  readonly code: number;+  readonly coordinateError: number;+  readonly inputIndex: bigint;+  readonly firstIndex: bigint;+  readonly secondIndex: bigint;+  readonly firstValue: number;+  readonly secondValue: number;+  readonly point: Point;++  constructor(status: number, obstruction: NativeObstruction) {+    super(obstructionMessage(obstruction));+    this.name = "MoonlightError";+    this.status = status;+    this.code = obstruction.code ?? 0;+    this.coordinateError = obstruction.coordinate_error ?? 0;+    this.inputIndex = toBigInt(obstruction.input_index);+    this.firstIndex = toBigInt(obstruction.first_index);+    this.secondIndex = toBigInt(obstruction.second_index);+    this.firstValue = obstruction.first_value ?? 0;+    this.secondValue = obstruction.second_value ?? 0;+    this.point = [obstruction.point_x ?? 0, obstruction.point_y ?? 0];+  }+}++export class Moonlight {+  readonly #native: NativeApi;++  constructor(libraryPath = process.env.MOONLIGHT_TRIANGULATION_LIBRARY) {+    if (libraryPath === undefined || libraryPath.length === 0) {+      throw new Error("set MOONLIGHT_TRIANGULATION_LIBRARY or pass libraryPath");+    }+    const native = createNativeApi(libraryPath);+    const initializationStatus = native.runtimeInitialize();+    if (initializationStatus !== STATUS_OK) {+      throw new Error(`Moonlight runtime initialization failed with status ${initializationStatus}`);+    }+    const abiVersion = native.abiVersion();+    if (abiVersion !== ABI_VERSION) {+      throw new Error(`unsupported Moonlight ABI version ${abiVersion}`);+    }+    this.#native = native;+  }++  delaunay(points: readonly Point[]): Mesh {+    const output: Array<MeshHandle | null> = [null];+    const obstruction: NativeObstruction = {};+    const status = this.#native.delaunay(flattenPoints(points), points.length, output, obstruction);+    checkStatus(status, obstruction);+    return new Mesh(this.#native, requiredHandle(output[0]));+  }+}++export class Mesh {+  readonly #native: NativeApi;+  #handle: MeshHandle | null;++  constructor(native: NativeApi, handle: MeshHandle) {+    this.#native = native;+    this.#handle = handle;+    meshFinalizer.register(this, { free: native.free, handle }, this);+  }++  close(): void {+    const handle = this.#handle;+    if (handle !== null) {+      meshFinalizer.unregister(this);+      this.#native.free(handle);+      this.#handle = null;+    }+  }++  vertexCount(): number {+    return this.#count(this.#native.vertexCount);+  }++  triangleCount(): number {+    return this.#count(this.#native.triangleCount);+  }++  vertices(): readonly Point[] {+    const count = this.vertexCount();+    const coordinates = new Float64Array(count * 2);+    const written = [0];+    const obstruction: NativeObstruction = {};+    const status = this.#native.copyVertices(this.#liveHandle(), coordinates, count, written, obstruction);+    checkStatus(status, obstruction);+    return Array.from({ length: written[0] ?? 0 }, (_unused, index): Point => [+      coordinates[index * 2] ?? 0,+      coordinates[index * 2 + 1] ?? 0,+    ]);+  }++  triangles(): readonly Triangle[] {+    const count = this.triangleCount();+    const triangles = new Uint32Array(count * 3);+    const written = [0];+    const obstruction: NativeObstruction = {};+    const status = this.#native.copyTriangles(this.#liveHandle(), triangles, count, written, obstruction);+    checkStatus(status, obstruction);+    return Array.from({ length: written[0] ?? 0 }, (_unused, index): Triangle => [+      triangles[index * 3] ?? 0,+      triangles[index * 3 + 1] ?? 0,+      triangles[index * 3 + 2] ?? 0,+    ]);+  }++  insertMany(points: readonly Point[]): Mesh {+    const output: Array<MeshHandle | null> = [null];+    const obstruction: NativeObstruction = {};+    const status = this.#native.insertMany(this.#liveHandle(), flattenPoints(points), points.length, output, obstruction);+    checkStatus(status, obstruction);+    return new Mesh(this.#native, requiredHandle(output[0]));+  }++  union(other: Mesh): Mesh {+    return this.#binary(other, this.#native.union);+  }++  intersection(other: Mesh): Mesh {+    return this.#binary(other, this.#native.intersection);+  }++  difference(other: Mesh): Mesh {+    return this.#binary(other, this.#native.difference);+  }++  symmetricDifference(other: Mesh): Mesh {+    return this.#binary(other, this.#native.symmetricDifference);+  }++  #binary(other: Mesh, operation: BinaryOperation): Mesh {+    if (this.#native !== other.#native) {+      throw new Error("both meshes must belong to the same Moonlight runtime");+    }+    const output: Array<MeshHandle | null> = [null];+    const obstruction: NativeObstruction = {};+    const status = operation(this.#liveHandle(), other.#liveHandle(), output, obstruction);+    checkStatus(status, obstruction);+    return new Mesh(this.#native, requiredHandle(output[0]));+  }++  #count(operation: CountOperation): number {+    const output = [0];+    const obstruction: NativeObstruction = {};+    const status = operation(this.#liveHandle(), output, obstruction);+    checkStatus(status, obstruction);+    return output[0] ?? 0;+  }++  #liveHandle(): MeshHandle {+    if (this.#handle === null) {+      throw new Error("mesh is closed");+    }+    return this.#handle;+  }+}++function createNativeApi(libraryPath: string): NativeApi {+  const library = koffi.load(libraryPath);+  const mesh = koffi.opaque();+  const meshPointer = koffi.pointer(mesh);+  const meshOutput = koffi.out(koffi.pointer(mesh, 2));+  const obstruction = koffi.struct({+    code: "uint32_t",+    coordinate_error: "uint32_t",+    input_index: "uint64_t",+    first_index: "uint64_t",+    second_index: "uint64_t",+    first_value: "double",+    second_value: "double",+    point_x: "double",+    point_y: "double",+    message: koffi.array("char", 256),+  });+  const obstructionOutput = koffi.out(koffi.pointer(obstruction));+  const meshCountOutput = koffi.out(koffi.pointer("size_t"));+  return {+    abiVersion: library.func("ml_abi_version", "uint32_t", []),+    runtimeInitialize: library.func("ml_runtime_initialize", "uint32_t", []),+    delaunay: library.func("ml_delaunay_f64", "uint32_t", [koffi.pointer("double"), "size_t", meshOutput, obstructionOutput]),+    insertMany: library.func("ml_mesh_insert_many_f64", "uint32_t", [meshPointer, koffi.pointer("double"), "size_t", meshOutput, obstructionOutput]),+    union: library.func("ml_mesh_union", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),+    intersection: library.func("ml_mesh_intersection", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),+    difference: library.func("ml_mesh_difference", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),+    symmetricDifference: library.func("ml_mesh_symmetric_difference", "uint32_t", [meshPointer, meshPointer, meshOutput, obstructionOutput]),+    vertexCount: library.func("ml_mesh_vertex_count", "uint32_t", [meshPointer, meshCountOutput, obstructionOutput]),+    triangleCount: library.func("ml_mesh_triangle_count", "uint32_t", [meshPointer, meshCountOutput, obstructionOutput]),+    copyVertices: library.func("ml_mesh_copy_vertices_f64", "uint32_t", [meshPointer, koffi.out(koffi.pointer("double")), "size_t", meshCountOutput, obstructionOutput]),+    copyTriangles: library.func("ml_mesh_copy_triangles_u32", "uint32_t", [meshPointer, koffi.out(koffi.pointer("uint32_t")), "size_t", meshCountOutput, obstructionOutput]),+    free: library.func("ml_mesh_free", "void", [meshPointer]),+  };+}++function flattenPoints(points: readonly Point[]): Float64Array {+  return Float64Array.from(points.flatMap(([x, y]) => [x, y]));+}++function checkStatus(status: number, obstruction: NativeObstruction): void {+  if (status !== STATUS_OK) {+    throw new MoonlightError(status, obstruction);+  }+}++function requiredHandle(handle: MeshHandle | null | undefined): MeshHandle {+  if (handle === null || handle === undefined) {+    throw new Error("Moonlight returned success without a mesh handle");+  }+  return handle;+}++function toBigInt(value: NativeInteger | undefined): bigint {+  return value === undefined ? 0n : BigInt(value);+}++function obstructionMessage(obstruction: NativeObstruction): string {+  const message = obstruction.message;+  if (message === undefined) {+    return `Moonlight ABI failure ${obstruction.code ?? 0}`;+  }+  if (typeof message === "string") {+    return message.split("\0", 1)[0] ?? "";+  }+  const terminator = message.indexOf(0);+  const length = terminator === -1 ? message.length : terminator;+  return new TextDecoder().decode(Uint8Array.from(message.slice(0, length)));+}
+ bindings/typescript/test/binding.test.ts view
@@ -0,0 +1,42 @@+import assert from "node:assert/strict";+import test from "node:test";++import { Moonlight, MoonlightError } from "../src/index.js";++test("immutable mesh algebra and dense projection", () => {+  const engine = new Moonlight();+  const left = engine.delaunay([[0, 0], [2, 0], [0, 2], [2, 2]]);+  const right = engine.delaunay([[2, 0], [4, 0], [2, 2], [4, 2]]);+  const meshes = [+    left,+    right,+    left.union(right),+    left.intersection(right),+    left.difference(right),+    left.symmetricDifference(right),+    left.insertMany([[1, 1], [3, 1]]),+  ];+  const [original, _right, union, intersection, difference, symmetric, extended] = meshes;+  assert.equal(original?.vertexCount(), 4);+  assert.equal(union?.vertexCount(), 6);+  assert.equal(intersection?.vertexCount(), 2);+  assert.equal(difference?.vertexCount(), 2);+  assert.equal(symmetric?.vertexCount(), 4);+  assert.equal(extended?.vertexCount(), 6);+  assert.equal(original?.vertices().length, original?.vertexCount());+  assert.equal(original?.triangles().length, original?.triangleCount());+  meshes.forEach((mesh) => mesh.close());+});++test("invalid coordinate preserves typed witness", () => {+  const engine = new Moonlight();+  assert.throws(+    () => engine.delaunay([[0, 0], [Number.NaN, 1], [1, 0]]),+    (failure: unknown) =>+      failure instanceof MoonlightError &&+      failure.status === 4 &&+      failure.code === 1 &&+      failure.coordinateError === 1 &&+      failure.inputIndex === 1n,+  );+});
+ bindings/typescript/tsconfig.json view
@@ -0,0 +1,23 @@+{+  "compilerOptions": {+    "target": "ES2023",+    "module": "NodeNext",+    "moduleResolution": "NodeNext",+    "strict": true,+    "noImplicitAny": true,+    "strictNullChecks": true,+    "noUncheckedIndexedAccess": true,+    "exactOptionalPropertyTypes": true,+    "types": [+      "node"+    ],+    "declaration": true,+    "rootDir": ".",+    "outDir": "dist",+    "skipLibCheck": false+  },+  "include": [+    "src/**/*.ts",+    "test/**/*.ts"+  ]+}
+ cbits/moonlight-triangulation.def view
@@ -0,0 +1,15 @@+LIBRARY moonlight-triangulation-c+EXPORTS+  ml_abi_version+  ml_runtime_initialize+  ml_delaunay_f64+  ml_mesh_insert_many_f64+  ml_mesh_union+  ml_mesh_intersection+  ml_mesh_difference+  ml_mesh_symmetric_difference+  ml_mesh_vertex_count+  ml_mesh_triangle_count+  ml_mesh_copy_vertices_f64+  ml_mesh_copy_triangles_u32+  ml_mesh_free
+ cbits/moonlight_runtime.c view
@@ -0,0 +1,48 @@+#include "moonlight_triangulation.h"+#include "HsFFI.h"++#if defined(_WIN32)+#include <windows.h>++static INIT_ONCE moonlight_runtime_once = INIT_ONCE_STATIC_INIT;++static BOOL CALLBACK moonlight_initialize_runtime(PINIT_ONCE once, PVOID parameter, PVOID *context) {+  int argc = 1;+  char program_name[] = "moonlight-triangulation";+  char *argv[] = {program_name, NULL};+  char **argv_pointer = argv;+  (void)once;+  (void)parameter;+  (void)context;+  hs_init(&argc, &argv_pointer);+  return TRUE;+}++ML_API ml_status ml_runtime_initialize(void) {+  return InitOnceExecuteOnce(&moonlight_runtime_once, moonlight_initialize_runtime, NULL, NULL)+    ? ML_STATUS_OK+    : ML_STATUS_RUNTIME_FAILURE;+}+#else+#include <pthread.h>++static pthread_once_t moonlight_runtime_once = PTHREAD_ONCE_INIT;++static void moonlight_initialize_runtime(void) {+  int argc = 1;+  char program_name[] = "moonlight-triangulation";+  char *argv[] = {program_name, NULL};+  char **argv_pointer = argv;+  hs_init(&argc, &argv_pointer);+}++ML_API ml_status ml_runtime_initialize(void) {+  return pthread_once(&moonlight_runtime_once, moonlight_initialize_runtime) == 0+    ? ML_STATUS_OK+    : ML_STATUS_RUNTIME_FAILURE;+}+#endif++ML_API uint32_t ml_abi_version(void) {+  return 1;+}
+ include/moonlight_triangulation.h view
@@ -0,0 +1,132 @@+#ifndef MOONLIGHT_TRIANGULATION_H+#define MOONLIGHT_TRIANGULATION_H++#include <stddef.h>+#include <stdint.h>++#if defined(_WIN32)+#define ML_API __declspec(dllexport)+#else+#define ML_API __attribute__((visibility("default")))+#endif++#ifdef __cplusplus+extern "C" {+#endif++typedef struct ml_mesh ml_mesh;+typedef uint32_t ml_status;+typedef uint32_t ml_obstruction_code;+typedef uint32_t ml_coordinate_error;++enum {+  ML_STATUS_OK = 0,+  ML_STATUS_NULL_POINTER = 1,+  ML_STATUS_COUNT_OVERFLOW = 2,+  ML_STATUS_BUFFER_TOO_SMALL = 3,+  ML_STATUS_BUILD_OBSTRUCTION = 4,+  ML_STATUS_RUNTIME_FAILURE = 5+};++enum {+  ML_OBSTRUCTION_NONE = 0,+  ML_OBSTRUCTION_INVALID_COORDINATE = 1,+  ML_OBSTRUCTION_POINT_LOCATION_FAILED = 2,+  ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED = 3,+  ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID = 4,+  ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX = 5,+  ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING = 6,+  ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING = 7,+  ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING = 8,+  ML_OBSTRUCTION_HULL_START_NOT_VISIBLE = 9,+  ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE = 10,+  ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE = 11,+  ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED = 12,+  ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE = 13,+  ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE = 14,+  ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE = 15,+  ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE = 16,+  ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION = 17,+  ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH = 18,+  ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH = 19,+  ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT = 20,+  ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH = 21,+  ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE = 22,+  ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY = 23,+  ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE = 24,+  ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH = 25,+  ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH = 26,+  ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED = 27,+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE = 28,+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE = 29,+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE = 30,+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE = 31,+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE = 32,+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE = 33,+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE = 34,+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE = 35,+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE = 36,+  ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE = 37,+  ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM = 38,+  ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE = 39,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE = 40,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING = 41,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS = 42,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED = 43,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION = 44,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION = 45,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION = 46,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE = 47,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE = 48,+  ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED = 49,+  ML_OBSTRUCTION_CAPACITY_EXCEEDED = 50,+  ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED = 51,+  ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED = 52,+  ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE = 53,+  ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH = 54,+  ML_OBSTRUCTION_NULL_POINTER = 100,+  ML_OBSTRUCTION_COUNT_OVERFLOW = 101,+  ML_OBSTRUCTION_BUFFER_TOO_SMALL = 102,+  ML_OBSTRUCTION_RUNTIME_FAILURE = 103+};++enum {+  ML_COORDINATE_ERROR_NONE = 0,+  ML_COORDINATE_ERROR_NAN = 1,+  ML_COORDINATE_ERROR_INFINITE = 2,+  ML_COORDINATE_ERROR_TOO_SMALL = 3,+  ML_COORDINATE_ERROR_TOO_LARGE = 4+};++typedef struct ml_obstruction {+  uint32_t code;+  uint32_t coordinate_error;+  uint64_t input_index;+  uint64_t first_index;+  uint64_t second_index;+  double first_value;+  double second_value;+  double point_x;+  double point_y;+  char message[256];+} ml_obstruction;++ML_API uint32_t ml_abi_version(void);+ML_API ml_status ml_runtime_initialize(void);+ML_API ml_status ml_delaunay_f64(const double *coordinates, size_t point_count, ml_mesh **result, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_insert_many_f64(const ml_mesh *mesh, const double *coordinates, size_t point_count, ml_mesh **result, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_union(const ml_mesh *left, const ml_mesh *right, ml_mesh **result, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_intersection(const ml_mesh *left, const ml_mesh *right, ml_mesh **result, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_difference(const ml_mesh *left, const ml_mesh *right, ml_mesh **result, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_symmetric_difference(const ml_mesh *left, const ml_mesh *right, ml_mesh **result, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_vertex_count(const ml_mesh *mesh, size_t *count, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_triangle_count(const ml_mesh *mesh, size_t *count, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_copy_vertices_f64(const ml_mesh *mesh, double *coordinates, size_t point_capacity, size_t *points_written, ml_obstruction *obstruction);+ML_API ml_status ml_mesh_copy_triangles_u32(const ml_mesh *mesh, uint32_t *triangles, size_t triangle_capacity, size_t *triangles_written, ml_obstruction *obstruction);+ML_API void ml_mesh_free(ml_mesh *mesh);++#ifdef __cplusplus+}+#endif++#endif
moonlight-triangulation.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.4 name:                moonlight-triangulation-version:             1.0.0.0+version:             1.0.1.0 synopsis:            Delaunay triangulations as a lawful finite-set algebra. description:         Delaunay and constrained Delaunay triangulation as a lawful                      finite-set algebra: a mesh is a value of its site set, so@@ -34,6 +34,20 @@   CHANGELOG.md extra-source-files:   weeder.toml+  include/moonlight_triangulation.h+  bindings/README.md+  bindings/python/pyproject.toml+  bindings/python/src/moonlight_triangulation/__init__.py+  bindings/python/tests/test_binding.py+  bindings/rust/Cargo.toml+  bindings/rust/build.rs+  bindings/rust/src/lib.rs+  bindings/rust/tests/binding.rs+  bindings/typescript/package.json+  bindings/typescript/pnpm-workspace.yaml+  bindings/typescript/tsconfig.json+  bindings/typescript/src/index.ts+  bindings/typescript/test/binding.test.ts  source-repository head   type:     git@@ -260,6 +274,43 @@     , moonlight-triangulation:dual   ghc-options: -fexpose-all-unfoldings +library ffi+  import: shared-properties+  visibility: private+  hs-source-dirs: src-ffi+  exposed-modules:+    Moonlight.Triangulation.Foreign.ABI+  build-depends:+    base >= 4.20 && < 5+    , vector >= 0.13 && < 0.14+    , moonlight-triangulation+    , moonlight-triangulation:build+    , moonlight-triangulation:dcel++foreign-library moonlight-triangulation-c+  import: shared-properties+  type: native-shared+  hs-source-dirs: src-capi+  other-modules:+    Moonlight.Triangulation.Foreign.Exports+  c-sources:+    cbits/moonlight_runtime.c+  include-dirs:+    include+  install-includes:+    moonlight_triangulation.h+  build-depends:+    base >= 4.20 && < 5+    , moonlight-triangulation:ffi >= 1.0 && < 1.1+  ghc-options: -threaded+  if os(windows)+    options: standalone+    mod-def-file: cbits/moonlight-triangulation.def+  else+    extra-libraries: pthread+  if os(linux)+    lib-version-info: 1:0:0+ -- ── test slices ──────────────────────────────────────────────────────────────  -- Each slice is an ATOM: a @common@ stanza binding a spec module to the@@ -390,6 +441,15 @@     test/parallel     test/algebra     test/support++test-suite moonlight-triangulation-ffi-test+  import: shared-properties+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs: test/ffi+  build-depends:+    base >= 4.20 && < 5+    , moonlight-triangulation:ffi  -- This component owns only cross-slice compile coherence.  Behavioral ownership -- remains in the four focused suites.  The shared test-properties stanza keeps
src-build/Moonlight/Triangulation/BulkLoad.hs view
@@ -10,6 +10,7 @@   ( empty   , clear   , delaunay+  , delaunayGeometry   , DuplicatePayloadPolicy (..)   , delaunayFromCoordinates   , insert@@ -116,6 +117,23 @@     (position . (input V.!))     (input V.!)     KeepFirstPayload++-- | Build the geometry-only Delaunay triangulation of a coordinate vector.+-- Exact duplicate positions collapse to one site. Use 'delaunay' or+-- 'delaunayFromCoordinates' when vertex annotations or the input-to-vertex+-- mapping are part of the result.+delaunayGeometry+  :: V.Vector Point+  -> Either BuildError (DelaunayTriangulation ())+delaunayGeometry coordinates = do+  V.iforM_ coordinates (\index point -> validatePoint (Just index) point)+  buildTriangulation+    <$> buildDelaunayFromSource+          unitElementDefaults+          (V.length coordinates)+          (coordinates V.!)+          (const ())+          KeepFirstPayload  -- | Canonical construction from separate geometry and annotation sources. -- The coordinate vector remains the only geometry in ingress; payloads never
src-build/Moonlight/Triangulation/SetAlgebra.hs view
@@ -17,7 +17,7 @@ import Data.Maybe (isJust) import qualified Data.Vector as V import Moonlight.Triangulation.BulkLoad (empty)-import Moonlight.Triangulation.Dcel (numVertices, vertexData, vertexPoint)+import Moonlight.Triangulation.Dcel (numVertices, vertexData, vertexPoint, vertexPoints) import Moonlight.Triangulation.Handles.Iterators.FixedIterators (vertices) import Moonlight.Triangulation.Internal.Join (joinBalanced, joinNormalForm) import Moonlight.Triangulation.Internal.Join.Rebuild (rebuildCanonicalSiteSet)@@ -137,7 +137,7 @@   | numVertices left == 0 || numVertices right == 0 = Right left   | numVertices right < numVertices left   , removalDeltaIsEligible (numVertices right) (numVertices left - numVertices right) =-      removeAvailableFrom left (V.fromList (fmap (vertexPoint right) (vertices right)))+      removeAvailableFrom left (vertexPoints right)   | otherwise =       rebuildCanonicalSiteSet         ( siteSetDifference
+ src-capi/Moonlight/Triangulation/Foreign/Exports.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE ForeignFunctionInterface #-}+{-# OPTIONS_GHC -Wno-missing-signatures #-}++module Moonlight.Triangulation.Foreign.Exports where++import Data.Word (Word32)+import Foreign.C.Types (CDouble, CSize (..), CUInt (..))+import Foreign.Ptr (Ptr)+import Moonlight.Triangulation.Foreign.ABI (CObstruction)+import qualified Moonlight.Triangulation.Foreign.ABI as ABI++delaunayF64 = ABI.delaunayF64+meshInsertManyF64 = ABI.meshInsertManyF64+meshUnion = ABI.meshUnion+meshIntersection = ABI.meshIntersection+meshDifference = ABI.meshDifference+meshSymmetricDifference = ABI.meshSymmetricDifference+meshVertexCount = ABI.meshVertexCount+meshTriangleCount = ABI.meshTriangleCount+meshCopyVerticesF64 = ABI.meshCopyVerticesF64+meshCopyTrianglesU32 = ABI.meshCopyTrianglesU32+meshFree = ABI.meshFree++foreign export ccall "ml_delaunay_f64" delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_insert_many_f64" meshInsertManyF64 :: Ptr () -> Ptr CDouble -> CSize -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_union" meshUnion :: Ptr () -> Ptr () -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_intersection" meshIntersection :: Ptr () -> Ptr () -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_difference" meshDifference :: Ptr () -> Ptr () -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_symmetric_difference" meshSymmetricDifference :: Ptr () -> Ptr () -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_vertex_count" meshVertexCount :: Ptr () -> Ptr CSize -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_triangle_count" meshTriangleCount :: Ptr () -> Ptr CSize -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_copy_vertices_f64" meshCopyVerticesF64 :: Ptr () -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_copy_triangles_u32" meshCopyTrianglesU32 :: Ptr () -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt+foreign export ccall "ml_mesh_free" meshFree :: Ptr () -> IO ()
+ src-ffi/Moonlight/Triangulation/Foreign/ABI.hs view
@@ -0,0 +1,445 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE RecordWildCards #-}++module Moonlight.Triangulation.Foreign.ABI+  ( CObstruction (..)+  , delaunayF64+  , meshInsertManyF64+  , meshUnion+  , meshIntersection+  , meshDifference+  , meshSymmetricDifference+  , meshVertexCount+  , meshTriangleCount+  , meshCopyVerticesF64+  , meshCopyTrianglesU32+  , meshFree+  ) where++import Control.Exception (SomeException, displayException, try)+import Control.Monad (void)+import Data.Foldable (traverse_)+import Data.Word (Word32, Word64)+import Foreign.C.String (peekCString, withCStringLen)+import Foreign.C.Types (CDouble (..), CSize (..), CUInt (..))+import Foreign.Marshal.Utils (copyBytes, fillBytes)+import Foreign.Ptr (Ptr, castPtr, nullPtr, plusPtr)+import Foreign.StablePtr+  ( StablePtr+  , castPtrToStablePtr+  , castStablePtrToPtr+  , deRefStablePtr+  , freeStablePtr+  , newStablePtr+  )+import Foreign.Storable (Storable (..))+import qualified Data.Vector as V+import qualified Moonlight.Triangulation as T+import Moonlight.Triangulation.Math (validatePoint)+import qualified Moonlight.Triangulation.Session as Session++type GeometryMesh = T.DelaunayTriangulation ()++data CObstruction = CObstruction+  { obstructionCode :: !Word32+  , obstructionCoordinateError :: !Word32+  , obstructionInputIndex :: !Word64+  , obstructionFirstIndex :: !Word64+  , obstructionSecondIndex :: !Word64+  , obstructionFirstValue :: !Double+  , obstructionSecondValue :: !Double+  , obstructionPointX :: !Double+  , obstructionPointY :: !Double+  , obstructionMessage :: !String+  }+  deriving stock (Eq, Show)++instance Storable CObstruction where+  sizeOf _ = 320+  alignment _ = alignment (undefined :: Word64)+  peek pointer = do+    obstructionCode <- peekByteOff pointer 0+    obstructionCoordinateError <- peekByteOff pointer 4+    obstructionInputIndex <- peekByteOff pointer 8+    obstructionFirstIndex <- peekByteOff pointer 16+    obstructionSecondIndex <- peekByteOff pointer 24+    obstructionFirstValue <- peekByteOff pointer 32+    obstructionSecondValue <- peekByteOff pointer 40+    obstructionPointX <- peekByteOff pointer 48+    obstructionPointY <- peekByteOff pointer 56+    obstructionMessage <- peekCString (castPtr pointer `plusPtr` 64)+    pure CObstruction {..}+  poke pointer CObstruction {..} = do+    pokeByteOff pointer 0 obstructionCode+    pokeByteOff pointer 4 obstructionCoordinateError+    pokeByteOff pointer 8 obstructionInputIndex+    pokeByteOff pointer 16 obstructionFirstIndex+    pokeByteOff pointer 24 obstructionSecondIndex+    pokeByteOff pointer 32 obstructionFirstValue+    pokeByteOff pointer 40 obstructionSecondValue+    pokeByteOff pointer 48 obstructionPointX+    pokeByteOff pointer 56 obstructionPointY+    let messagePointer = castPtr pointer `plusPtr` 64+    fillBytes messagePointer 0 256+    withCStringLen obstructionMessage $ \(source, lengthInBytes) ->+      copyBytes messagePointer source (min 255 lengthInBytes)++data AbiFailure = AbiFailure !CUInt !CObstruction++statusOk, statusNullPointer, statusCountOverflow, statusBufferTooSmall, statusBuildObstruction, statusRuntimeFailure :: CUInt+statusOk = 0+statusNullPointer = 1+statusCountOverflow = 2+statusBufferTooSmall = 3+statusBuildObstruction = 4+statusRuntimeFailure = 5++emptyObstruction :: CObstruction+emptyObstruction =+  CObstruction+    { obstructionCode = 0+    , obstructionCoordinateError = 0+    , obstructionInputIndex = maxBound+    , obstructionFirstIndex = 0+    , obstructionSecondIndex = 0+    , obstructionFirstValue = 0+    , obstructionSecondValue = 0+    , obstructionPointX = 0+    , obstructionPointY = 0+    , obstructionMessage = ""+    }++apiFailure :: CUInt -> Word32 -> String -> AbiFailure+apiFailure status code message =+  AbiFailure status emptyObstruction {obstructionCode = code, obstructionMessage = message}++nullPointerFailure :: String -> AbiFailure+nullPointerFailure label = apiFailure statusNullPointer 100 (label <> " must not be null")++countOverflowFailure :: Word64 -> AbiFailure+countOverflowFailure count =+  AbiFailure statusCountOverflow emptyObstruction+    { obstructionCode = 101+    , obstructionFirstIndex = count+    , obstructionMessage = "count exceeds the host Int range"+    }++bufferTooSmallFailure :: Int -> Int -> AbiFailure+bufferTooSmallFailure required capacity =+  AbiFailure statusBufferTooSmall emptyObstruction+    { obstructionCode = 102+    , obstructionFirstIndex = fromIntegral required+    , obstructionSecondIndex = fromIntegral capacity+    , obstructionMessage = "output buffer is smaller than the required element count"+    }++runtimeFailure :: SomeException -> AbiFailure+runtimeFailure = apiFailure statusRuntimeFailure 103 . displayException++runBoundary :: Ptr CObstruction -> IO (Either AbiFailure ()) -> IO CUInt+runBoundary obstructionPointer action = do+  writeObstruction obstructionPointer emptyObstruction+  outcome <- try action :: IO (Either SomeException (Either AbiFailure ()))+  case outcome of+    Left exception -> finishFailure obstructionPointer (runtimeFailure exception)+    Right (Left failure) -> finishFailure obstructionPointer failure+    Right (Right ()) -> pure statusOk++finishFailure :: Ptr CObstruction -> AbiFailure -> IO CUInt+finishFailure obstructionPointer (AbiFailure status obstruction) = do+  writeObstruction obstructionPointer obstruction+  pure status++writeObstruction :: Ptr CObstruction -> CObstruction -> IO ()+writeObstruction pointer obstruction+  | pointer == nullPtr = pure ()+  | otherwise = poke pointer obstruction++requirePointer :: String -> Ptr value -> Either AbiFailure ()+requirePointer label pointer+  | pointer == nullPtr = Left (nullPointerFailure label)+  | otherwise = Right ()++checkedCount :: Int -> CSize -> Either AbiFailure Int+checkedCount elementsPerItem rawCount+  | toInteger rawCount * toInteger elementsPerItem > toInteger (maxBound :: Int) =+      Left (countOverflowFailure (fromIntegral rawCount))+  | otherwise = Right (fromIntegral rawCount)++readPoints :: Ptr CDouble -> Int -> IO (Either AbiFailure (V.Vector T.Point))+readPoints pointer count+  | count == 0 = pure (Right V.empty)+  | pointer == nullPtr = pure (Left (nullPointerFailure "coordinates"))+  | otherwise =+      Right+        <$> V.generateM+              count+              ( \index -> do+                  CDouble x <- peekElemOff pointer (index * 2)+                  CDouble y <- peekElemOff pointer (index * 2 + 1)+                  pure (T.Point x y)+              )++prepareMeshOutput :: Ptr (Ptr ()) -> IO (Either AbiFailure ())+prepareMeshOutput pointer =+  case requirePointer "result" pointer of+    Left failure -> pure (Left failure)+    Right () -> poke pointer nullPtr >> pure (Right ())++publishMesh :: Ptr (Ptr ()) -> GeometryMesh -> IO ()+publishMesh output mesh = do+  stable <- newStablePtr mesh+  poke output (castStablePtrToPtr stable)++produceMesh :: Ptr (Ptr ()) -> IO (Either AbiFailure (Either T.BuildError GeometryMesh)) -> IO (Either AbiFailure ())+produceMesh output obtain = do+  prepared <- prepareMeshOutput output+  case prepared of+    Left failure -> pure (Left failure)+    Right () -> do+      outcome <- obtain+      case outcome of+        Left failure -> pure (Left failure)+        Right (Left obstruction) ->+          pure (Left (AbiFailure statusBuildObstruction (buildErrorObstruction obstruction)))+        Right (Right mesh) -> publishMesh output mesh >> pure (Right ())++delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+delaunayF64 coordinates rawCount output obstructionPointer =+  runBoundary obstructionPointer $ produceMesh output $ do+    case checkedCount 2 rawCount of+      Left failure -> pure (Left failure)+      Right count -> fmap (fmap T.delaunayGeometry) (readPoints coordinates count)++meshInsertManyF64 :: Ptr () -> Ptr CDouble -> CSize -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+meshInsertManyF64 meshPointer coordinates rawCount output obstructionPointer =+  runBoundary obstructionPointer $ produceMesh output $ do+    case (requirePointer "mesh" meshPointer, checkedCount 2 rawCount) of+      (Left failure, _) -> pure (Left failure)+      (_, Left failure) -> pure (Left failure)+      (Right (), Right count) -> do+        pointsOutcome <- readPoints coordinates count+        case pointsOutcome of+          Left failure -> pure (Left failure)+          Right points -> do+            mesh <- dereferenceMesh meshPointer+            pure (Right (insertGeometryBatch mesh points))++insertGeometryBatch :: GeometryMesh -> V.Vector T.Point -> Either T.BuildError GeometryMesh+insertGeometryBatch mesh points = do+  normalized <-+    V.imapM+      (\index point -> T.queryPointValue <$> validatePoint (Just index) point)+      points+  (_, revised, _) <-+    Session.withSession+      mesh+      (V.length normalized)+      (traverse_ (\point -> void (Session.insertVertexAt point ())) normalized)+  pure revised++meshUnion, meshIntersection, meshDifference, meshSymmetricDifference :: Ptr () -> Ptr () -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+meshUnion = binaryMeshOperation T.union+meshIntersection = binaryMeshOperation T.intersection+meshDifference = binaryMeshOperation T.difference+meshSymmetricDifference = binaryMeshOperation T.symmetricDifference++binaryMeshOperation :: (GeometryMesh -> GeometryMesh -> Either T.BuildError GeometryMesh) -> Ptr () -> Ptr () -> Ptr (Ptr ()) -> Ptr CObstruction -> IO CUInt+binaryMeshOperation operation leftPointer rightPointer output obstructionPointer =+  runBoundary obstructionPointer $ produceMesh output $ do+    case (requirePointer "left mesh" leftPointer, requirePointer "right mesh" rightPointer) of+      (Left failure, _) -> pure (Left failure)+      (_, Left failure) -> pure (Left failure)+      (Right (), Right ()) -> do+        left <- dereferenceMesh leftPointer+        right <- dereferenceMesh rightPointer+        pure (Right (operation left right))++meshVertexCount, meshTriangleCount :: Ptr () -> Ptr CSize -> Ptr CObstruction -> IO CUInt+meshVertexCount = meshCount T.numVertices+meshTriangleCount = meshCount (V.length . T.innerFaceVertexTriples)++meshCount :: (GeometryMesh -> Int) -> Ptr () -> Ptr CSize -> Ptr CObstruction -> IO CUInt+meshCount observe meshPointer output obstructionPointer =+  runBoundary obstructionPointer $+    case (requirePointer "mesh" meshPointer, requirePointer "count" output) of+      (Left failure, _) -> pure (Left failure)+      (_, Left failure) -> pure (Left failure)+      (Right (), Right ()) -> do+        mesh <- dereferenceMesh meshPointer+        poke output (fromIntegral (observe mesh))+        pure (Right ())++meshCopyVerticesF64 :: Ptr () -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt+meshCopyVerticesF64 =+  copyMeshProjection+    "points_written"+    "coordinates"+    T.vertexPoints+    ( \output index (T.Point x y) -> do+        pokeElemOff output (index * 2) (CDouble x)+        pokeElemOff output (index * 2 + 1) (CDouble y)+    )++meshCopyTrianglesU32 :: Ptr () -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt+meshCopyTrianglesU32 =+  copyMeshProjection+    "triangles_written"+    "triangles"+    T.innerFaceVertexTriples+    ( \output index (first, second, third) -> do+        pokeElemOff output (index * 3) (T.unVertexId first)+        pokeElemOff output (index * 3 + 1) (T.unVertexId second)+        pokeElemOff output (index * 3 + 2) (T.unVertexId third)+    )++copyMeshProjection+  :: String -> String -> (GeometryMesh -> V.Vector item) -> (Ptr element -> Int -> item -> IO ())+  -> Ptr () -> Ptr element -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt+{-# INLINE copyMeshProjection #-}+copyMeshProjection writtenLabel outputLabel project writeItem meshPointer output rawCapacity written obstructionPointer =+  runBoundary obstructionPointer $+    case (requirePointer "mesh" meshPointer, requirePointer writtenLabel written, checkedCount 1 rawCapacity) of+      (Left failure, _, _) -> pure (Left failure)+      (_, Left failure, _) -> pure (Left failure)+      (_, _, Left failure) -> pure (Left failure)+      (Right (), Right (), Right capacity) -> do+        mesh <- dereferenceMesh meshPointer+        let items = project mesh+            required = V.length items+        poke written (fromIntegral required)+        case requireOutputCapacity outputLabel output required capacity of+          Left failure -> pure (Left failure)+          Right () -> V.imapM_ (writeItem output) items >> pure (Right ())++requireOutputCapacity :: String -> Ptr value -> Int -> Int -> Either AbiFailure ()+requireOutputCapacity label output required capacity+  | capacity < required = Left (bufferTooSmallFailure required capacity)+  | required > 0 = requirePointer label output+  | otherwise = Right ()++dereferenceMesh :: Ptr () -> IO GeometryMesh+dereferenceMesh = deRefStablePtr . (castPtrToStablePtr :: Ptr () -> StablePtr GeometryMesh)++meshFree :: Ptr () -> IO ()+meshFree pointer+  | pointer == nullPtr = pure ()+  | otherwise = freeStablePtr ((castPtrToStablePtr pointer) :: StablePtr GeometryMesh)++buildErrorObstruction :: T.BuildError -> CObstruction+buildErrorObstruction failure =+  (case failure of+    T.InvalidCoordinate inputIndex value reason ->+      emptyObstruction+        { obstructionCode = 1+        , obstructionCoordinateError = coordinateErrorCode reason+        , obstructionInputIndex = maybe maxBound fromIntegral inputIndex+        , obstructionFirstValue = value+        }+    T.PointLocationFailed (T.Point x y) -> pointObstruction 2 x y+    T.LocationWalkExhausted (T.Point x y) steps ->+      (pointObstruction 3 x y) {obstructionFirstIndex = fromIntegral steps}+    T.RefinementInputTopologyInvalid _ -> codeOnly 4+    T.FreshInsertionMatchedExistingVertex first second -> indices 5 (T.unVertexId first) (T.unVertexId second)+    T.DegenerateLineEndpointMissingOutgoing vertex -> firstIndex 6 (T.unVertexId vertex)+    T.DegenerateLineEndpointTurnMissing index -> firstIndex 7 index+    T.DegenerateLineConnectedVertexMissing index -> firstIndex 8 index+    T.HullStartNotVisible edge -> firstIndex 9 (T.unDirectedEdgeId edge)+    T.OuterRangeDidNotTerminate first second steps ->+      (indices 10 (T.unDirectedEdgeId first) (T.unDirectedEdgeId second))+        {obstructionFirstValue = fromIntegral steps}+    T.OuterRangeContainsInnerEdge edge face -> indices 11 (T.unDirectedEdgeId edge) (T.unFaceId face)+    T.ConstrainedEdgeFlipRefused edge -> firstIndex 12 (T.unUndirectedEdgeId edge)+    T.RemovalVertexOutOfRange vertex count -> indices 13 (T.unVertexId vertex) count+    T.RemovalEdgeOutOfRange edge count -> indices 14 (T.unUndirectedEdgeId edge) count+    T.RemovalFaceOutOfRange face count -> indices 15 (T.unFaceId face) count+    T.RemovalFaceCycleDidNotTerminate face edge steps ->+      (indices 16 (T.unFaceId face) (T.unDirectedEdgeId edge))+        {obstructionFirstValue = fromIntegral steps}+    T.RemovalEmptyTriangulation vertex -> firstIndex 17 (T.unVertexId vertex)+    T.RemovalTwoPointDegreeMismatch vertex degree -> indices 18 (T.unVertexId vertex) degree+    T.RemovalCollinearDegreeMismatch vertex degree -> indices 19 (T.unVertexId vertex) degree+    T.RemovalBorderTooShort count -> firstIndex 20 count+    T.RemovalBorderArityMismatch count -> firstIndex 21 count+    T.RemovalOutgoingCycleDidNotTerminate vertex edge steps ->+      (indices 22 (T.unVertexId vertex) (T.unDirectedEdgeId edge))+        {obstructionFirstValue = fromIntegral steps}+    T.CircleSweepHullEmpty -> codeOnly 23+    T.OuterCycleDidNotTerminate first second steps ->+      (indices 24 (T.unDirectedEdgeId first) (T.unDirectedEdgeId second))+        {obstructionFirstValue = fromIntegral steps}+    T.HierarchyLevelPopulationMismatch level expected observed ->+      (indices 25 expected observed) {obstructionFirstValue = fromIntegral level}+    T.HierarchyInsertionHandleMismatch expected observed -> indices 26 (T.unVertexId expected) (T.unVertexId observed)+    T.PointIndexCapacityExhausted count -> firstIndex 27 count+    T.RefinementMinimumAngleNotFinite value -> nonFinite 28 value+    T.RefinementMinimumAngleOutOfRange value -> firstValue 29 value+    T.RefinementMinimumAngleDerivedRatioNotFinite value -> nonFinite 30 value+    T.RefinementMaximumAdditionalVerticesNegative value -> firstValue 31 (fromIntegral value)+    T.RefinementMinimumAreaNotFinite value -> nonFinite 32 value+    T.RefinementMinimumAreaNegative value -> firstValue 33 value+    T.RefinementMaximumAreaNotFinite value -> nonFinite 34 value+    T.RefinementMaximumAreaNotPositive value -> firstValue 35 value+    T.RefinementMaximumRadiusEdgeRatioNotFinite value -> nonFinite 36 value+    T.RefinementMaximumRadiusEdgeRatioNotPositive value -> firstValue 37 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+    T.RefinementDomainInterfaceMissing edge -> firstIndex 41 (T.unUndirectedEdgeId edge)+    T.RefinementDomainInterfaceExtraneous edge -> firstIndex 42 (T.unUndirectedEdgeId edge)+    T.RefinementDomainTopologyChanged -> codeOnly 43+    T.RefinementDomainRequiresConvexHullPreservation -> codeOnly 44+    T.RefinementDomainRequiresConstraintPreservation -> codeOnly 45+    T.RefinementDomainForbidsOuterFaceExclusion -> codeOnly 46+    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.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+  )+    {obstructionMessage = show failure}+ where+  codeOnly :: Word32 -> CObstruction+  codeOnly code = emptyObstruction {obstructionCode = code}+  firstIndex :: Integral index => Word32 -> index -> CObstruction+  firstIndex code index = (codeOnly code) {obstructionFirstIndex = fromIntegral index}+  indices :: (Integral first, Integral second) => Word32 -> first -> second -> CObstruction+  indices code first second =+    (codeOnly code)+      { obstructionFirstIndex = fromIntegral first+      , obstructionSecondIndex = fromIntegral second+      }+  firstValue :: Word32 -> Double -> CObstruction+  firstValue code value = (codeOnly code) {obstructionFirstValue = value}+  values :: Word32 -> Double -> Double -> CObstruction+  values code first second =+    (codeOnly code)+      { obstructionFirstValue = first+      , obstructionSecondValue = second+      }+  pointObstruction :: Word32 -> Double -> Double -> CObstruction+  pointObstruction code x y =+    (codeOnly code)+      { obstructionPointX = x+      , obstructionPointY = y+      }+  nonFinite :: Word32 -> T.NonFiniteValue -> CObstruction+  nonFinite code value = firstIndex code (nonFiniteCode value)++coordinateErrorCode :: T.CoordinateError -> Word32+coordinateErrorCode reason =+  case reason of+    T.CoordinateNaN -> 1+    T.CoordinateInfinite -> 2+    T.CoordinateTooSmall -> 3+    T.CoordinateTooLarge -> 4++nonFiniteCode :: T.NonFiniteValue -> Word32+nonFiniteCode value =+  case value of+    T.ValueNaN -> 1+    T.ValuePositiveInfinity -> 2+    T.ValueNegativeInfinity -> 3
src-public/Moonlight/Triangulation.hs view
@@ -53,6 +53,9 @@      -- * Generation — @delaunay@; canonical observation factors through the site set   , delaunay+  , delaunayGeometry+  , DuplicatePayloadPolicy (..)+  , delaunayFromCoordinates   , BuildResult   , buildTriangulation   , buildInputVertices@@ -185,7 +188,12 @@   , InvariantViolation (..)   ) where -import Moonlight.Triangulation.BulkLoad (delaunay)+import Moonlight.Triangulation.BulkLoad+  ( DuplicatePayloadPolicy (..)+  , delaunay+  , delaunayFromCoordinates+  , delaunayGeometry+  ) import Moonlight.Triangulation.Dcel   ( destination   , faceDirectedEdges
test/algebra/Moonlight/Triangulation/AlgebraFixtures.hs view
@@ -36,7 +36,7 @@   , buildTriangulation   , canonicalize   , delaunay-  , mapVertices+  , delaunayGeometry   , numFaces   , numUndirectedEdges   , numVertices@@ -150,7 +150,8 @@ -- ── construction ─────────────────────────────────────────────────────────────  meshOf :: String -> [Point] -> IO Mesh-meshOf label points = mapVertices (const ()) <$> pointMeshOf label points+meshOf label points =+  requireRight ("build geometry " <> label) (delaunayGeometry (V.fromList points))  pointMeshOf :: String -> [Point] -> IO PointMesh pointMeshOf label points =
test/algebra/Moonlight/Triangulation/AlgebraSpec.hs view
@@ -20,12 +20,15 @@   ( BuildError   , ConstraintMode (..)   , DelaunayTriangulation+  , DuplicatePayloadPolicy (..)   , HasPosition (..)   , JoinSemilattice (..)   , Point (Point)   , Triangulation   , buildTriangulation   , delaunay+  , delaunayFromCoordinates+  , delaunayGeometry   , unitElementDefaults   , vertexData   , vertexPoint@@ -69,6 +72,7 @@  tests :: IO () tests = do+  testConstructionEntrances   testJoinIdentity   testJoinCommutative   testJoinAssociative@@ -94,6 +98,38 @@   testAnnotationPreservation   testOldEdgeAccounting   putStrLn "algebra: ok"++testConstructionEntrances :: IO ()+testConstructionEntrances = do+  let coordinates = V.fromList [Point 0 0, Point 3 0, Point 0 4, Point 0 0]+      payloads = V.fromList [2 :: Int, 3, 5, 7]+  geometry <- requireRight "geometry-only construction" (delaunayGeometry coordinates)+  legacy <-+    mapVertices (const ()) . buildTriangulation+      <$> requireRight "annotated point construction" (delaunay unitElementDefaults coordinates)+  unless (geometry == legacy) $+    fail "delaunayGeometry disagreed with the existing point construction"+  annotated <-+    buildTriangulation+      <$> requireRight+            "coordinate/payload construction"+            ( delaunayFromCoordinates+                unitElementDefaults+                coordinates+                payloads+                (CombineDuplicatePayload (+))+            )+  unless (validateTriangulation geometry == [] && validateTriangulation annotated == []) $+    fail "public construction entrance produced an invalid triangulation"+  let observed =+        Map.fromList+          [ ((x, y), vertexData annotated vertex)+          | vertex <- vertices annotated+          , let Point x y = vertexPoint annotated vertex+          ]+      expected = Map.fromList [((0, 0), 9), ((3, 0), 3), ((0, 4), 5)]+  unless (observed == expected) $+    fail "delaunayFromCoordinates did not apply its duplicate payload policy"  -- ── laws ───────────────────────────────────────────────────────────────────── 
+ test/ffi/Main.hs view
@@ -0,0 +1,149 @@+module Main (main) where++import Control.Exception (bracket)+import Control.Monad (unless)+import Data.Word (Word32)+import Foreign.C.Types (CDouble (..), CSize (..), CUInt)+import Foreign.Marshal.Alloc (alloca)+import Foreign.Marshal.Array (allocaArray, peekArray, withArray)+import Foreign.Ptr (Ptr, nullPtr)+import Foreign.Storable (Storable (sizeOf), peek)+import Moonlight.Triangulation.Foreign.ABI++type MeshPointer = Ptr ()++main :: IO ()+main = do+  unless (sizeOf (undefined :: CObstruction) == 320) $+    fail "C obstruction layout changed"+  bracket (buildMesh [(0, 0), (2, 0), (0, 2), (2, 2)]) meshFree $ \left ->+    bracket (buildMesh [(2, 0), (4, 0), (2, 2), (4, 2)]) meshFree $ \right -> do+      requireMeshCount "left vertex count" meshVertexCount left 4+      requireMeshCount "left triangle count" meshTriangleCount left 2+      testDenseCopies left+      testImmutableBatch left+      testBinaryAlgebra left right+  testTypedCoordinateRefusal+  testCoordinateCountOverflow+  testNullPointerRefusal+  putStrLn "ffi: ok"++buildMesh :: [(Double, Double)] -> IO MeshPointer+buildMesh points =+  withPointArray points $ \coordinates ->+    alloca $ \output ->+      alloca $ \obstruction -> do+        status <- delaunayF64 coordinates (fromIntegral (length points)) output obstruction+        requireStatus "delaunay" 0 status obstruction+        handle <- peek output+        unless (handle /= nullPtr) (fail "delaunay returned a null handle")+        pure handle++testDenseCopies :: MeshPointer -> IO ()+testDenseCopies mesh = do+  alloca $ \written ->+    alloca $ \obstruction -> do+      status <- meshCopyVerticesF64 mesh nullPtr 0 written obstruction+      requireStatus "undersized vertex copy" 3 status obstruction+      required <- peek written+      refusal <- peek obstruction+      unless (required == 4 && obstructionCode refusal == 102) $+        fail "undersized vertex copy lost its required-capacity witness"+  allocaArray 8 $ \coordinates ->+    alloca $ \written ->+      alloca $ \obstruction -> do+        status <- meshCopyVerticesF64 mesh coordinates 4 written obstruction+        requireStatus "vertex copy" 0 status obstruction+        values <- peekArray 8 coordinates+        unless (length values == 8) (fail "vertex copy wrote the wrong coordinate extent")+  allocaArray 6 $ \triangles ->+    alloca $ \written ->+      alloca $ \obstruction -> do+        status <- meshCopyTrianglesU32 mesh triangles 2 written obstruction+        requireStatus "triangle copy" 0 status obstruction+        indices <- peekArray 6 triangles :: IO [Word32]+        unless (all (< 4) indices) (fail "triangle copy produced an out-of-range vertex")++testImmutableBatch :: MeshPointer -> IO ()+testImmutableBatch original =+  withPointArray [(1, 1), (3, 1)] $ \coordinates ->+    bracket+      (produceMesh "batch insert" (meshInsertManyF64 original coordinates 2))+      meshFree+      (\revised -> do+        requireMeshCount "original after batch" meshVertexCount original 4+        requireMeshCount "revised after batch" meshVertexCount revised 6+      )++testBinaryAlgebra :: MeshPointer -> MeshPointer -> IO ()+testBinaryAlgebra left right = do+  test "union" meshUnion 6+  test "intersection" meshIntersection 2+  test "difference" meshDifference 2+  test "symmetric difference" meshSymmetricDifference 4+ where+  test label operation expected =+    bracket (produceMesh label (operation left right)) meshFree $ \result ->+      requireMeshCount label meshVertexCount result expected++testTypedCoordinateRefusal :: IO ()+testTypedCoordinateRefusal =+  withPointArray [(0, 0), (0 / 0, 1), (1, 0)] $ \coordinates ->+    alloca $ \output ->+      alloca $ \obstruction -> do+        status <- delaunayF64 coordinates 3 output obstruction+        requireStatus "invalid coordinate" 4 status obstruction+        refusal <- peek obstruction+        unless (obstructionCode refusal == 1 && obstructionCoordinateError refusal == 1 && obstructionInputIndex refusal == 1) $+          fail "invalid coordinate lost its typed witness"++testNullPointerRefusal :: IO ()+testNullPointerRefusal =+  alloca $ \count ->+    alloca $ \obstruction -> do+      status <- meshVertexCount nullPtr count obstruction+      requireStatus "null mesh" 1 status obstruction+      refusal <- peek obstruction+      unless (obstructionCode refusal == 100) $+        fail "null pointer refusal lost its typed witness"++testCoordinateCountOverflow :: IO ()+testCoordinateCountOverflow =+  alloca $ \output ->+    alloca $ \obstruction -> do+      let overflowingCount = fromIntegral (maxBound `div` 2 + 1 :: Int)+      status <- delaunayF64 nullPtr overflowingCount output obstruction+      requireStatus "coordinate count overflow" 2 status obstruction+      refusal <- peek obstruction+      unless (obstructionCode refusal == 101) $+        fail "coordinate count overflow lost its typed witness"++requireMeshCount :: String -> (MeshPointer -> Ptr CSize -> Ptr CObstruction -> IO CUInt) -> MeshPointer -> Int -> IO ()+requireMeshCount label operation mesh expected =+  alloca $ \count ->+    alloca $ \obstruction -> do+      status <- operation mesh count obstruction+      requireStatus label 0 status obstruction+      observed <- peek count+      unless (observed == fromIntegral expected) $+        fail (label <> " produced " <> show observed <> ", expected " <> show expected)++produceMesh :: String -> (Ptr MeshPointer -> Ptr CObstruction -> IO CUInt) -> IO MeshPointer+produceMesh label operation =+  alloca $ \output ->+    alloca $ \obstruction -> do+      status <- operation output obstruction+      requireStatus label 0 status obstruction+      handle <- peek output+      unless (handle /= nullPtr) (fail (label <> " returned a null handle"))+      pure handle++requireStatus :: String -> CUInt -> CUInt -> Ptr CObstruction -> IO ()+requireStatus label expected observed obstruction+  | observed == expected = pure ()+  | otherwise = do+      refusal <- peek obstruction+      fail (label <> " returned status " <> show observed <> ": " <> obstructionMessage refusal)++withPointArray :: [(Double, Double)] -> (Ptr CDouble -> IO result) -> IO result+withPointArray points = withArray (concatMap (\(x, y) -> [CDouble x, CDouble y]) points)
weeder.toml view
@@ -39,6 +39,7 @@   '^Moonlight\.Triangulation\.Voronoi\.Handles$',   '^Moonlight\.Triangulation\.Interpolation$',   '^Moonlight\.Triangulation\.HintGenerator$',+  '^Moonlight\.Triangulation\.Foreign\.Exports$',   '^Moonlight\.Triangulation$' ]