packages feed

moonlight-triangulation-1.0.1.0: bindings/typescript/src/index.ts

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