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)));
}