packages feed

moonlight-triangulation-1.4.0.2: ffi/bindings/typescript/src/region.ts

import { Buffer } from "node:buffer";

import { collectResults, failure, success, type Result } from "./failure.js";
import { captureNativeCall, callStatus, produceHandle, produceMorphology } from "./internal/call.js";
import { decodeLocation, decodeStatus, pointsFromCoordinates, toBigInt, toSafeNumber } from "./internal/decode.js";
import {
  ML_OBSTRUCTION_BUFFER_TOO_SMALL,
  ML_STATUS_BUFFER_TOO_SMALL,
  type NativeApi,
  type NativeHandle,
  type NativeInteger,
  type NativeObstruction,
} from "./internal/native.generated.js";
import { borrowResource, OwnedNativeResource, type RuntimeContext } from "./internal/resource.js";
import type { StructuringElement } from "./morphology.js";
import type { OwnedResource } from "./resource.js";
import type { MinkowskiReceipt, Point, PolygonComponent, RegionValuations } from "./values.js";
import type { RegionLocation } from "./wire.generated.js";

export interface MorphologyResult {
  readonly region: Region;
  readonly receipt: MinkowskiReceipt;
}

export interface Region extends OwnedResource {
  components(): Result<readonly PolygonComponent[]>;
  valuations(): Result<RegionValuations>;
  locate(point: Point): Result<RegionLocation>;
  union(other: Region): Result<Region>;
  intersection(other: Region): Result<Region>;
  difference(other: Region): Result<Region>;
  symmetricDifference(other: Region): Result<Region>;
  minkowskiSum(other: Region): Result<MorphologyResult>;
  offset(element: StructuringElement): Result<MorphologyResult>;
  inset(element: StructuringElement): Result<MorphologyResult>;
  open(element: StructuringElement): Result<MorphologyResult>;
  close(element: StructuringElement): Result<MorphologyResult>;
}

/** @internal */
export function createRegion(context: RuntimeContext, handle: NativeHandle): Region {
  return new RegionResource(context, handle);
}

class RegionResource extends OwnedNativeResource implements Region {
  constructor(context: RuntimeContext, handle: NativeHandle) {
    super(context, handle, context.native.ml_region_free, "region");
  }

  components(): Result<readonly PolygonComponent[]> {
    const handle = this.handle();
    if (!handle.ok) {
      return handle;
    }
    const counts = this.counts(handle.value);
    if (!counts.ok) {
      return counts;
    }
    const [componentCount, loopCount, pointCount] = counts.value;
    const coordinates = new Float64Array(pointCount * 2);
    const loopPointOffsets = new BigUint64Array(loopCount + 1);
    const componentLoopOffsets = new BigUint64Array(componentCount + 1);
    const copied = callStatus((obstruction) =>
      this.context().native.ml_region_copy_f64(
        handle.value,
        coordinates,
        pointCount,
        loopPointOffsets,
        loopCount + 1,
        componentLoopOffsets,
        componentCount + 1,
        obstruction,
      ),
    );
    if (!copied.ok) {
      return copied;
    }
    const points = pointsFromCoordinates(coordinates, pointCount);
    const loopOffsets = collectResults(
      Array.from({ length: loopCount + 1 }, (_unused, index) => toSafeNumber(loopPointOffsets[index])),
    );
    if (!loopOffsets.ok) {
      return loopOffsets;
    }
    const componentOffsets = collectResults(
      Array.from({ length: componentCount + 1 }, (_unused, index) => toSafeNumber(componentLoopOffsets[index])),
    );
    if (!componentOffsets.ok) {
      return componentOffsets;
    }
    const loops = Array.from({ length: loopCount }, (_unused, index) =>
      points.slice(loopOffsets.value[index] ?? 0, loopOffsets.value[index + 1] ?? 0),
    );
    return collectResults(
      Array.from({ length: componentCount }, (_unused, index): Result<PolygonComponent> => {
        const start = componentOffsets.value[index] ?? 0;
        const end = componentOffsets.value[index + 1] ?? 0;
        const outer = loops[start];
        return outer === undefined || start >= end
          ? failure({ kind: "invalid-native-result", message: "Moonlight returned a component without an outer loop" })
          : success({ outer, holes: loops.slice(start + 1, end) });
      }),
    );
  }

  valuations(): Result<RegionValuations> {
    const handle = this.handle();
    return handle.ok ? this.measureWithCapacity(handle.value, 128) : handle;
  }

  locate([x, y]: Point): Result<RegionLocation> {
    const handle = this.handle();
    if (!handle.ok) {
      return handle;
    }
    const location = [0];
    const located = callStatus((obstruction) =>
      this.context().native.ml_region_locate_point_f64(handle.value, x, y, location, obstruction),
    );
    return located.ok ? decodeLocation(location[0]) : located;
  }

  union(other: Region): Result<Region> {
    return this.binary(other, this.context().native.ml_region_union);
  }

  intersection(other: Region): Result<Region> {
    return this.binary(other, this.context().native.ml_region_intersection);
  }

