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 +8/−0
- README.md +23/−11
- bench/join/Moonlight/Triangulation/JoinBench.hs +1/−1
- bindings/README.md +55/−0
- bindings/python/pyproject.toml +13/−0
- bindings/python/src/moonlight_triangulation/__init__.py +247/−0
- bindings/python/tests/test_binding.py +54/−0
- bindings/rust/Cargo.toml +14/−0
- bindings/rust/build.rs +11/−0
- bindings/rust/src/lib.rs +373/−0
- bindings/rust/tests/binding.rs +49/−0
- bindings/typescript/package.json +33/−0
- bindings/typescript/pnpm-workspace.yaml +5/−0
- bindings/typescript/src/index.ts +280/−0
- bindings/typescript/test/binding.test.ts +42/−0
- bindings/typescript/tsconfig.json +23/−0
- cbits/moonlight-triangulation.def +15/−0
- cbits/moonlight_runtime.c +48/−0
- include/moonlight_triangulation.h +132/−0
- moonlight-triangulation.cabal +61/−1
- src-build/Moonlight/Triangulation/BulkLoad.hs +18/−0
- src-build/Moonlight/Triangulation/SetAlgebra.hs +2/−2
- src-capi/Moonlight/Triangulation/Foreign/Exports.hs +34/−0
- src-ffi/Moonlight/Triangulation/Foreign/ABI.hs +445/−0
- src-public/Moonlight/Triangulation.hs +9/−1
- test/algebra/Moonlight/Triangulation/AlgebraFixtures.hs +3/−2
- test/algebra/Moonlight/Triangulation/AlgebraSpec.hs +36/−0
- test/ffi/Main.hs +149/−0
- weeder.toml +1/−0
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$' ]