packages feed

moonlight-triangulation-1.4.0.2: ffi/bindings/typescript/test/binding.test.ts

import assert from "node:assert/strict";
import test from "node:test";

import {
  AbiStatus,
  CoordinateError,
  MinkowskiOperation,
  ObstructionCode,
  RegionLocation,
  formatMoonlightFailure,
  loadMoonlight,
  type Moonlight,
  type Result,
} from "../src/index.js";

function expectSuccess<Value>(result: Result<Value>): Value {
  if (!result.ok) {
    assert.fail(formatMoonlightFailure(result.error));
  }
  return result.value;
}

function libraryPath(): string {
  const path = process.env.MOONLIGHT_TRIANGULATION_LIBRARY;
  assert.notEqual(path, undefined, "MOONLIGHT_TRIANGULATION_LIBRARY is required");
  return path ?? "";
}

function runtime(): Moonlight {
  return expectSuccess(loadMoonlight({ kind: "path", path: libraryPath() }));
}

test("immutable site-set algebra and dense projection", () => {
  const engine = runtime();
  using left = expectSuccess(engine.delaunay([[0, 0], [2, 0], [0, 2], [2, 2]]));
  using right = expectSuccess(engine.delaunay([[2, 0], [4, 0], [2, 2], [4, 2]]));
  using union = expectSuccess(left.siteUnion(right));
  using intersection = expectSuccess(left.siteIntersection(right));
  using difference = expectSuccess(left.siteDifference(right));
  using symmetric = expectSuccess(left.siteSymmetricDifference(right));
  using extended = expectSuccess(left.insertMany([[1, 1], [3, 1]]));

  assert.equal(expectSuccess(left.vertexCount()), 4);
  assert.equal(expectSuccess(union.vertexCount()), 6);
  assert.equal(expectSuccess(intersection.vertexCount()), 2);
  assert.equal(expectSuccess(difference.vertexCount()), 2);
  assert.equal(expectSuccess(symmetric.vertexCount()), 4);
  assert.equal(expectSuccess(extended.vertexCount()), 6);
  assert.equal(expectSuccess(left.vertices()).length, expectSuccess(left.vertexCount()));
  assert.equal(expectSuccess(left.triangles()).length, expectSuccess(left.triangleCount()));

});

test("exact regions expose Boolean, valuation, location, and all morphology operations", () => {
  const engine = runtime();
  using left = expectSuccess(engine.region([{ outer: [[0, 0], [2, 0], [2, 2], [0, 2]] }]));
  using right = expectSuccess(engine.region([{ outer: [[1, 0], [3, 0], [3, 2], [1, 2]] }]));
  using intersection = expectSuccess(left.intersection(right));
  using symmetric = expectSuccess(left.symmetricDifference(right));
  using kernel = expectSuccess(
    engine.structuringElement([[-0.25, -0.25], [0.25, -0.25], [0.25, 0.25], [-0.25, 0.25]]),
  );
  const sum = expectSuccess(left.minkowskiSum(right));
  const offset = expectSuccess(left.offset(kernel));
  const inset = expectSuccess(left.inset(kernel));
  const opened = expectSuccess(left.open(kernel));
  const closed = expectSuccess(left.close(kernel));
  using sumRegion = sum.region;
  using offsetRegion = offset.region;
  using insetRegion = inset.region;
  using openedRegion = opened.region;
  using closedRegion = closed.region;

  assert.equal(expectSuccess(left.components()).length, 1);
  assert.equal(expectSuccess(left.components())[0]?.outer.length, 4);
  assert.deepEqual(expectSuccess(intersection.valuations()).area, { numerator: 2n, denominator: 1n });
  assert.equal(expectSuccess(symmetric.valuations()).eulerCharacteristic, 2n);
  assert.equal(expectSuccess(left.locate([1, 1])), RegionLocation.Interior);
  assert.equal(expectSuccess(left.locate([0, 1])), RegionLocation.Boundary);
  assert.equal(expectSuccess(left.locate([3, 1])), RegionLocation.Exterior);
  assert.equal(offset.receipt.operation, MinkowskiOperation.Addition);
  assert.equal(inset.receipt.operation, MinkowskiOperation.Erosion);
  assert.equal(opened.receipt.operation, MinkowskiOperation.Opening);
  assert.equal(closed.receipt.operation, MinkowskiOperation.Closing);
  assert(sum.receipt.generatedPieces >= 1n);

});

test("invalid coordinates return a typed obstruction", () => {
  const result = runtime().delaunay([[0, 0], [Number.NaN, 1], [1, 0]]);
  assert.equal(result.ok, false);
  if (result.ok || result.error.kind !== "abi-obstruction") {
    throw new Error("expected an ABI obstruction");
  }
  assert.equal(result.error.obstruction.status, AbiStatus.GeometryObstruction);
  assert.equal(result.error.obstruction.code, ObstructionCode.InvalidCoordinate);
  assert.equal(result.error.obstruction.coordinateError, CoordinateError.Nan);
  assert.equal(result.error.obstruction.inputIndex, 1n);
});

test("materialized coordinates own native counts", () => {
  const points = new Proxy<readonly (readonly [number, number])[]>([[0, 0]], {
    get(target, property, receiver) {
      return property === "length" ? 1_000 : Reflect.get(target, property, receiver);
    },
  });
  using mesh = expectSuccess(runtime().delaunay(points));
  assert.equal(expectSuccess(mesh.vertexCount()), 1);
});

test("loading, runtime descent, and disposal are explicit", () => {
  const unavailable = loadMoonlight({ kind: "path", path: "" });
  assert.equal(unavailable.ok, false);
  if (!unavailable.ok) {
    assert.equal(unavailable.error.kind, "native-library-unavailable");
  }

  const firstRuntime = runtime();
  const secondRuntime = runtime();
  const first = expectSuccess(firstRuntime.delaunay([[0, 0]]));
  const second = expectSuccess(secondRuntime.delaunay([[1, 1]]));
  const mixed = first.siteUnion(second);
  assert.equal(mixed.ok, false);
  if (!mixed.ok) {
    assert.equal(mixed.error.kind, "foreign-runtime");
  }

  first[Symbol.dispose]();
  assert.equal(first.disposed, true);
  const afterDispose = first.vertexCount();
  assert.equal(afterDispose.ok, false);
  if (!afterDispose.ok) {
    assert.equal(afterDispose.error.kind, "disposed-resource");
  }
  second.dispose();
});