  difference(other: Region): Result<Region> {
    return this.binary(other, this.context().native.ml_region_difference);
  }

  symmetricDifference(other: Region): Result<Region> {
    return this.binary(other, this.context().native.ml_region_symmetric_difference);
  }

  minkowskiSum(other: Region): Result<MorphologyResult> {
    const left = this.handle();
    if (!left.ok) {
      return left;
    }
    const right = borrowResource(other, this.context(), "region");
    return right.ok
      ? this.morphology(this.context().native.ml_region_minkowski_sum, left.value, right.value)
      : right;
  }

  offset(element: StructuringElement): Result<MorphologyResult> {
    return this.withElement(element, this.context().native.ml_region_offset);
  }

  inset(element: StructuringElement): Result<MorphologyResult> {
    return this.withElement(element, this.context().native.ml_region_inset);
  }

  open(element: StructuringElement): Result<MorphologyResult> {
    return this.withElement(element, this.context().native.ml_region_open);
  }

  close(element: StructuringElement): Result<MorphologyResult> {
    return this.withElement(element, this.context().native.ml_region_close);
  }

  private binary(other: Region, operation: NativeApi["ml_region_union"]): Result<Region> {
    const left = this.handle();
    if (!left.ok) {
      return left;
    }
    const right = borrowResource(other, this.context(), "region");
    if (!right.ok) {
      return right;
    }
    const produced = produceHandle((output, obstruction) =>
      operation(left.value, right.value, output, obstruction),
    );
    return produced.ok ? success(createRegion(this.context(), produced.value)) : produced;
  }

  private withElement(
    element: StructuringElement,
    operation: NativeApi["ml_region_offset"],
  ): Result<MorphologyResult> {
    const region = this.handle();
    if (!region.ok) {
      return region;
    }
    const structuringElement = borrowResource(element, this.context(), "structuring-element");
    return structuringElement.ok
      ? this.morphology(operation, structuringElement.value, region.value)
      : structuringElement;
  }

  private morphology(
    operation: NativeApi["ml_region_minkowski_sum"],
    first: NativeHandle,
    second: NativeHandle,
  ): Result<MorphologyResult> {
    const produced = produceMorphology((output, receipt, obstruction) =>
      operation(first, second, output, receipt, obstruction),
    );
    return produced.ok
      ? success({ region: createRegion(this.context(), produced.value.handle), receipt: produced.value.receipt })
      : produced;
  }

  private counts(handle: NativeHandle): Result<readonly [number, number, number]> {
    const componentCount: NativeInteger[] = [0];
    const loopCount: NativeInteger[] = [0];
    const pointCount: NativeInteger[] = [0];
    const counted = callStatus((obstruction) =>
      this.context().native.ml_region_counts(
        handle,
        componentCount,
        loopCount,
        pointCount,
        obstruction,
      ),
    );
    if (!counted.ok) {
      return counted;
    }
    const components = toSafeNumber(componentCount[0]);
    const loops = toSafeNumber(loopCount[0]);
    const points = toSafeNumber(pointCount[0]);
    if (!components.ok) {
      return failure(components.error);
    }
    if (!loops.ok) {
      return failure(loops.error);
    }
    if (!points.ok) {
      return failure(points.error);
    }
    return success([components.value, loops.value, points.value]);
  }

  private measureWithCapacity(handle: NativeHandle, capacity: number): Result<RegionValuations> {
    return captureNativeCall(() => {
      const euler: NativeInteger[] = [0];
      const areaRatio = Buffer.alloc(capacity);
      const areaBytes: NativeInteger[] = [0];
      const perimeterLower = [0];
      const perimeterUpper = [0];
      const obstruction: NativeObstruction = {};
      const status = this.context().native.ml_region_measure(
        handle,
        euler,
        areaRatio,
        capacity,
        areaBytes,
        perimeterLower,
        perimeterUpper,
        obstruction,
      );
      if (status === ML_STATUS_BUFFER_TOO_SMALL && obstruction.code === ML_OBSTRUCTION_BUFFER_TOO_SMALL) {
        const required = toSafeNumber(areaBytes[0]);
        return required.ok ? this.measureWithCapacity(handle, required.value + 1) : required;
      }
      const measured = decodeStatus(status, obstruction);
      if (!measured.ok) {
        return measured;
      }
      const byteCount = toSafeNumber(areaBytes[0]);
      if (!byteCount.ok) {
        return byteCount;
      }
      const ratio = areaRatio.subarray(0, byteCount.value).toString("ascii");
      const [numerator, denominator, remainder] = ratio.split("/");
      if (numerator === undefined || denominator === undefined || remainder !== undefined) {
        return failure({ kind: "invalid-native-result", message: "Moonlight returned a malformed exact-area ratio" });
      }
      return success({
        eulerCharacteristic: toBigInt(euler[0]),
        area: { numerator: BigInt(numerator), denominator: BigInt(denominator) },
        perimeterBounds: [perimeterLower[0] ?? 0, perimeterUpper[0] ?? 0],
      });
    });
  }
}