Skip to content

Commit

Permalink
[polygon] Add plane option to earcut (#308)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress authored Dec 29, 2022
1 parent cfc0015 commit d799ea1
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 44 deletions.
6 changes: 5 additions & 1 deletion docs/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ Codebase has been fully converted to typescript. In general this means that user
the types exported from math.gl to be considerably improved, however in some function signatures
are no longer supported. For details, consult the [upgrade guide](./upgrade-guide).

**`@math.gl/types` (NEW)
**`@math.gl/types` (NEW)**

- New module that exports a few typescript types that e.g. generalize handling of numeric arrays.

**`@math.gl/polygon` (NEW)**
- Includes earcut 2.2 (various bug fixes for edge cases)
- The `earcut` utility supports a new argument `plane` to calculate tesselation on alternative projection planes.

## v3.5

Release Date: July 14, 2021
Expand Down
3 changes: 2 additions & 1 deletion modules/polygon/docs/api-reference/earcut.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ earcut([0, 0, 100, 0, 100, 100, 0, 100, 20, 20, 80, 20, 80, 80, 20, 80], [4]);
## Usage

```js
earcut(positions[, holeIndices, size = 2, areas]);
earcut(positions[, holeIndices, size = 2, areas, plane]);
```

Arguments:
Expand All @@ -29,6 +29,7 @@ Arguments:
- `holeIndices` (Array, optional) - is an array of hole indices if any (e.g. [5, 8] for a 12-vertex input would mean one hole with vertices 5–7 and another with 8–11).
- `size` (Number, optional) - the number of elements in each vertex. Size `2` will interpret `positions` as `[x0, y0, x1, y1, ...]` and size `3` will interpret `positions` as `[x0, y0, z0, x1, y1, z1, ...]`. Default `2`.
- `areas` (Array, optional) - areas of outer polygon and holes as computed by `getPolygonSignedArea()`. Can be optionally supplied to speed up triangulation
- `plane` (String, optional) - the 2D projection plane on which to tesselate a 3D polygon on. One of `xy`, `yz`, `xz`. Default to `xy`

Returns:

Expand Down
5 changes: 3 additions & 2 deletions modules/polygon/docs/api-reference/polygon-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Fields:
- `end` (number) - End index of the polygon in the array of positions. Defaults to number of positions.
- `size` (Number) - Size of a point, 2 (XZ) or 3 (XYZ). Defaults to 2. Affects only polygons stored in flat arrays.
- `isClosed` (Boolean) - Indicates that the first point of the polygon is equal to the last point, and additional checks should be ommited.
- `plane` ('xy' | 'yz' | 'xz') - The 2D projection plane on which to calculate the area of a 3D polygon. Default `'xy'`.

## Functions

Expand All @@ -43,12 +44,12 @@ Returns true if the winding direction was changed.

Returns signed area of the polygon.

`getPolygonSignedArea(points, options)`
`getPolygonSignedArea(points, options, plane)`

Arguments:

- `points` (Array|TypedArray) - a flat array of the points that define the polygon.
- `options` (PolygonParams) - Polygon parameters.
- `options` (PolygonParams, optional) - Polygon parameters.

Returns:

Expand Down
79 changes: 49 additions & 30 deletions modules/polygon/src/earcut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@

/* eslint-disable */

import {getPolygonSignedArea} from './polygon-utils';
import type {NumericArray} from '@math.gl/core';
import {getPolygonSignedArea, DimIndex, Plane2D} from './polygon-utils';

/**
* Computes a triangulation of a polygon
Expand All @@ -38,14 +39,15 @@ import {getPolygonSignedArea} from './polygon-utils';
* Adapted from https://github.com/mapbox/earcut
*/
export function earcut(
positions: number[],
holeIndices?: number[],
positions: NumericArray,
holeIndices?: NumericArray,
dim: number = 2,
areas?: number[]
areas?: NumericArray,
plane: Plane2D = 'xy'
): number[] {
const hasHoles = holeIndices && holeIndices.length;
const outerLen = hasHoles ? holeIndices[0] * dim : positions.length;
let outerNode = linkedList(positions, 0, outerLen, dim, true, areas && areas[0]);
let outerNode = linkedList(positions, 0, outerLen, dim, true, areas && areas[0], plane);
const triangles = [];

if (!outerNode || outerNode.next === outerNode.prev) return triangles;
Expand All @@ -58,7 +60,7 @@ export function earcut(
let x;
let y;

if (hasHoles) outerNode = eliminateHoles(positions, holeIndices, outerNode, dim, areas);
if (hasHoles) outerNode = eliminateHoles(positions, holeIndices, outerNode, dim, areas, plane);

// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
if (positions.length > 80 * dim) {
Expand All @@ -85,20 +87,31 @@ export function earcut(
}

// create a circular doubly linked list from polygon points in the specified winding order
function linkedList(data, start, end, dim, clockwise, area) {
function linkedList(
data: NumericArray,
start: number,
end: number,
dim: number,
clockwise: boolean,
area: number | undefined,
plane: Plane2D
): Vertex {
let i;
let last;
if (area === undefined) {
area = getPolygonSignedArea(data, {start, end, size: dim});
area = getPolygonSignedArea(data, {start, end, size: dim, plane});
}

let i0 = DimIndex[plane[0]];
let i1 = DimIndex[plane[1]];
// Note that the signed area calculation in math.gl
// has the opposite sign to that which was originally
// present in earcut, thus the `< 0` is reversed
if (clockwise === area < 0) {
for (i = start; i < end; i += dim) last = insertNode(i, data[i], data[i + 1], last);
for (i = start; i < end; i += dim) last = insertNode(i, data[i + i0], data[i + i1], last);
} else {
for (i = end - dim; i >= start; i -= dim) last = insertNode(i, data[i], data[i + 1], last);
for (i = end - dim; i >= start; i -= dim)
last = insertNode(i, data[i + i0], data[i + i1], last);
}

if (last && equals(last, last.next)) {
Expand Down Expand Up @@ -372,7 +385,14 @@ function splitEarcut(start, triangles, dim, minX, minY, invSize) {
}

// link every hole into the outer loop, producing a single-ring polygon without holes
function eliminateHoles(data, holeIndices, outerNode, dim, areas) {
function eliminateHoles(
data: NumericArray,
holeIndices: NumericArray,
outerNode: Vertex,
dim: number,
areas: NumericArray | undefined,
plane: Plane2D
): Vertex {
const queue = [];
let i;
let len;
Expand All @@ -383,7 +403,7 @@ function eliminateHoles(data, holeIndices, outerNode, dim, areas) {
for (i = 0, len = holeIndices.length; i < len; i++) {
start = holeIndices[i] * dim;
end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
list = linkedList(data, start, end, dim, false, areas && areas[i + 1]);
list = linkedList(data, start, end, dim, false, areas && areas[i + 1], plane);
if (list === list.next) list.steiner = true;
queue.push(getLeftmost(list));
}
Expand Down Expand Up @@ -698,8 +718,8 @@ function middleInside(a, b) {
// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
// if one belongs to the outer ring and another to a hole, it merges it into a single ring
function splitPolygon(a, b) {
const a2 = new Node(a.i, a.x, a.y);
const b2 = new Node(b.i, b.x, b.y);
const a2 = new Vertex(a.i, a.x, a.y);
const b2 = new Vertex(b.i, b.x, b.y);
const an = a.next;
const bp = b.prev;

Expand All @@ -720,7 +740,7 @@ function splitPolygon(a, b) {

// create a node and optionally link it with previous one (in a circular doubly linked list)
function insertNode(i, x, y, last) {
const p = new Node(i, x, y);
const p = new Vertex(i, x, y);

if (!last) {
p.prev = p;
Expand All @@ -742,32 +762,31 @@ function removeNode(p) {
if (p.nextZ) p.nextZ.prevZ = p.prevZ;
}

class Node {
class Vertex {
// vertex index in coordinates array
i: number;

// vertex coordinates
x: number;
y: number;

// previous and next vertex nodes in a polygon ring
prev = null;
next = null;
prev: Vertex = null;
next: Vertex = null;

// z-order curve value
z: number;
z: number = 0;

// previous and next nodes in z-order
prevZ = null;
nextZ = null;
prevZ: Vertex = null;
nextZ: Vertex = null;

// indicates whether this is a steiner point
steiner = false;

i: number;
x: number;
y: number;
steiner: boolean = false;

constructor(i: number, x: number, y: number) {
// vertex index in coordinates array
this.i = i;

// vertex coordinates
this.x = x;
this.y = y;
this.z = 0;
}
}
50 changes: 42 additions & 8 deletions modules/polygon/src/polygon-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,34 @@ export type SegmentVisitorPoints = (
i2: number
) => void;

export type Plane2D = 'xy' | 'yz' | 'xz';

/** Parameters of a polygon. */
type PolygonParams = {
start?: number; // Start index of the polygon in the array of positions. Defaults to 0.
end?: number; // End index of the polygon in the array of positions. Defaults to number of positions.
size?: number; // Size of a point, 2 (XZ) or 3 (XYZ). Defaults to 2. Affects only polygons stored in flat arrays.
isClosed?: boolean; // Indicates that the first point of the polygon is equal to the last point, and additional checks should be ommited.
/**
* Start index of the polygon in the array of positions.
* @default `0`
*/
start?: number;
/**
* End index of the polygon in the array of positions.
* @default number of positions
*/
end?: number;
/**
* Size of a point, 2 (XZ) or 3 (XYZ). Affects only polygons stored in flat arrays.
* @default `2`
*/
size?: number;
/**
* Indicates that the first point of the polygon is equal to the last point, and additional checks should be ommited.
*/
isClosed?: boolean;
/**
* The 2D projection plane on which to calculate the area of a 3D polygon.
* @default `'xy'`
*/
plane?: Plane2D;
};

/**
Expand Down Expand Up @@ -71,6 +93,12 @@ export function getPolygonWindingDirection(
return Math.sign(getPolygonSignedArea(points, options));
}

export const DimIndex: Record<string, number> = {
x: 0,
y: 1,
z: 2
} as const;

/**
* Returns signed area of the polygon.
* @param points An array that represents points of the polygon.
Expand All @@ -79,11 +107,14 @@ export function getPolygonWindingDirection(
* https://en.wikipedia.org/wiki/Shoelace_formula
*/
export function getPolygonSignedArea(points: NumericArray, options: PolygonParams = {}): number {
const {start = 0, end = points.length} = options;
const {start = 0, end = points.length, plane = 'xy'} = options;
const dim = options.size || 2;
let area = 0;
const i0 = DimIndex[plane[0]];
const i1 = DimIndex[plane[1]];

for (let i = start, j = end - dim; i < end; i += dim) {
area += (points[i] - points[j]) * (points[i + 1] + points[j + 1]);
area += (points[i + i0] - points[j + i0]) * (points[i + i1] + points[j + i1]);
j = i;
}
return area / 2;
Expand Down Expand Up @@ -196,10 +227,13 @@ export function getPolygonSignedAreaPoints(
options: PolygonParams = {}
): number {
// https://en.wikipedia.org/wiki/Shoelace_formula
const {start = 0, end = points.length} = options;
const {start = 0, end = points.length, plane = 'xy'} = options;
let area = 0;
const i0 = DimIndex[plane[0]];
const i1 = DimIndex[plane[1]];

for (let i = start, j = end - 1; i < end; ++i) {
area += (points[i][0] - points[j][0]) * (points[i][1] + points[j][1]);
area += (points[i][i0] - points[j][i0]) * (points[i][i1] + points[j][i1]);
j = i;
}
return area / 2;
Expand Down
15 changes: 13 additions & 2 deletions modules/polygon/test/earcut.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,19 @@ test('indices-3d', function (t) {
t.end();
});

test('empty', function (t) {
t.same(earcut([]), []);
test('indices-3d', function (t) {
const indices = earcut([10, 4, 0, 0, 50, 0, 60, 60, 0, 70, 10, 0], null, 3);
t.same(indices, [1, 0, 3, 3, 2, 1]);
t.end();
});

test('projection', function (t) {
let indices = earcut([0, 4, 0, 0, 50, 0, 0, 60, 20, 0, 10, 20], null, 3, undefined, 'xy');
t.same(indices, []); // Polygon has no area on the XY plane

indices = earcut([0, 4, 0, 0, 50, 0, 0, 60, 20, 0, 10, 20], null, 3, undefined, 'yz');
t.same(indices, [2, 3, 0, 0, 1, 2]);

t.end();
});

Expand Down

0 comments on commit d799ea1

Please sign in to comment.