🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

earcut

Package Overview
Dependencies
Maintainers
29
Versions
45
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

earcut - npm Package Compare versions

Comparing version
3.0.2
to
3.1.0
+73
src/earcut.d.ts
/**
* Triangulate a polygon given as a flat array of vertex coordinates.
*
* @param {ArrayLike<number>} data flat array of vertex coordinates
* @param {ArrayLike<number> | null} [holeIndices] indices (in vertices, not coordinates) where each hole ring starts
* @param {number} [dim=2] number of coordinates per vertex in `data`
* @returns {number[]} triangles as triplets of vertex indices into `data`
* @example earcut([10,0, 0,50, 60,60, 70,10]); // [1,0,3, 3,2,1]
*/
export default function earcut(data: ArrayLike<number>, holeIndices?: ArrayLike<number> | null, dim?: number): number[];
/**
* Return the relative difference between the polygon area and the area of its triangulation —
* a value near 0 means a correct triangulation. Useful for verifying output in tests.
*
* @param {ArrayLike<number>} data
* @param {ArrayLike<number> | null} holeIndices
* @param {number} dim number of coordinates per vertex in `data`
* @param {ArrayLike<number>} triangles output of {@link earcut}
* @returns {number}
* @example deviation(data, holes, dim, earcut(data, holes, dim)); // ~0 if correct
*/
export function deviation(data: ArrayLike<number>, holeIndices: ArrayLike<number> | null, dim: number, triangles: ArrayLike<number>): number;
/**
* Turn a polygon in multi-dimensional array form (e.g. as in GeoJSON) into the flat form Earcut accepts.
*
* @param {ReadonlyArray<ReadonlyArray<ArrayLike<number>>>} data array of rings; the first ring is the outer contour, the rest are holes
* @returns {{vertices: number[], holes: number[], dimensions: number}}
* @example const {vertices, holes, dimensions} = flatten(geojson.coordinates);
*/
export function flatten(data: ReadonlyArray<ReadonlyArray<ArrayLike<number>>>): {
vertices: number[];
holes: number[];
dimensions: number;
};
/**
* A vertex in a circular doubly linked list representing a polygon ring.
* `prev`/`next` are always linked (set immediately after {@link createNode}), so they're typed
* non-null; `prevZ`/`nextZ` are the z-order list links and are null at the ends.
*/
export type Node = {
/**
* vertex index in the coordinates array
*/
i: number;
/**
* vertex x coordinate
*/
x: number;
/**
* vertex y coordinate
*/
y: number;
/**
* previous vertex node in the polygon ring
*/
prev: Node;
/**
* next vertex node in the polygon ring
*/
next: Node;
/**
* z-order curve value; doubles as the owning block index during eliminateHoles
*/
z: number;
/**
* previous node in z-order
*/
prevZ: Node | null;
/**
* next node in z-order
*/
nextZ: Node | null;
};
+367
-160

@@ -7,2 +7,32 @@ (function (global, factory) {

/**
* A vertex in a circular doubly linked list representing a polygon ring.
* `prev`/`next` are always linked (set immediately after {@link createNode}), so they're typed
* non-null; `prevZ`/`nextZ` are the z-order list links and are null at the ends.
*
* @typedef {object} Node
* @property {number} i vertex index in the coordinates array
* @property {number} x vertex x coordinate
* @property {number} y vertex y coordinate
* @property {Node} prev previous vertex node in the polygon ring
* @property {Node} next next vertex node in the polygon ring
* @property {number} z z-order curve value; doubles as the owning block index during eliminateHoles
* @property {Node | null} prevZ previous node in z-order
* @property {Node | null} nextZ next node in z-order
*/
// single-vertex holes to preserve through filterPoints (steiner points); kept off the Node
// shape since they're rare — the empty-set fast path means non-steiner inputs pay nothing
/** @type {Set<Node>} */
const steiners = new Set();
/**
* Triangulate a polygon given as a flat array of vertex coordinates.
*
* @param {ArrayLike<number>} data flat array of vertex coordinates
* @param {ArrayLike<number> | null} [holeIndices] indices (in vertices, not coordinates) where each hole ring starts
* @param {number} [dim=2] number of coordinates per vertex in `data`
* @returns {number[]} triangles as triplets of vertex indices into `data`
* @example earcut([10,0, 0,50, 60,60, 70,10]); // [1,0,3, 3,2,1]
*/
function earcut(data, holeIndices, dim = 2) {

@@ -12,3 +42,6 @@

const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
if (steiners.size) steiners.clear();
let outerNode = linkedList(data, 0, outerLen, dim, true);
/** @type {number[]} */
const triangles = [];

@@ -18,5 +51,9 @@

let minX, minY, invSize;
let minX = 0, minY = 0, invSize = 0;
if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
if (hasHoles) {
outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
// collapse collinear/coincident points across the whole merged ring once before clipping
outerNode = filterPoints(outerNode);
}

@@ -50,4 +87,6 @@ // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox

// create a circular doubly linked list from polygon points in the specified winding order
/** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @param {boolean} clockwise @returns {Node | null} */
function linkedList(data, start, end, dim, clockwise) {
let last;
/** @type {Node | null} */
let last = null;

@@ -68,20 +107,23 @@ if (clockwise === (signedArea(data, start, end, dim) > 0)) {

// eliminate colinear or duplicate points
function filterPoints(start, end) {
if (!start) return start;
if (!end) end = start;
// Remove collinear or coincident points; removability depends only on a node's immediate
// neighbors, so we sweep forward and re-check the predecessor after each removal. With no `end`
// we sweep the whole ring, lapping until nothing is removable (the fixpoint the clipper needs).
// With an explicit `end` we heal only the dirty window around a bridge/diagonal cut, stopping at
// `end` rather than lapping — O(window) instead of O(ring).
/** @param {Node} start @param {Node} [end] @returns {Node} */
function filterPoints(start, end = start) {
const full = end === start;
let p = start,
again;
let p = start, again;
do {
again = false;
if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
if (p !== p.next && (steiners.size === 0 || !steiners.has(p)) &&
(equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
if (full || p === end) end = p.prev; // pull the stop bound back past the removal
removeNode(p);
p = end = p.prev;
if (p === p.next) break;
p = p.prev; // re-check the predecessor
again = true;
} else {
} else if (full || p !== end) {
p = p.next;
again = !full; // local heal: keep looping until the sweep reaches end
}

@@ -94,2 +136,3 @@ } while (again || p !== end);

// main ear slicing loop which triangulates a polygon (given as a linked list)
/** @param {Node | null} ear @param {number[]} triangles @param {number} dim @param {number} minX @param {number} minY @param {number} invSize @param {number} pass */
function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {

@@ -106,5 +149,6 @@ if (!ear) return;

const prev = ear.prev;
/** @type {Node} */
const next = ear.next;
if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
if (area(prev, ear, next) < 0 && (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear))) {
triangles.push(prev.i, ear.i, next.i); // cut off the triangle

@@ -114,5 +158,4 @@

// skipping the next vertex leads to less sliver triangles
ear = next.next;
stop = next.next;
ear = next;
stop = next;

@@ -146,2 +189,3 @@ continue;

// check whether a polygon node forms a valid ear with adjacent nodes
/** @param {Node} ear @returns {boolean} */
function isEar(ear) {

@@ -152,5 +196,7 @@ const a = ear.prev,

if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// reflex check (area(a, b, c) >= 0) is hoisted into the earcutLinked caller to avoid non-inlined call here
// now make sure we don't have other points inside the potential ear
// make sure we don't have other points inside the potential ear; the point-in-triangle
// test (false when the point coincides with the first vertex a) is inlined here and in
// isEarHashed rather than called — V8 doesn't inline it and the call sits in the hot loop
const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

@@ -167,3 +213,3 @@

if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
!(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) &&
area(p.prev, p, p.next) >= 0) return false;

@@ -176,2 +222,3 @@ p = p.next;

/** @param {Node} ear @param {number} minX @param {number} minY @param {number} invSize @returns {boolean} */
function isEarHashed(ear, minX, minY, invSize) {

@@ -182,3 +229,3 @@ const a = ear.prev,

if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// reflex check is hoisted into the earcutLinked caller (see isEar)

@@ -202,8 +249,8 @@ const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

while (p && p.z >= minZ && n && n.z <= maxZ) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== c &&
!(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== c &&
!(ax === n.x && ay === n.y) && (cx - n.x) * (ay - n.y) >= (ax - n.x) * (cy - n.y) && (ax - n.x) * (by - n.y) >= (bx - n.x) * (ay - n.y) && (bx - n.x) * (cy - n.y) >= (cx - n.x) * (by - n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;

@@ -214,4 +261,4 @@ }

while (p && p.z >= minZ) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== c &&
!(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;

@@ -222,4 +269,4 @@ }

while (n && n.z <= maxZ) {
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== c &&
!(ax === n.x && ay === n.y) && (cx - n.x) * (ay - n.y) >= (ax - n.x) * (cy - n.y) && (ax - n.x) * (by - n.y) >= (bx - n.x) * (ay - n.y) && (bx - n.x) * (cy - n.y) >= (cx - n.x) * (by - n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;

@@ -232,4 +279,6 @@ }

// go through all polygon nodes and cure small local self-intersections
/** @param {Node} start @param {number[]} triangles @returns {Node} */
function cureLocalIntersections(start, triangles) {
let p = start;
let cured = false;
do {

@@ -239,3 +288,3 @@ const a = p.prev,

if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
if (intersects(a, p, p.next, b, false) && locallyInside(a, b) && locallyInside(b, a)) {

@@ -249,2 +298,3 @@ triangles.push(a.i, p.i, b.i);

p = start = b;
cured = true;
}

@@ -254,6 +304,7 @@ p = p.next;

return filterPoints(p);
return cured ? filterPoints(p) : p;
}
// try splitting polygon into two and triangulate them independently
/** @param {Node} start @param {number[]} triangles @param {number} dim @param {number} minX @param {number} minY @param {number} invSize */
function splitEarcut(start, triangles, dim, minX, minY, invSize) {

@@ -284,3 +335,7 @@ // look for a valid diagonal that divides the polygon into two

// true only while eliminateHoles merges holes, so removeNode keeps the block index live (growBlock)
let indexActive = false;
// link every hole into the outer loop, producing a single-ring polygon without holes
/** @param {ArrayLike<number>} data @param {ArrayLike<number>} holeIndices @param {Node} outerNode @param {number} dim @returns {Node} */
function eliminateHoles(data, holeIndices, outerNode, dim) {

@@ -292,4 +347,4 @@ const queue = [];

const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
const list = linkedList(data, start, end, dim, false);
if (list === list.next) list.steiner = true;
const list = /** @type {Node} */ (linkedList(data, start, end, dim, false));
if (list === list.next) steiners.add(list);
queue.push(getLeftmost(list));

@@ -300,6 +355,14 @@ }

// process holes from left to right
// block-bbox index for findHoleBridge, grown append-only as holes merge (see notes
// above buildBlockIndex). Seed it with the outer ring, then append each merged hole.
buildBlockIndex(data.length / dim, holeIndices.length);
indexSegment(outerNode, outerNode);
// process holes from left to right; indexActive lets removeNode keep block bboxes live as
// filterPoints heals edges during merges (see growBlock)
indexActive = true;
for (let i = 0; i < queue.length; i++) {
outerNode = eliminateHole(queue[i], outerNode);
}
indexActive = false;

@@ -309,18 +372,13 @@ return outerNode;

/** @param {Node} a @param {Node} b @returns {number} */
function compareXYSlope(a, b) {
let result = a.x - b.x;
// when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
// the bridge to the outer shell is always the point that they meet at.
if (result === 0) {
result = a.y - b.y;
if (result === 0) {
const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
result = aSlope - bSlope;
}
}
return result;
return a.x - b.x || a.y - b.y ||
(a.next.y - a.y) / (a.next.x - a.x) -
(b.next.y - b.y) / (b.next.x - b.x);
}
// find a bridge between vertices that connects hole with an outer ring and link it
/** @param {Node} hole @param {Node} outerNode @returns {Node} */
function eliminateHole(hole, outerNode) {

@@ -334,3 +392,10 @@ const bridge = findHoleBridge(hole, outerNode);

// filter collinear points around the cuts
// index the merged-in segment before filtering: in ring order the splice runs
// bridge -> hole -> bridgeReverse -> bridge2 -> (bridge's old next), covering the
// hole's edges and both new slit edges. filterPoints below only drops collinear /
// coincident points, so these bboxes stay valid (conservative) supersets.
const bridge2 = bridgeReverse.next;
indexSegment(bridge, bridge2.next);
// heal collinear/coincident points around the two new slit edges
filterPoints(bridgeReverse, bridgeReverse.next);

@@ -340,3 +405,90 @@ return filterPoints(bridge, bridge.next);

// Block-bbox index for findHoleBridge (issue #183): one [minX,minY,maxX,maxY] bbox per K
// consecutive ring edges, in a flat Float64Array, so the leftward-ray scan can skip whole
// blocks in O(1) instead of walking the entire merged ring. Grown append-only — the outer
// ring seeds it, then each merged hole appends a segment (head node, stop node, K-blocks
// over head..stop); independent segments, not a ring tiling, since splices land mid-ring.
// Buffers are sized once from the input upper bound and reused across calls.
//
// filterPoints only drops collinear/coincident points, so a stale bbox stays a conservative
// superset of its live edges (never a false skip); the scan skips dead nodes (p.prev.next !==
// p) and lazily advances a dead stop. Blocks are scanned in append (not ring) order, so the
// chosen bridge can differ from the un-indexed code — a different but equally valid result.
const K = 16; // edges per block
let blockBBox = new Float64Array(0); // [minX,minY,maxX,maxY] per block
let numBlocks = 0;
/** @type {Node[]} */
const blockHead = []; // first node of each block's segment
/** @type {Node[]} */
const blockStop = []; // node just past each block's segment (exclusive walk bound)
/** @param {number} maxNodes @param {number} numHoles */
function buildBlockIndex(maxNodes, numHoles) {
// upper bound: every input node indexed once, +2 bridge nodes per hole, plus a partial
// trailing block per appended segment (outer ring + one per hole)
const maxBlocks = Math.ceil((maxNodes + 2 * numHoles) / K) + numHoles + 2;
if (blockBBox.length < maxBlocks * 4) blockBBox = new Float64Array(maxBlocks * 4);
numBlocks = 0;
}
// index the ring run head..stop (exclusive) as ceil(len / K) blocks; head === stop means
// the whole ring. each block's bbox covers both endpoints of every edge it owns.
/** @param {Node} head @param {Node} stop */
function indexSegment(head, stop) {
let p = head;
do {
const b = numBlocks++;
blockHead[b] = p;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
let k = 0;
do {
const c = p.next; // edge p->c; bbox must bound both endpoints
p.z = b; // reuse z as the owning block during eliminateHoles (see growBlock)
if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y;
if (c.x < minX) minX = c.x; if (c.x > maxX) maxX = c.x;
if (c.y < minY) minY = c.y; if (c.y > maxY) maxY = c.y;
p = c;
} while (++k < K && p !== stop);
blockStop[b] = p;
const g = b * 4;
blockBBox[g] = minX; blockBBox[g + 1] = minY; blockBBox[g + 2] = maxX; blockBBox[g + 3] = maxY;
} while (p !== stop);
}
// when filterPoints heals an edge head->tail (removing the collinear node between them), the
// healed edge can extend past head's frozen block bbox if its old far endpoint lived in another
// block; grow head's block bbox to cover tail so the leftward-ray prune can't false-skip it.
/** @param {Node} head @param {Node} tail */
function growBlock(head, tail) {
const g = head.z * 4;
if (tail.x < blockBBox[g]) blockBBox[g] = tail.x;
if (tail.y < blockBBox[g + 1]) blockBBox[g + 1] = tail.y;
if (tail.x > blockBBox[g + 2]) blockBBox[g + 2] = tail.x;
if (tail.y > blockBBox[g + 3]) blockBBox[g + 3] = tail.y;
}
/** @param {number} b @returns {Node} */
function liveBlockStop(b) {
let stop = blockStop[b];
while (stop.prev.next !== stop) stop = stop.next;
blockStop[b] = stop;
return stop;
}
// the block's head node can be removed by filterPoints during merges; advance it to the next
// live node so the walk doesn't start on (and immediately terminate at) a dead node. For the
// single full-ring seed block (head === stop) the same forward advance keeps them equal, so the
// do-while still laps the whole ring instead of collapsing to an empty walk.
/** @param {number} b @returns {Node} */
function liveBlockHead(b) {
let head = blockHead[b];
while (head.prev.next !== head) head = head.next;
blockHead[b] = head;
return head;
}
// David Eberly's algorithm for finding a bridge between hole and outer polygon
/** @param {Node} hole @param {Node} outerNode @returns {Node | null} */
function findHoleBridge(hole, outerNode) {

@@ -347,2 +499,3 @@ let p = outerNode;

let qx = -Infinity;
/** @type {Node | undefined} */
let m;

@@ -354,14 +507,27 @@

if (equals(hole, p)) return p;
do {
if (equals(hole, p.next)) return p.next;
else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
if (x <= hx && x > qx) {
qx = x;
m = p.x < p.next.x ? p : p.next;
if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
// scan blocks; skip any whose bbox can't hold a crossing that beats qx and lies left
// of hx (the prune Morton order can't express — explicit per-axis [minY,maxY]/[minX,maxX])
for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
if (hy < blockBBox[g + 1] || hy > blockBBox[g + 3] || blockBBox[g] > hx || blockBBox[g + 2] <= qx) continue;
// ensure the walk's exclusive bound is live so we don't overrun into other blocks
const stop = liveBlockStop(b);
p = liveBlockHead(b);
do {
if (p.prev.next === p) { // skip nodes removed by filterPoints (stale in the index)
if (equals(hole, p.next)) return p.next;
else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
if (x <= hx && x > qx) {
qx = x;
m = p.x < p.next.x ? p : p.next;
if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
}
}
}
}
p = p.next;
} while (p !== outerNode);
p = p.next;
} while (p !== stop);
}

@@ -374,24 +540,33 @@ if (!m) return null;

const stop = m;
const mx = m.x;
const my = m.y;
const tminY = Math.min(hy, my); // the triangle's y span; x span is [mx, hx]
const tmaxY = Math.max(hy, my);
let tanMin = Infinity;
p = m;
// scan the same blocks; skip any whose bbox can't overlap the triangle's [mx,hx]×[tminY,tmaxY] box
for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
if (blockBBox[g + 2] < mx || blockBBox[g] > hx || blockBBox[g + 3] < tminY || blockBBox[g + 1] > tmaxY) continue;
do {
if (hx >= p.x && p.x >= mx && hx !== p.x &&
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
const stop = liveBlockStop(b);
const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
p = liveBlockHead(b);
do {
if (p.prev.next === p && hx >= p.x && p.x >= mx && hx !== p.x && // skip dead nodes
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
if (locallyInside(p, hole) &&
(tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
m = p;
tanMin = tan;
const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
// if hole point sits on p's horizontal edge (T-junction touch): the bridge runs
// along that edge — locallyInside rejects it as collinear, but it's valid
if ((locallyInside(p, hole) || (p.y === hy && p.next.y === hy && p.next.x > hx)) &&
(tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
m = p;
tanMin = tan;
}
}
}
p = p.next;
} while (p !== stop);
p = p.next;
} while (p !== stop);
}

@@ -402,2 +577,3 @@ return m;

// whether sector in vertex m contains sector in vertex p in the same coordinates
/** @param {Node} m @param {Node} p @returns {boolean} */
function sectorContainsSector(m, p) {

@@ -407,73 +583,65 @@ return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;

// interlink polygon nodes in z-order
// scratch array of node refs, reused across calls and grown on demand
/** @type {Node[]} */
const sortArr = [];
// interlink polygon nodes in z-order: collect into an array, quicksort by z, relink
/** @param {Node} start @param {number} minX @param {number} minY @param {number} invSize */
function indexCurve(start, minX, minY, invSize) {
let p = start;
let n = 0;
do {
if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
p.prevZ = p.prev;
p.nextZ = p.next;
// always (re)compute: z may still hold a block index left over from eliminateHoles
p.z = zOrder(p.x, p.y, minX, minY, invSize);
sortArr[n++] = p;
p = p.next;
} while (p !== start);
p.prevZ.nextZ = null;
p.prevZ = null;
quicksortNodes(sortArr, 0, n - 1);
sortLinked(p);
/** @type {Node | null} */
let prev = null;
for (let i = 0; i < n; i++) {
const node = sortArr[i];
node.prevZ = prev;
if (prev) prev.nextZ = node;
prev = node;
}
/** @type {Node} */ (prev).nextZ = null;
}
// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
function sortLinked(list) {
let numMerges;
let inSize = 1;
// quicksort an array of nodes by z; middle-element pivot + insertion sort for small ranges
/** @param {Node[]} arr @param {number} left @param {number} right */
function quicksortNodes(arr, left, right) {
while (right - left > 20) {
// middle pivot splits already-sorted/reversed runs evenly; real ring-order-by-z data
// is non-adversarial, so the median-of-three guard isn't needed
const pivot = arr[(left + right) >> 1].z;
do {
let p = list;
let e;
list = null;
let tail = null;
numMerges = 0;
while (p) {
numMerges++;
let q = p;
let pSize = 0;
for (let i = 0; i < inSize; i++) {
pSize++;
q = q.nextZ;
if (!q) break;
}
let qSize = inSize;
while (pSize > 0 || (qSize > 0 && q)) {
if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
e = p;
p = p.nextZ;
pSize--;
} else {
e = q;
q = q.nextZ;
qSize--;
}
if (tail) tail.nextZ = e;
else list = e;
e.prevZ = tail;
tail = e;
}
p = q;
let i = left, j = right, t;
while (i <= j) {
while (arr[i].z < pivot) i++;
while (arr[j].z > pivot) j--;
if (i <= j) { t = arr[i]; arr[i] = arr[j]; arr[j] = t; i++; j--; }
}
tail.nextZ = null;
inSize *= 2;
} while (numMerges > 1);
return list;
// recurse into the smaller half, loop on the larger to bound stack depth
if (j - left < right - i) {
quicksortNodes(arr, left, j);
left = i;
} else {
quicksortNodes(arr, i, right);
right = j;
}
}
// insertion sort the small remaining range
for (let i = left + 1; i <= right; i++) {
const node = arr[i], z = node.z;
let j = i - 1;
while (j >= left && arr[j].z > z) { arr[j + 1] = arr[j]; j--; }
arr[j + 1] = node;
}
}
// z-order of a point given coords and inverse of the longer side of data bbox
/** @param {number} x @param {number} y @param {number} minX @param {number} minY @param {number} invSize @returns {number} */
function zOrder(x, y, minX, minY, invSize) {

@@ -498,2 +666,3 @@ // coords are transformed into non-negative 15-bit integer range

// find the leftmost node of a polygon ring
/** @param {Node} start @returns {Node} */
function getLeftmost(start) {

@@ -511,2 +680,3 @@ let p = start,

// check if a point lies within a convex triangle
/** @param {number} ax @param {number} ay @param {number} bx @param {number} by @param {number} cx @param {number} cy @param {number} px @param {number} py @returns {boolean} */
function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {

@@ -518,12 +688,8 @@ return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&

// check if a point lies within a convex triangle but false if its equal to the first point of the triangle
function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
}
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
/** @param {Node} a @param {Node} b @returns {boolean} true when the diagonal is valid */
function isValidDiagonal(a, b) {
return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
return a.next.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
(locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
(area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
(area(a.prev, a, b.prev) !== 0 || area(a, b.prev, b) !== 0) || // does not create opposite-facing sectors
equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case

@@ -533,2 +699,3 @@ }

// signed area of a triangle
/** @param {Node} p @param {Node} q @param {Node} r @returns {number} */
function area(p, q, r) {

@@ -539,2 +706,3 @@ return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);

// check if two points are equal
/** @param {Node} p1 @param {Node} p2 @returns {boolean} */
function equals(p1, p2) {

@@ -544,11 +712,14 @@ return p1.x === p2.x && p1.y === p2.y;

// check if two segments intersect
function intersects(p1, q1, p2, q2) {
const o1 = sign(area(p1, q1, p2));
const o2 = sign(area(p1, q1, q2));
const o3 = sign(area(p2, q2, p1));
const o4 = sign(area(p2, q2, q1));
// check if two segments intersect; by default includes collinear boundary touches
/** @param {Node} p1 @param {Node} q1 @param {Node} p2 @param {Node} q2 @param {boolean} [includeBoundary] @returns {boolean} */
function intersects(p1, q1, p2, q2, includeBoundary = true) {
const o1 = area(p1, q1, p2);
const o2 = area(p1, q1, q2);
const o3 = area(p2, q2, p1);
const o4 = area(p2, q2, q1);
if (o1 !== o2 && o3 !== o4) return true; // general case
if (((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) && ((o3 > 0 && o4 < 0) || (o3 < 0 && o4 > 0))) return true;
if (!includeBoundary) return false;
if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1

@@ -563,2 +734,3 @@ if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1

// for collinear points p, q, r, check if point q lies on segment pr
/** @param {Node} p @param {Node} q @param {Node} r @returns {boolean} */
function onSegment(p, q, r) {

@@ -568,13 +740,23 @@ return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);

function sign(num) {
return num > 0 ? 1 : num < 0 ? -1 : 0;
}
// check if a polygon diagonal intersects any polygon segments
/** @param {Node} a @param {Node} b @returns {boolean} */
function intersectsPolygon(a, b) {
// diagonal bbox; an edge whose bbox can't overlap it can't intersect it, so
// skip the orientation test for those (the common case — the diagonal is short)
const minX = Math.min(a.x, b.x);
const maxX = Math.max(a.x, b.x);
const minY = Math.min(a.y, b.y);
const maxY = Math.max(a.y, b.y);
let p = a;
do {
if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
intersects(p, p.next, a, b)) return true;
p = p.next;
const n = p.next;
if ((p.x > maxX && n.x > maxX) || (p.x < minX && n.x < minX) ||
(p.y > maxY && n.y > maxY) || (p.y < minY && n.y < minY)) {
p = n;
continue;
}
if (p.i !== a.i && n.i !== a.i && p.i !== b.i && n.i !== b.i &&
intersects(p, n, a, b)) return true;
p = n;
} while (p !== a);

@@ -586,2 +768,3 @@

// check if a polygon diagonal is locally inside the polygon
/** @param {Node} a @param {Node} b @returns {boolean} */
function locallyInside(a, b) {

@@ -594,2 +777,3 @@ return area(a.prev, a, a.next) < 0 ?

// check if the middle point of a polygon diagonal is inside the polygon
/** @param {Node} a @param {Node} b @returns {boolean} */
function middleInside(a, b) {

@@ -601,6 +785,6 @@ let p = a;

do {
if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
(px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
const n = p.next;
if (((p.y > py) !== (n.y > py)) && (px < (n.x - p.x) * (py - p.y) / (n.y - p.y) + p.x))
inside = !inside;
p = p.next;
p = n;
} while (p !== a);

@@ -613,2 +797,3 @@

// if one belongs to the outer ring and another to a hole, it merges it into a single ring
/** @param {Node} a @param {Node} b @returns {Node} */
function splitPolygon(a, b) {

@@ -636,2 +821,3 @@ const a2 = createNode(a.i, a.x, a.y),

// create a node and optionally link it with previous one (in a circular doubly linked list)
/** @param {number} i @param {number} x @param {number} y @param {Node | null} last @returns {Node} */
function insertNode(i, x, y, last) {

@@ -653,2 +839,3 @@ const p = createNode(i, x, y);

/** @param {Node} p */
function removeNode(p) {

@@ -660,6 +847,11 @@ p.next.prev = p.prev;

if (p.nextZ) p.nextZ.prevZ = p.prevZ;
// keep the hole-bridge index's block bboxes covering the healed prev->next edge
if (indexActive) growBlock(p.prev, p.next);
}
/** @param {number} i @param {number} x @param {number} y @returns {Node} */
function createNode(i, x, y) {
return {
// prev/next are assigned by the caller before any read, so the null init is cast away here
return /** @type {Node} */ (/** @type {unknown} */ ({
i, // vertex index in coordinates array

@@ -669,11 +861,19 @@ x, y, // vertex coordinates

next: null,
z: 0, // z-order curve value
z: 0, // z-order curve value; doubles as owning block in the hole-bridge index during eliminateHoles
prevZ: null, // previous and next nodes in z-order
nextZ: null,
steiner: false // indicates whether this is a steiner point
};
nextZ: null
}));
}
// return a percentage difference between the polygon area and its triangulation area;
// used to verify correctness of triangulation
/**
* Return the relative difference between the polygon area and the area of its triangulation —
* a value near 0 means a correct triangulation. Useful for verifying output in tests.
*
* @param {ArrayLike<number>} data
* @param {ArrayLike<number> | null} holeIndices
* @param {number} dim number of coordinates per vertex in `data`
* @param {ArrayLike<number>} triangles output of {@link earcut}
* @returns {number}
* @example deviation(data, holes, dim, earcut(data, holes, dim)); // ~0 if correct
*/
function deviation(data, holeIndices, dim, triangles) {

@@ -706,2 +906,3 @@ const hasHoles = holeIndices && holeIndices.length;

/** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @returns {number} */
function signedArea(data, start, end, dim) {

@@ -716,3 +917,9 @@ let sum = 0;

// turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
/**
* Turn a polygon in multi-dimensional array form (e.g. as in GeoJSON) into the flat form Earcut accepts.
*
* @param {ReadonlyArray<ReadonlyArray<ArrayLike<number>>>} data array of rings; the first ring is the outer contour, the rest are holes
* @returns {{vertices: number[], holes: number[], dimensions: number}}
* @example const {vertices, holes, dimensions} = flatten(geojson.coordinates);
*/
function flatten(data) {

@@ -719,0 +926,0 @@ const vertices = [];

@@ -1,1 +0,1 @@

!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).earcut={})}(this,function(e){"use strict";function t(e,t,n,r,x){let i;if(x===k(e,t,n,r)>0)for(let x=t;x<n;x+=r)i=w(x/r|0,e[x],e[x+1],i);else for(let x=n-r;x>=t;x-=r)i=w(x/r|0,e[x],e[x+1],i);return i&&Z(i,i.next)&&(z(i),i=i.next),i}function n(e,t){if(!e)return e;t||(t=e);let n,r=e;do{if(n=!1,r.steiner||!Z(r,r.next)&&0!==a(r.prev,r,r.next))r=r.next;else{if(z(r),r=t=r.prev,r===r.next)break;n=!0}}while(n||r!==t);return t}function r(e,t,f,l,c,p,s){if(!e)return;!s&&p&&function(e,t,n,r){let x=e;do{0===x.z&&(x.z=y(x.x,x.y,t,n,r)),x.prevZ=x.prev,x.nextZ=x.next,x=x.next}while(x!==e);x.prevZ.nextZ=null,x.prevZ=null,function(e){let t,n=1;do{let r,x=e;e=null;let i=null;for(t=0;x;){t++;let o=x,u=0;for(let e=0;e<n&&(u++,o=o.nextZ,o);e++);let f=n;for(;u>0||f>0&&o;)0!==u&&(0===f||!o||x.z<=o.z)?(r=x,x=x.nextZ,u--):(r=o,o=o.nextZ,f--),i?i.nextZ=r:e=r,r.prevZ=i,i=r;x=o}i.nextZ=null,n*=2}while(t>1)}(x)}(e,l,c,p);let v=e;for(;e.prev!==e.next;){const y=e.prev,h=e.next;if(p?i(e,l,c,p):x(e))t.push(y.i,e.i,h.i),z(e),e=h.next,v=h.next;else if((e=h)===v){s?1===s?r(e=o(n(e),t),t,f,l,c,p,2):2===s&&u(e,t,f,l,c,p):r(n(e),t,f,l,c,p,1);break}}}function x(e){const t=e.prev,n=e,r=e.next;if(a(t,n,r)>=0)return!1;const x=t.x,i=n.x,o=r.x,u=t.y,f=n.y,l=r.y,c=Math.min(x,i,o),y=Math.min(u,f,l),p=Math.max(x,i,o),s=Math.max(u,f,l);let h=r.next;for(;h!==t;){if(h.x>=c&&h.x<=p&&h.y>=y&&h.y<=s&&v(x,u,i,f,o,l,h.x,h.y)&&a(h.prev,h,h.next)>=0)return!1;h=h.next}return!0}function i(e,t,n,r){const x=e.prev,i=e,o=e.next;if(a(x,i,o)>=0)return!1;const u=x.x,f=i.x,l=o.x,c=x.y,p=i.y,s=o.y,h=Math.min(u,f,l),Z=Math.min(c,p,s),d=Math.max(u,f,l),M=Math.max(c,p,s),m=y(h,Z,t,n,r),g=y(d,M,t,n,r);let b=e.prevZ,w=e.nextZ;for(;b&&b.z>=m&&w&&w.z<=g;){if(b.x>=h&&b.x<=d&&b.y>=Z&&b.y<=M&&b!==x&&b!==o&&v(u,c,f,p,l,s,b.x,b.y)&&a(b.prev,b,b.next)>=0)return!1;if(b=b.prevZ,w.x>=h&&w.x<=d&&w.y>=Z&&w.y<=M&&w!==x&&w!==o&&v(u,c,f,p,l,s,w.x,w.y)&&a(w.prev,w,w.next)>=0)return!1;w=w.nextZ}for(;b&&b.z>=m;){if(b.x>=h&&b.x<=d&&b.y>=Z&&b.y<=M&&b!==x&&b!==o&&v(u,c,f,p,l,s,b.x,b.y)&&a(b.prev,b,b.next)>=0)return!1;b=b.prevZ}for(;w&&w.z<=g;){if(w.x>=h&&w.x<=d&&w.y>=Z&&w.y<=M&&w!==x&&w!==o&&v(u,c,f,p,l,s,w.x,w.y)&&a(w.prev,w,w.next)>=0)return!1;w=w.nextZ}return!0}function o(e,t){let r=e;do{const n=r.prev,x=r.next.next;!Z(n,x)&&d(n,r,r.next,x)&&g(n,x)&&g(x,n)&&(t.push(n.i,r.i,x.i),z(r),z(r.next),r=e=x),r=r.next}while(r!==e);return n(r)}function u(e,t,x,i,o,u){let f=e;do{let e=f.next.next;for(;e!==f.prev;){if(f.i!==e.i&&h(f,e)){let l=b(f,e);return f=n(f,f.next),l=n(l,l.next),r(f,t,x,i,o,u,0),void r(l,t,x,i,o,u,0)}e=e.next}f=f.next}while(f!==e)}function f(e,t){let n=e.x-t.x;if(0===n&&(n=e.y-t.y,0===n)){n=(e.next.y-e.y)/(e.next.x-e.x)-(t.next.y-t.y)/(t.next.x-t.x)}return n}function l(e,t){const r=function(e,t){let n=t;const r=e.x,x=e.y;let i,o=-1/0;if(Z(e,n))return n;do{if(Z(e,n.next))return n.next;if(x<=n.y&&x>=n.next.y&&n.next.y!==n.y){const e=n.x+(x-n.y)*(n.next.x-n.x)/(n.next.y-n.y);if(e<=r&&e>o&&(o=e,i=n.x<n.next.x?n:n.next,e===r))return i}n=n.next}while(n!==t);if(!i)return null;const u=i,f=i.x,l=i.y;let y=1/0;n=i;do{if(r>=n.x&&n.x>=f&&r!==n.x&&s(x<l?r:o,x,f,l,x<l?o:r,x,n.x,n.y)){const t=Math.abs(x-n.y)/(r-n.x);g(n,e)&&(t<y||t===y&&(n.x>i.x||n.x===i.x&&c(i,n)))&&(i=n,y=t)}n=n.next}while(n!==u);return i}(e,t);if(!r)return t;const x=b(r,e);return n(x,x.next),n(r,r.next)}function c(e,t){return a(e.prev,e,t.prev)<0&&a(t.next,e,e.next)<0}function y(e,t,n,r,x){return(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=(e-n)*x|0)|e<<8))|e<<4))|e<<2))|e<<1))|(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=(t-r)*x|0)|t<<8))|t<<4))|t<<2))|t<<1))<<1}function p(e){let t=e,n=e;do{(t.x<n.x||t.x===n.x&&t.y<n.y)&&(n=t),t=t.next}while(t!==e);return n}function s(e,t,n,r,x,i,o,u){return(x-o)*(t-u)>=(e-o)*(i-u)&&(e-o)*(r-u)>=(n-o)*(t-u)&&(n-o)*(i-u)>=(x-o)*(r-u)}function v(e,t,n,r,x,i,o,u){return!(e===o&&t===u)&&s(e,t,n,r,x,i,o,u)}function h(e,t){return e.next.i!==t.i&&e.prev.i!==t.i&&!function(e,t){let n=e;do{if(n.i!==e.i&&n.next.i!==e.i&&n.i!==t.i&&n.next.i!==t.i&&d(n,n.next,e,t))return!0;n=n.next}while(n!==e);return!1}(e,t)&&(g(e,t)&&g(t,e)&&function(e,t){let n=e,r=!1;const x=(e.x+t.x)/2,i=(e.y+t.y)/2;do{n.y>i!=n.next.y>i&&n.next.y!==n.y&&x<(n.next.x-n.x)*(i-n.y)/(n.next.y-n.y)+n.x&&(r=!r),n=n.next}while(n!==e);return r}(e,t)&&(a(e.prev,e,t.prev)||a(e,t.prev,t))||Z(e,t)&&a(e.prev,e,e.next)>0&&a(t.prev,t,t.next)>0)}function a(e,t,n){return(t.y-e.y)*(n.x-t.x)-(t.x-e.x)*(n.y-t.y)}function Z(e,t){return e.x===t.x&&e.y===t.y}function d(e,t,n,r){const x=m(a(e,t,n)),i=m(a(e,t,r)),o=m(a(n,r,e)),u=m(a(n,r,t));return x!==i&&o!==u||(!(0!==x||!M(e,n,t))||(!(0!==i||!M(e,r,t))||(!(0!==o||!M(n,e,r))||!(0!==u||!M(n,t,r)))))}function M(e,t,n){return t.x<=Math.max(e.x,n.x)&&t.x>=Math.min(e.x,n.x)&&t.y<=Math.max(e.y,n.y)&&t.y>=Math.min(e.y,n.y)}function m(e){return e>0?1:e<0?-1:0}function g(e,t){return a(e.prev,e,e.next)<0?a(e,t,e.next)>=0&&a(e,e.prev,t)>=0:a(e,t,e.prev)<0||a(e,e.next,t)<0}function b(e,t){const n=j(e.i,e.x,e.y),r=j(t.i,t.x,t.y),x=e.next,i=t.prev;return e.next=t,t.prev=e,n.next=x,x.prev=n,r.next=n,n.prev=r,i.next=r,r.prev=i,r}function w(e,t,n,r){const x=j(e,t,n);return r?(x.next=r.next,x.prev=r,r.next.prev=x,r.next=x):(x.prev=x,x.next=x),x}function z(e){e.next.prev=e.prev,e.prev.next=e.next,e.prevZ&&(e.prevZ.nextZ=e.nextZ),e.nextZ&&(e.nextZ.prevZ=e.prevZ)}function j(e,t,n){return{i:e,x:t,y:n,prev:null,next:null,z:0,prevZ:null,nextZ:null,steiner:!1}}function k(e,t,n,r){let x=0;for(let i=t,o=n-r;i<n;i+=r)x+=(e[o]-e[i])*(e[i+1]+e[o+1]),o=i;return x}e.default=function(e,n,x=2){const i=n&&n.length,o=i?n[0]*x:e.length;let u=t(e,0,o,x,!0);const c=[];if(!u||u.next===u.prev)return c;let y,s,v;if(i&&(u=function(e,n,r,x){const i=[];for(let r=0,o=n.length;r<o;r++){const u=t(e,n[r]*x,r<o-1?n[r+1]*x:e.length,x,!1);u===u.next&&(u.steiner=!0),i.push(p(u))}i.sort(f);for(let e=0;e<i.length;e++)r=l(i[e],r);return r}(e,n,u,x)),e.length>80*x){y=e[0],s=e[1];let t=y,n=s;for(let r=x;r<o;r+=x){const x=e[r],i=e[r+1];x<y&&(y=x),i<s&&(s=i),x>t&&(t=x),i>n&&(n=i)}v=Math.max(t-y,n-s),v=0!==v?32767/v:0}return r(u,c,x,y,s,v,0),c},e.deviation=function(e,t,n,r){const x=t&&t.length,i=x?t[0]*n:e.length;let o=Math.abs(k(e,0,i,n));if(x)for(let r=0,x=t.length;r<x;r++){const i=t[r]*n,u=r<x-1?t[r+1]*n:e.length;o-=Math.abs(k(e,i,u,n))}let u=0;for(let t=0;t<r.length;t+=3){const x=r[t]*n,i=r[t+1]*n,o=r[t+2]*n;u+=Math.abs((e[x]-e[o])*(e[i+1]-e[x+1])-(e[x]-e[i])*(e[o+1]-e[x+1]))}return 0===o&&0===u?0:Math.abs((u-o)/o)},e.flatten=function(e){const t=[],n=[],r=e[0][0].length;let x=0,i=0;for(const o of e){for(const e of o)for(let n=0;n<r;n++)t.push(e[n]);i&&(x+=i,n.push(x)),i=o.length}return{vertices:t,holes:n,dimensions:r}},Object.defineProperty(e,"__esModule",{value:!0})});
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).earcut={})}(this,function(t){"use strict";const n=new Set;function e(t,n,e,x,r){let o=null;if(r===C(t,n,e,x)>0)for(let r=n;r<e;r+=x)o=S(r/x|0,t[r],t[r+1],o);else for(let r=e-x;r>=n;r-=x)o=S(r/x|0,t[r],t[r+1],o);return o&&T(o,o.next)&&(q(o),o=o.next),o}function x(t,e=t){const x=e===t;let r,o=t;do{r=!1,o===o.next||0!==n.size&&n.has(o)||!T(o,o.next)&&0!==F(o.prev,o,o.next)?(x||o!==e)&&(o=o.next,r=!x):((x||o===e)&&(e=o.prev),q(o),o=o.prev,r=!0)}while(r||o!==e);return e}function r(t,n,e,u,l,c,s){if(!t)return;!s&&c&&function(t,n,e,x){let r=t,o=0;do{r.z=w(r.x,r.y,n,e,x),g[o++]=r,r=r.next}while(r!==t);z(g,0,o-1);let i=null;for(let t=0;t<o;t++){const n=g[t];n.prevZ=i,i&&(i.nextZ=n),i=n}i.nextZ=null}(t,u,l,c);let p=t;for(;t.prev!==t.next;){const h=t.prev,a=t.next;if(F(h,t,a)<0&&(c?i(t,u,l,c):o(t)))n.push(h.i,t.i,a.i),q(t),t=a,p=a;else if((t=a)===p){s?1===s?r(t=y(x(t),n),n,e,u,l,c,2):2===s&&f(t,n,e,u,l,c):r(x(t),n,e,u,l,c,1);break}}}function o(t){const n=t.prev,e=t,x=t.next,r=n.x,o=e.x,i=x.x,y=n.y,f=e.y,u=x.y,l=Math.min(r,o,i),c=Math.min(y,f,u),s=Math.max(r,o,i),p=Math.max(y,f,u);let h=x.next;for(;h!==n;){if(h.x>=l&&h.x<=s&&h.y>=c&&h.y<=p&&(r!==h.x||y!==h.y)&&(i-h.x)*(y-h.y)>=(r-h.x)*(u-h.y)&&(r-h.x)*(f-h.y)>=(o-h.x)*(y-h.y)&&(o-h.x)*(u-h.y)>=(i-h.x)*(f-h.y)&&F(h.prev,h,h.next)>=0)return!1;h=h.next}return!0}function i(t,n,e,x){const r=t.prev,o=t,i=t.next,y=r.x,f=o.x,u=i.x,l=r.y,c=o.y,s=i.y,p=Math.min(y,f,u),h=Math.min(l,c,s),a=Math.max(y,f,u),v=Math.max(l,c,s),d=w(p,h,n,e,x),M=w(a,v,n,e,x);let m=t.prevZ,Z=t.nextZ;for(;m&&m.z>=d&&Z&&Z.z<=M;){if(m.x>=p&&m.x<=a&&m.y>=h&&m.y<=v&&m!==i&&(y!==m.x||l!==m.y)&&(u-m.x)*(l-m.y)>=(y-m.x)*(s-m.y)&&(y-m.x)*(c-m.y)>=(f-m.x)*(l-m.y)&&(f-m.x)*(s-m.y)>=(u-m.x)*(c-m.y)&&F(m.prev,m,m.next)>=0)return!1;if(m=m.prevZ,Z.x>=p&&Z.x<=a&&Z.y>=h&&Z.y<=v&&Z!==i&&(y!==Z.x||l!==Z.y)&&(u-Z.x)*(l-Z.y)>=(y-Z.x)*(s-Z.y)&&(y-Z.x)*(c-Z.y)>=(f-Z.x)*(l-Z.y)&&(f-Z.x)*(s-Z.y)>=(u-Z.x)*(c-Z.y)&&F(Z.prev,Z,Z.next)>=0)return!1;Z=Z.nextZ}for(;m&&m.z>=d;){if(m.x>=p&&m.x<=a&&m.y>=h&&m.y<=v&&m!==i&&(y!==m.x||l!==m.y)&&(u-m.x)*(l-m.y)>=(y-m.x)*(s-m.y)&&(y-m.x)*(c-m.y)>=(f-m.x)*(l-m.y)&&(f-m.x)*(s-m.y)>=(u-m.x)*(c-m.y)&&F(m.prev,m,m.next)>=0)return!1;m=m.prevZ}for(;Z&&Z.z<=M;){if(Z.x>=p&&Z.x<=a&&Z.y>=h&&Z.y<=v&&Z!==i&&(y!==Z.x||l!==Z.y)&&(u-Z.x)*(l-Z.y)>=(y-Z.x)*(s-Z.y)&&(y-Z.x)*(c-Z.y)>=(f-Z.x)*(l-Z.y)&&(f-Z.x)*(s-Z.y)>=(u-Z.x)*(c-Z.y)&&F(Z.prev,Z,Z.next)>=0)return!1;Z=Z.nextZ}return!0}function y(t,n){let e=t,r=!1;do{const x=e.prev,o=e.next.next;_(x,e,e.next,o,!1)&&O(x,o)&&O(o,x)&&(n.push(x.i,e.i,o.i),q(e),q(e.next),e=t=o,r=!0),e=e.next}while(e!==t);return r?x(e):e}function f(t,n,e,o,i,y){let f=t;do{let t=f.next.next;for(;t!==f.prev;){if(f.i!==t.i&&A(f,t)){let u=P(f,t);return f=x(f,f.next),u=x(u,u.next),r(f,n,e,o,i,y,0),void r(u,n,e,o,i,y,0)}t=t.next}f=f.next}while(f!==t)}let u=!1;function l(t,n){return t.x-n.x||t.y-n.y||(t.next.y-t.y)/(t.next.x-t.x)-(n.next.y-n.y)/(n.next.x-n.x)}function c(t,n){const e=function(t,n){let e=n;const x=t.x,r=t.y;let o,i=-1/0;if(T(t,e))return e;for(let n=0,y=0;n<h;n++,y+=4){if(r<p[y+1]||r>p[y+3]||p[y]>x||p[y+2]<=i)continue;const f=M(n);e=m(n);do{if(e.prev.next===e){if(T(t,e.next))return e.next;if(r<=e.y&&r>=e.next.y&&e.next.y!==e.y){const t=e.x+(r-e.y)*(e.next.x-e.x)/(e.next.y-e.y);if(t<=x&&t>i&&(i=t,o=e.x<e.next.x?e:e.next,t===x))return o}}e=e.next}while(e!==f)}if(!o)return null;const y=o.x,f=o.y,u=Math.min(r,f),l=Math.max(r,f);let c=1/0;for(let n=0,s=0;n<h;n++,s+=4){if(p[s+2]<y||p[s]>x||p[s+3]<u||p[s+1]>l)continue;const h=M(n);e=m(n);do{if(e.prev.next===e&&x>=e.x&&e.x>=y&&x!==e.x&&j(r<f?x:i,r,y,f,r<f?i:x,r,e.x,e.y)){const n=Math.abs(r-e.y)/(x-e.x);(O(e,t)||e.y===r&&e.next.y===r&&e.next.x>x)&&(n<c||n===c&&(e.x>o.x||e.x===o.x&&Z(o,e)))&&(o=e,c=n)}e=e.next}while(e!==h)}return o}(t,n);if(!e)return n;const r=P(e,t);return d(e,r.next.next),x(r,r.next),x(e,e.next)}const s=16;let p=new Float64Array(0),h=0;const a=[],v=[];function d(t,n){let e=t;do{const t=h++;a[t]=e;let x=1/0,r=1/0,o=-1/0,i=-1/0,y=0;do{const n=e.next;e.z=t,e.x<x&&(x=e.x),e.x>o&&(o=e.x),e.y<r&&(r=e.y),e.y>i&&(i=e.y),n.x<x&&(x=n.x),n.x>o&&(o=n.x),n.y<r&&(r=n.y),n.y>i&&(i=n.y),e=n}while(++y<s&&e!==n);v[t]=e;const f=4*t;p[f]=x,p[f+1]=r,p[f+2]=o,p[f+3]=i}while(e!==n)}function M(t){let n=v[t];for(;n.prev.next!==n;)n=n.next;return v[t]=n,n}function m(t){let n=a[t];for(;n.prev.next!==n;)n=n.next;return a[t]=n,n}function Z(t,n){return F(t.prev,t,n.prev)<0&&F(n.next,t,t.next)<0}const g=[];function z(t,n,e){for(;e-n>20;){const x=t[n+e>>1].z;let r,o=n,i=e;for(;o<=i;){for(;t[o].z<x;)o++;for(;t[i].z>x;)i--;o<=i&&(r=t[o],t[o]=t[i],t[i]=r,o++,i--)}i-n<e-o?(z(t,n,i),n=o):(z(t,o,e),e=i)}for(let x=n+1;x<=e;x++){const e=t[x],r=e.z;let o=x-1;for(;o>=n&&t[o].z>r;)t[o+1]=t[o],o--;t[o+1]=e}}function w(t,n,e,x,r){return(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=(t-e)*r|0)|t<<8))|t<<4))|t<<2))|t<<1))|(n=1431655765&((n=858993459&((n=252645135&((n=16711935&((n=(n-x)*r|0)|n<<8))|n<<4))|n<<2))|n<<1))<<1}function b(t){let n=t,e=t;do{(n.x<e.x||n.x===e.x&&n.y<e.y)&&(e=n),n=n.next}while(n!==t);return e}function j(t,n,e,x,r,o,i,y){return(r-i)*(n-y)>=(t-i)*(o-y)&&(t-i)*(x-y)>=(e-i)*(n-y)&&(e-i)*(o-y)>=(r-i)*(x-y)}function A(t,n){return t.next.i!==n.i&&!function(t,n){const e=Math.min(t.x,n.x),x=Math.max(t.x,n.x),r=Math.min(t.y,n.y),o=Math.max(t.y,n.y);let i=t;do{const y=i.next;if(i.x>x&&y.x>x||i.x<e&&y.x<e||i.y>o&&y.y>o||i.y<r&&y.y<r)i=y;else{if(i.i!==t.i&&y.i!==t.i&&i.i!==n.i&&y.i!==n.i&&_(i,y,t,n))return!0;i=y}}while(i!==t);return!1}(t,n)&&(O(t,n)&&O(n,t)&&function(t,n){let e=t,x=!1;const r=(t.x+n.x)/2,o=(t.y+n.y)/2;do{const t=e.next;e.y>o!=t.y>o&&r<(t.x-e.x)*(o-e.y)/(t.y-e.y)+e.x&&(x=!x),e=t}while(e!==t);return x}(t,n)&&(0!==F(t.prev,t,n.prev)||0!==F(t,n.prev,n))||T(t,n)&&F(t.prev,t,t.next)>0&&F(n.prev,n,n.next)>0)}function F(t,n,e){return(n.y-t.y)*(e.x-n.x)-(n.x-t.x)*(e.y-n.y)}function T(t,n){return t.x===n.x&&t.y===n.y}function _(t,n,e,x,r=!0){const o=F(t,n,e),i=F(t,n,x),y=F(e,x,t),f=F(e,x,n);return(o>0&&i<0||o<0&&i>0)&&(y>0&&f<0||y<0&&f>0)||!!r&&(!(0!==o||!k(t,e,n))||(!(0!==i||!k(t,x,n))||(!(0!==y||!k(e,t,x))||!(0!==f||!k(e,n,x)))))}function k(t,n,e){return n.x<=Math.max(t.x,e.x)&&n.x>=Math.min(t.x,e.x)&&n.y<=Math.max(t.y,e.y)&&n.y>=Math.min(t.y,e.y)}function O(t,n){return F(t.prev,t,t.next)<0?F(t,n,t.next)>=0&&F(t,t.prev,n)>=0:F(t,n,t.prev)<0||F(t,t.next,n)<0}function P(t,n){const e=B(t.i,t.x,t.y),x=B(n.i,n.x,n.y),r=t.next,o=n.prev;return t.next=n,n.prev=t,e.next=r,r.prev=e,x.next=e,e.prev=x,o.next=x,x.prev=o,x}function S(t,n,e,x){const r=B(t,n,e);return x?(r.next=x.next,r.prev=x,x.next.prev=r,x.next=r):(r.prev=r,r.next=r),r}function q(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ),u&&function(t,n){const e=4*t.z;n.x<p[e]&&(p[e]=n.x),n.y<p[e+1]&&(p[e+1]=n.y),n.x>p[e+2]&&(p[e+2]=n.x),n.y>p[e+3]&&(p[e+3]=n.y)}(t.prev,t.next)}function B(t,n,e){return{i:t,x:n,y:e,prev:null,next:null,z:0,prevZ:null,nextZ:null}}function C(t,n,e,x){let r=0;for(let o=n,i=e-x;o<e;o+=x)r+=(t[i]-t[o])*(t[o+1]+t[i+1]),i=o;return r}t.default=function(t,o,i=2){const y=o&&o.length,f=y?o[0]*i:t.length;n.size&&n.clear();let a=e(t,0,f,i,!0);const v=[];if(!a||a.next===a.prev)return v;let M=0,m=0,Z=0;if(y&&(a=function(t,x,r,o){const i=[];for(let r=0,y=x.length;r<y;r++){const f=e(t,x[r]*o,r<y-1?x[r+1]*o:t.length,o,!1);f===f.next&&n.add(f),i.push(b(f))}i.sort(l),function(t,n){const e=Math.ceil((t+2*n)/s)+n+2;p.length<4*e&&(p=new Float64Array(4*e));h=0}(t.length/o,x.length),d(r,r),u=!0;for(let t=0;t<i.length;t++)r=c(i[t],r);return u=!1,r}(t,o,a,i),a=x(a)),t.length>80*i){M=t[0],m=t[1];let n=M,e=m;for(let x=i;x<f;x+=i){const r=t[x],o=t[x+1];r<M&&(M=r),o<m&&(m=o),r>n&&(n=r),o>e&&(e=o)}Z=Math.max(n-M,e-m),Z=0!==Z?32767/Z:0}return r(a,v,i,M,m,Z,0),v},t.deviation=function(t,n,e,x){const r=n&&n.length,o=r?n[0]*e:t.length;let i=Math.abs(C(t,0,o,e));if(r)for(let x=0,r=n.length;x<r;x++){const o=n[x]*e,y=x<r-1?n[x+1]*e:t.length;i-=Math.abs(C(t,o,y,e))}let y=0;for(let n=0;n<x.length;n+=3){const r=x[n]*e,o=x[n+1]*e,i=x[n+2]*e;y+=Math.abs((t[r]-t[i])*(t[o+1]-t[r+1])-(t[r]-t[o])*(t[i+1]-t[r+1]))}return 0===i&&0===y?0:Math.abs((y-i)/i)},t.flatten=function(t){const n=[],e=[],x=t[0][0].length;let r=0,o=0;for(const i of t){for(const t of i)for(let e=0;e<x;e++)n.push(t[e]);o&&(r+=o,e.push(r)),o=i.length}return{vertices:n,holes:e,dimensions:x}},Object.defineProperty(t,"__esModule",{value:!0})});
ISC License
Copyright (c) 2024, Mapbox
Copyright (c) 2026, Mapbox

@@ -5,0 +5,0 @@ Permission to use, copy, modify, and/or distribute this software for any purpose

{
"name": "earcut",
"version": "3.0.2",
"version": "3.1.0",
"description": "The fastest and smallest JavaScript polygon triangulation library for your WebGL apps",

@@ -8,4 +8,6 @@ "main": "src/earcut.js",

"exports": "./src/earcut.js",
"types": "src/earcut.d.ts",
"files": [
"src/earcut.js",
"src/earcut.d.ts",
"dist/earcut.min.js",

@@ -15,28 +17,25 @@ "dist/earcut.dev.js"

"scripts": {
"pretest": "eslint src test/test.js bench/*.js viz/viz.js",
"pretest": "eslint src test/test.js bench/*.js viz/viz.js && tsc",
"test": "node --test",
"bench": "node bench/bench-tiles.js",
"build": "rollup -c",
"prepublishOnly": "npm run build",
"prepublishOnly": "npm test && npm run build",
"cov": "node --test --experimental-test-coverage"
},
"author": "Vladimir Agafonkin",
"author": "Volodymyr Agafonkin",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-terser": "^0.4.4",
"benchmark": "^2.1.4",
"eslint": "^9.31.0",
"eslint-config-mourner": "^4.1.0",
"rollup": "^4.45.1"
"@rollup/plugin-terser": "^1.0.0",
"eslint": "^10.6.0",
"eslint-config-mourner": "^5.0.1",
"rollup": "^4.62.2",
"typescript": "^6.0.3"
},
"eslintConfig": {
"extends": "mourner",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2020
}
},
"repository": {
"type": "git",
"url": "git://github.com/mapbox/earcut.git"
},
"allowScripts": {
"fsevents@2.3.3": true
}
}
+71
-56

@@ -1,5 +0,13 @@

## Earcut
# Earcut
The fastest and smallest JavaScript polygon triangulation library. 3KB gzipped.
The fastest and smallest JavaScript **polygon triangulation** library. 3.5KB gzipped.
It is designed to be fast enough for real-time triangulation in the browser,
sacrificing triangulation quality for raw speed and simplicity,
while being robust enough to handle most practical datasets without crashing or producing garbage.
Originally built for [Mapbox GL JS](https://www.mapbox.com/mapbox-gljs) (WebGL-based interactive maps),
it's now also used by [Three.js](https://threejs.org/) and many other projects.
![Earcut triangulation example](earcut.png)
[![Node](https://github.com/mapbox/earcut/actions/workflows/node.yml/badge.svg)](https://github.com/mapbox/earcut/actions/workflows/node.yml)

@@ -10,6 +18,13 @@ [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/mapbox/earcut.svg)](http://isitmaintained.com/project/mapbox/earcut "Average time to resolve an issue")

#### The algorithm
## Usage
```js
import earcut from 'earcut';
const triangles = earcut([10,0, 0,50, 60,60, 70,10]); // returns [1,0,3, 3,2,1]
```
## Algorithm
The library implements a modified ear slicing algorithm,
optimized by [z-order curve](http://en.wikipedia.org/wiki/Z-order_curve) hashing
optimized by [z-order curve](http://en.wikipedia.org/wiki/Z-order_curve) and spatial hashing
and extended to handle holes, twisted polygons, degeneracies and self-intersections

@@ -23,35 +38,55 @@ in a way that doesn't _guarantee_ correctness of triangulation,

#### Why another triangulation library?
## Performance
The aim of this project is to create a JS triangulation library
that is **fast enough for real-time triangulation in the browser**,
sacrificing triangulation quality for raw speed and simplicity,
while being robust enough to handle most practical datasets without crashing or producing garbage.
Some benchmarks using Node 0.12:
Earcut is heavily optimized for its primary workload — triangulating polygons from
[Mapbox Vector Tiles](https://github.com/mapbox/vector-tile-spec). On a representative
benchmark of **119,680 real-world polygons** (1.9M vertices) drawn from a window of map tiles
through zooms 4–16, it triangulates the whole set in **~480 ms** on a Macbook Pro M1 Pro (2021).
You can run the benchmark yourself with `npm run bench`.
(ops/sec) | pts | earcut | libtess | poly2tri | pnltri | polyk
------------------| ---- | --------- | -------- | -------- | --------- | ------
OSM building | 15 | _795,935_ | _50,640_ | _61,501_ | _122,966_ | _175,570_
dude shape | 94 | _35,658_ | _10,339_ | _8,784_ | _11,172_ | _13,557_
holed dude shape | 104 | _28,319_ | _8,883_ | _7,494_ | _2,130_ | n/a
complex OSM water | 2523 | _543_ | _77.54_ | failure | failure | n/a
huge OSM water | 5667 | _95_ | _29.30_ | failure | failure | n/a
## Robustness
The original use case it was created for is [Mapbox GL](https://www.mapbox.com/mapbox-gl), WebGL-based interactive maps.
Earcut does **not** guarantee a correct triangulation on arbitrary input — it trades quality
for speed, aiming to always produce an acceptable result on practical data without crashing or
emitting garbage. The input is assumed to be a valid polygon: rings that don't self-cross or
overlap, holes that stay inside the outer ring, and no duplicate or zero-length edges. On input
that breaks these assumptions, the result can be noticeably wrong — overlapping triangles, gaps,
or triangles outside the polygon. If correctness matters, clean your input first; for a
guaranteed-correct triangulation even on bad data, see
[libtess.js](https://github.com/brendankenny/libtess.js) (slower and larger).
If you want to get correct triangulation even on very bad data with lots of self-intersections
and earcut is not precise enough, take a look at [libtess.js](https://github.com/brendankenny/libtess.js).
The output is also not _conforming_ — a vertex may land in the middle of another triangle's edge
(a T-junction). This is harmless for rendering but can break navmesh or FEM use; if you need a
conforming mesh, [remove T-junctions in a post-process](https://github.com/mapbox/earcut/issues/74#issuecomment-4826113682).
#### Usage
## Install
Install with NPM: `npm install earcut`, then import as a module:
```js
const triangles = earcut([10,0, 0,50, 60,60, 70,10]); // returns [1,0,3, 3,2,1]
import earcut, {flatten, deviation} from 'earcut';
```
Signature: `earcut(vertices[, holes, dimensions = 2])`.
Or use as a module directly in the browser with [jsDelivr](https://www.jsdelivr.com/esm):
```html
<script type="module">
import earcut from 'https://cdn.jsdelivr.net/npm/earcut/+esm';
</script>
```
Alternatively, there's a UMD browser bundle with an `earcut` global variable (exposing the main function as `earcut.default`):
```html
<script src="https://cdn.jsdelivr.net/npm/earcut/dist/earcut.min.js"></script>
```
## API
### `earcut(vertices[, holes, dimensions = 2])`
* `vertices` is a flat array of vertex coordinates like `[x0,y0, x1,y1, x2,y2, ...]`.
* `holes` is an array of hole _indices_ if any
(e.g. `[5, 8]` for a 12-vertex input would mean one hole with vertices 5&ndash;7 and another with 8&ndash;11).
* `dimensions` is the number of coordinates per vertex in the input array (`2` by default). Only two are used for triangulation (`x` and `y`), and the rest are ignored.
* `dimensions` is the number of coordinates per vertex in the input array (`2` by default). Earcut is a **2D** algorithm: only `x` and `y` are used for triangulation, and any extra coordinates are ignored (3D data is treated as projected onto the XY plane).

@@ -72,16 +107,20 @@ Each group of three vertex indices in the resulting array forms a triangle.

Note that Earcut is a **2D** triangulation algorithm, and handles 3D data as if it was projected onto the XY plane (with Z component ignored).
Output triangles always have a consistent **winding order** regardless of the input polygon's winding &mdash; counter-clockwise in a y-up coordinate system (clockwise in y-down/screen space). If you need the opposite orientation (e.g. for back-face culling or normals in 3D), call `.reverse()` on the result.
### `flatten(data)`
If your input is a multi-dimensional array (e.g. [GeoJSON Polygon](http://geojson.org/geojson-spec.html#polygon)),
you can convert it to the format expected by Earcut with `earcut.flatten`:
you can convert it to the format expected by Earcut with `flatten`:
```js
const data = earcut.flatten(geojson.geometry.coordinates);
const data = flatten(geojson.geometry.coordinates);
const triangles = earcut(data.vertices, data.holes, data.dimensions);
```
After getting a triangulation, you can verify its correctness with `earcut.deviation`:
### `deviation(vertices, holes, dimensions, triangles)`
After getting a triangulation, you can verify its correctness with `deviation`:
```js
const deviation = earcut.deviation(vertices, holes, dimensions, triangles);
const d = deviation(vertices, holes, dimensions, triangles);
```

@@ -92,34 +131,10 @@

#### Install
## Ports to other languages
Install with NPM: `npm install earcut`, then import as a module:
```js
import earcut from 'earcut';
```
Or use as a module directly in the browser with [jsDelivr](https://www.jsdelivr.com/esm):
```html
<script type="module">
import earcut from 'https://cdn.jsdelivr.net/npm/earcut/+esm';
</script>
```
Alternatively, there's a UMD browser bundle with an `earcut` global variable (exposing the main function as `earcut.default`):
```html
<script src="https://cdn.jsdelivr.net/npm/earcut/dist/earcut.min.js"></script>
```
![](https://cloud.githubusercontent.com/assets/25395/5778431/e8ec0c10-9da3-11e4-8d4e-a2ced6a7d2b7.png)
#### Ports to other languages
- [mapbox/earcut.hpp](https://github.com/mapbox/earcut.hpp) (C++11)
- [JaffaKetchup/dart_earcut](https://github.com/JaffaKetchup/dart_earcut) (Dart)
- [earcut4j/earcut4j](https://github.com/earcut4j/earcut4j) (Java)
- [the3deers/earcut-java](https://github.com/the3deers/earcut-java) (Java)
- [Larpon/earcut](https://github.com/Larpon/earcut) (V)
- [Cawfree/earcut-j](https://github.com/Cawfree/earcut-j) (Java, outdated)
- [measuredweighed/SwiftEarcut](https://github.com/measuredweighed/SwiftEarcut) (Swift)
- [goswinr/Earcut](https://github.com/goswinr/Earcut/)(F# / .NET)
- [tenyoru/earcut.zig](https://github.com/tenyoru/earcut.zig) (Zig)
/**
* A vertex in a circular doubly linked list representing a polygon ring.
* `prev`/`next` are always linked (set immediately after {@link createNode}), so they're typed
* non-null; `prevZ`/`nextZ` are the z-order list links and are null at the ends.
*
* @typedef {object} Node
* @property {number} i vertex index in the coordinates array
* @property {number} x vertex x coordinate
* @property {number} y vertex y coordinate
* @property {Node} prev previous vertex node in the polygon ring
* @property {Node} next next vertex node in the polygon ring
* @property {number} z z-order curve value; doubles as the owning block index during eliminateHoles
* @property {Node | null} prevZ previous node in z-order
* @property {Node | null} nextZ next node in z-order
*/
// single-vertex holes to preserve through filterPoints (steiner points); kept off the Node
// shape since they're rare — the empty-set fast path means non-steiner inputs pay nothing
/** @type {Set<Node>} */
const steiners = new Set();
/**
* Triangulate a polygon given as a flat array of vertex coordinates.
*
* @param {ArrayLike<number>} data flat array of vertex coordinates
* @param {ArrayLike<number> | null} [holeIndices] indices (in vertices, not coordinates) where each hole ring starts
* @param {number} [dim=2] number of coordinates per vertex in `data`
* @returns {number[]} triangles as triplets of vertex indices into `data`
* @example earcut([10,0, 0,50, 60,60, 70,10]); // [1,0,3, 3,2,1]
*/
export default function earcut(data, holeIndices, dim = 2) {

@@ -6,3 +36,6 @@

const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
if (steiners.size) steiners.clear();
let outerNode = linkedList(data, 0, outerLen, dim, true);
/** @type {number[]} */
const triangles = [];

@@ -12,5 +45,9 @@

let minX, minY, invSize;
let minX = 0, minY = 0, invSize = 0;
if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
if (hasHoles) {
outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
// collapse collinear/coincident points across the whole merged ring once before clipping
outerNode = filterPoints(outerNode);
}

@@ -44,4 +81,6 @@ // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox

// create a circular doubly linked list from polygon points in the specified winding order
/** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @param {boolean} clockwise @returns {Node | null} */
function linkedList(data, start, end, dim, clockwise) {
let last;
/** @type {Node | null} */
let last = null;

@@ -62,20 +101,23 @@ if (clockwise === (signedArea(data, start, end, dim) > 0)) {

// eliminate colinear or duplicate points
function filterPoints(start, end) {
if (!start) return start;
if (!end) end = start;
// Remove collinear or coincident points; removability depends only on a node's immediate
// neighbors, so we sweep forward and re-check the predecessor after each removal. With no `end`
// we sweep the whole ring, lapping until nothing is removable (the fixpoint the clipper needs).
// With an explicit `end` we heal only the dirty window around a bridge/diagonal cut, stopping at
// `end` rather than lapping — O(window) instead of O(ring).
/** @param {Node} start @param {Node} [end] @returns {Node} */
function filterPoints(start, end = start) {
const full = end === start;
let p = start,
again;
let p = start, again;
do {
again = false;
if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
if (p !== p.next && (steiners.size === 0 || !steiners.has(p)) &&
(equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
if (full || p === end) end = p.prev; // pull the stop bound back past the removal
removeNode(p);
p = end = p.prev;
if (p === p.next) break;
p = p.prev; // re-check the predecessor
again = true;
} else {
} else if (full || p !== end) {
p = p.next;
again = !full; // local heal: keep looping until the sweep reaches end
}

@@ -88,2 +130,3 @@ } while (again || p !== end);

// main ear slicing loop which triangulates a polygon (given as a linked list)
/** @param {Node | null} ear @param {number[]} triangles @param {number} dim @param {number} minX @param {number} minY @param {number} invSize @param {number} pass */
function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {

@@ -100,5 +143,6 @@ if (!ear) return;

const prev = ear.prev;
/** @type {Node} */
const next = ear.next;
if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
if (area(prev, ear, next) < 0 && (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear))) {
triangles.push(prev.i, ear.i, next.i); // cut off the triangle

@@ -108,5 +152,4 @@

// skipping the next vertex leads to less sliver triangles
ear = next.next;
stop = next.next;
ear = next;
stop = next;

@@ -140,2 +183,3 @@ continue;

// check whether a polygon node forms a valid ear with adjacent nodes
/** @param {Node} ear @returns {boolean} */
function isEar(ear) {

@@ -146,5 +190,7 @@ const a = ear.prev,

if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// reflex check (area(a, b, c) >= 0) is hoisted into the earcutLinked caller to avoid non-inlined call here
// now make sure we don't have other points inside the potential ear
// make sure we don't have other points inside the potential ear; the point-in-triangle
// test (false when the point coincides with the first vertex a) is inlined here and in
// isEarHashed rather than called — V8 doesn't inline it and the call sits in the hot loop
const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

@@ -161,3 +207,3 @@

if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
!(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) &&
area(p.prev, p, p.next) >= 0) return false;

@@ -170,2 +216,3 @@ p = p.next;

/** @param {Node} ear @param {number} minX @param {number} minY @param {number} invSize @returns {boolean} */
function isEarHashed(ear, minX, minY, invSize) {

@@ -176,3 +223,3 @@ const a = ear.prev,

if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// reflex check is hoisted into the earcutLinked caller (see isEar)

@@ -196,8 +243,8 @@ const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

while (p && p.z >= minZ && n && n.z <= maxZ) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== c &&
!(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== c &&
!(ax === n.x && ay === n.y) && (cx - n.x) * (ay - n.y) >= (ax - n.x) * (cy - n.y) && (ax - n.x) * (by - n.y) >= (bx - n.x) * (ay - n.y) && (bx - n.x) * (cy - n.y) >= (cx - n.x) * (by - n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;

@@ -208,4 +255,4 @@ }

while (p && p.z >= minZ) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== c &&
!(ax === p.x && ay === p.y) && (cx - p.x) * (ay - p.y) >= (ax - p.x) * (cy - p.y) && (ax - p.x) * (by - p.y) >= (bx - p.x) * (ay - p.y) && (bx - p.x) * (cy - p.y) >= (cx - p.x) * (by - p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;

@@ -216,4 +263,4 @@ }

while (n && n.z <= maxZ) {
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== c &&
!(ax === n.x && ay === n.y) && (cx - n.x) * (ay - n.y) >= (ax - n.x) * (cy - n.y) && (ax - n.x) * (by - n.y) >= (bx - n.x) * (ay - n.y) && (bx - n.x) * (cy - n.y) >= (cx - n.x) * (by - n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;

@@ -226,4 +273,6 @@ }

// go through all polygon nodes and cure small local self-intersections
/** @param {Node} start @param {number[]} triangles @returns {Node} */
function cureLocalIntersections(start, triangles) {
let p = start;
let cured = false;
do {

@@ -233,3 +282,3 @@ const a = p.prev,

if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
if (intersects(a, p, p.next, b, false) && locallyInside(a, b) && locallyInside(b, a)) {

@@ -243,2 +292,3 @@ triangles.push(a.i, p.i, b.i);

p = start = b;
cured = true;
}

@@ -248,6 +298,7 @@ p = p.next;

return filterPoints(p);
return cured ? filterPoints(p) : p;
}
// try splitting polygon into two and triangulate them independently
/** @param {Node} start @param {number[]} triangles @param {number} dim @param {number} minX @param {number} minY @param {number} invSize */
function splitEarcut(start, triangles, dim, minX, minY, invSize) {

@@ -278,3 +329,7 @@ // look for a valid diagonal that divides the polygon into two

// true only while eliminateHoles merges holes, so removeNode keeps the block index live (growBlock)
let indexActive = false;
// link every hole into the outer loop, producing a single-ring polygon without holes
/** @param {ArrayLike<number>} data @param {ArrayLike<number>} holeIndices @param {Node} outerNode @param {number} dim @returns {Node} */
function eliminateHoles(data, holeIndices, outerNode, dim) {

@@ -286,4 +341,4 @@ const queue = [];

const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
const list = linkedList(data, start, end, dim, false);
if (list === list.next) list.steiner = true;
const list = /** @type {Node} */ (linkedList(data, start, end, dim, false));
if (list === list.next) steiners.add(list);
queue.push(getLeftmost(list));

@@ -294,6 +349,14 @@ }

// process holes from left to right
// block-bbox index for findHoleBridge, grown append-only as holes merge (see notes
// above buildBlockIndex). Seed it with the outer ring, then append each merged hole.
buildBlockIndex(data.length / dim, holeIndices.length);
indexSegment(outerNode, outerNode);
// process holes from left to right; indexActive lets removeNode keep block bboxes live as
// filterPoints heals edges during merges (see growBlock)
indexActive = true;
for (let i = 0; i < queue.length; i++) {
outerNode = eliminateHole(queue[i], outerNode);
}
indexActive = false;

@@ -303,18 +366,13 @@ return outerNode;

/** @param {Node} a @param {Node} b @returns {number} */
function compareXYSlope(a, b) {
let result = a.x - b.x;
// when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
// the bridge to the outer shell is always the point that they meet at.
if (result === 0) {
result = a.y - b.y;
if (result === 0) {
const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
result = aSlope - bSlope;
}
}
return result;
return a.x - b.x || a.y - b.y ||
(a.next.y - a.y) / (a.next.x - a.x) -
(b.next.y - b.y) / (b.next.x - b.x);
}
// find a bridge between vertices that connects hole with an outer ring and link it
/** @param {Node} hole @param {Node} outerNode @returns {Node} */
function eliminateHole(hole, outerNode) {

@@ -328,3 +386,10 @@ const bridge = findHoleBridge(hole, outerNode);

// filter collinear points around the cuts
// index the merged-in segment before filtering: in ring order the splice runs
// bridge -> hole -> bridgeReverse -> bridge2 -> (bridge's old next), covering the
// hole's edges and both new slit edges. filterPoints below only drops collinear /
// coincident points, so these bboxes stay valid (conservative) supersets.
const bridge2 = bridgeReverse.next;
indexSegment(bridge, bridge2.next);
// heal collinear/coincident points around the two new slit edges
filterPoints(bridgeReverse, bridgeReverse.next);

@@ -334,3 +399,90 @@ return filterPoints(bridge, bridge.next);

// Block-bbox index for findHoleBridge (issue #183): one [minX,minY,maxX,maxY] bbox per K
// consecutive ring edges, in a flat Float64Array, so the leftward-ray scan can skip whole
// blocks in O(1) instead of walking the entire merged ring. Grown append-only — the outer
// ring seeds it, then each merged hole appends a segment (head node, stop node, K-blocks
// over head..stop); independent segments, not a ring tiling, since splices land mid-ring.
// Buffers are sized once from the input upper bound and reused across calls.
//
// filterPoints only drops collinear/coincident points, so a stale bbox stays a conservative
// superset of its live edges (never a false skip); the scan skips dead nodes (p.prev.next !==
// p) and lazily advances a dead stop. Blocks are scanned in append (not ring) order, so the
// chosen bridge can differ from the un-indexed code — a different but equally valid result.
const K = 16; // edges per block
let blockBBox = new Float64Array(0); // [minX,minY,maxX,maxY] per block
let numBlocks = 0;
/** @type {Node[]} */
const blockHead = []; // first node of each block's segment
/** @type {Node[]} */
const blockStop = []; // node just past each block's segment (exclusive walk bound)
/** @param {number} maxNodes @param {number} numHoles */
function buildBlockIndex(maxNodes, numHoles) {
// upper bound: every input node indexed once, +2 bridge nodes per hole, plus a partial
// trailing block per appended segment (outer ring + one per hole)
const maxBlocks = Math.ceil((maxNodes + 2 * numHoles) / K) + numHoles + 2;
if (blockBBox.length < maxBlocks * 4) blockBBox = new Float64Array(maxBlocks * 4);
numBlocks = 0;
}
// index the ring run head..stop (exclusive) as ceil(len / K) blocks; head === stop means
// the whole ring. each block's bbox covers both endpoints of every edge it owns.
/** @param {Node} head @param {Node} stop */
function indexSegment(head, stop) {
let p = head;
do {
const b = numBlocks++;
blockHead[b] = p;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
let k = 0;
do {
const c = p.next; // edge p->c; bbox must bound both endpoints
p.z = b; // reuse z as the owning block during eliminateHoles (see growBlock)
if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y;
if (c.x < minX) minX = c.x; if (c.x > maxX) maxX = c.x;
if (c.y < minY) minY = c.y; if (c.y > maxY) maxY = c.y;
p = c;
} while (++k < K && p !== stop);
blockStop[b] = p;
const g = b * 4;
blockBBox[g] = minX; blockBBox[g + 1] = minY; blockBBox[g + 2] = maxX; blockBBox[g + 3] = maxY;
} while (p !== stop);
}
// when filterPoints heals an edge head->tail (removing the collinear node between them), the
// healed edge can extend past head's frozen block bbox if its old far endpoint lived in another
// block; grow head's block bbox to cover tail so the leftward-ray prune can't false-skip it.
/** @param {Node} head @param {Node} tail */
function growBlock(head, tail) {
const g = head.z * 4;
if (tail.x < blockBBox[g]) blockBBox[g] = tail.x;
if (tail.y < blockBBox[g + 1]) blockBBox[g + 1] = tail.y;
if (tail.x > blockBBox[g + 2]) blockBBox[g + 2] = tail.x;
if (tail.y > blockBBox[g + 3]) blockBBox[g + 3] = tail.y;
}
/** @param {number} b @returns {Node} */
function liveBlockStop(b) {
let stop = blockStop[b];
while (stop.prev.next !== stop) stop = stop.next;
blockStop[b] = stop;
return stop;
}
// the block's head node can be removed by filterPoints during merges; advance it to the next
// live node so the walk doesn't start on (and immediately terminate at) a dead node. For the
// single full-ring seed block (head === stop) the same forward advance keeps them equal, so the
// do-while still laps the whole ring instead of collapsing to an empty walk.
/** @param {number} b @returns {Node} */
function liveBlockHead(b) {
let head = blockHead[b];
while (head.prev.next !== head) head = head.next;
blockHead[b] = head;
return head;
}
// David Eberly's algorithm for finding a bridge between hole and outer polygon
/** @param {Node} hole @param {Node} outerNode @returns {Node | null} */
function findHoleBridge(hole, outerNode) {

@@ -341,2 +493,3 @@ let p = outerNode;

let qx = -Infinity;
/** @type {Node | undefined} */
let m;

@@ -348,14 +501,27 @@

if (equals(hole, p)) return p;
do {
if (equals(hole, p.next)) return p.next;
else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
if (x <= hx && x > qx) {
qx = x;
m = p.x < p.next.x ? p : p.next;
if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
// scan blocks; skip any whose bbox can't hold a crossing that beats qx and lies left
// of hx (the prune Morton order can't express — explicit per-axis [minY,maxY]/[minX,maxX])
for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
if (hy < blockBBox[g + 1] || hy > blockBBox[g + 3] || blockBBox[g] > hx || blockBBox[g + 2] <= qx) continue;
// ensure the walk's exclusive bound is live so we don't overrun into other blocks
const stop = liveBlockStop(b);
p = liveBlockHead(b);
do {
if (p.prev.next === p) { // skip nodes removed by filterPoints (stale in the index)
if (equals(hole, p.next)) return p.next;
else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
if (x <= hx && x > qx) {
qx = x;
m = p.x < p.next.x ? p : p.next;
if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
}
}
}
}
p = p.next;
} while (p !== outerNode);
p = p.next;
} while (p !== stop);
}

@@ -368,24 +534,33 @@ if (!m) return null;

const stop = m;
const mx = m.x;
const my = m.y;
const tminY = Math.min(hy, my); // the triangle's y span; x span is [mx, hx]
const tmaxY = Math.max(hy, my);
let tanMin = Infinity;
p = m;
// scan the same blocks; skip any whose bbox can't overlap the triangle's [mx,hx]×[tminY,tmaxY] box
for (let b = 0, g = 0; b < numBlocks; b++, g += 4) {
if (blockBBox[g + 2] < mx || blockBBox[g] > hx || blockBBox[g + 3] < tminY || blockBBox[g + 1] > tmaxY) continue;
do {
if (hx >= p.x && p.x >= mx && hx !== p.x &&
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
const stop = liveBlockStop(b);
const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
p = liveBlockHead(b);
do {
if (p.prev.next === p && hx >= p.x && p.x >= mx && hx !== p.x && // skip dead nodes
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
if (locallyInside(p, hole) &&
(tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
m = p;
tanMin = tan;
const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
// if hole point sits on p's horizontal edge (T-junction touch): the bridge runs
// along that edge — locallyInside rejects it as collinear, but it's valid
if ((locallyInside(p, hole) || (p.y === hy && p.next.y === hy && p.next.x > hx)) &&
(tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
m = p;
tanMin = tan;
}
}
}
p = p.next;
} while (p !== stop);
p = p.next;
} while (p !== stop);
}

@@ -396,2 +571,3 @@ return m;

// whether sector in vertex m contains sector in vertex p in the same coordinates
/** @param {Node} m @param {Node} p @returns {boolean} */
function sectorContainsSector(m, p) {

@@ -401,73 +577,65 @@ return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;

// interlink polygon nodes in z-order
// scratch array of node refs, reused across calls and grown on demand
/** @type {Node[]} */
const sortArr = [];
// interlink polygon nodes in z-order: collect into an array, quicksort by z, relink
/** @param {Node} start @param {number} minX @param {number} minY @param {number} invSize */
function indexCurve(start, minX, minY, invSize) {
let p = start;
let n = 0;
do {
if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
p.prevZ = p.prev;
p.nextZ = p.next;
// always (re)compute: z may still hold a block index left over from eliminateHoles
p.z = zOrder(p.x, p.y, minX, minY, invSize);
sortArr[n++] = p;
p = p.next;
} while (p !== start);
p.prevZ.nextZ = null;
p.prevZ = null;
quicksortNodes(sortArr, 0, n - 1);
sortLinked(p);
/** @type {Node | null} */
let prev = null;
for (let i = 0; i < n; i++) {
const node = sortArr[i];
node.prevZ = prev;
if (prev) prev.nextZ = node;
prev = node;
}
/** @type {Node} */ (prev).nextZ = null;
}
// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
function sortLinked(list) {
let numMerges;
let inSize = 1;
// quicksort an array of nodes by z; middle-element pivot + insertion sort for small ranges
/** @param {Node[]} arr @param {number} left @param {number} right */
function quicksortNodes(arr, left, right) {
while (right - left > 20) {
// middle pivot splits already-sorted/reversed runs evenly; real ring-order-by-z data
// is non-adversarial, so the median-of-three guard isn't needed
const pivot = arr[(left + right) >> 1].z;
do {
let p = list;
let e;
list = null;
let tail = null;
numMerges = 0;
while (p) {
numMerges++;
let q = p;
let pSize = 0;
for (let i = 0; i < inSize; i++) {
pSize++;
q = q.nextZ;
if (!q) break;
}
let qSize = inSize;
while (pSize > 0 || (qSize > 0 && q)) {
if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
e = p;
p = p.nextZ;
pSize--;
} else {
e = q;
q = q.nextZ;
qSize--;
}
if (tail) tail.nextZ = e;
else list = e;
e.prevZ = tail;
tail = e;
}
p = q;
let i = left, j = right, t;
while (i <= j) {
while (arr[i].z < pivot) i++;
while (arr[j].z > pivot) j--;
if (i <= j) { t = arr[i]; arr[i] = arr[j]; arr[j] = t; i++; j--; }
}
tail.nextZ = null;
inSize *= 2;
} while (numMerges > 1);
return list;
// recurse into the smaller half, loop on the larger to bound stack depth
if (j - left < right - i) {
quicksortNodes(arr, left, j);
left = i;
} else {
quicksortNodes(arr, i, right);
right = j;
}
}
// insertion sort the small remaining range
for (let i = left + 1; i <= right; i++) {
const node = arr[i], z = node.z;
let j = i - 1;
while (j >= left && arr[j].z > z) { arr[j + 1] = arr[j]; j--; }
arr[j + 1] = node;
}
}
// z-order of a point given coords and inverse of the longer side of data bbox
/** @param {number} x @param {number} y @param {number} minX @param {number} minY @param {number} invSize @returns {number} */
function zOrder(x, y, minX, minY, invSize) {

@@ -492,2 +660,3 @@ // coords are transformed into non-negative 15-bit integer range

// find the leftmost node of a polygon ring
/** @param {Node} start @returns {Node} */
function getLeftmost(start) {

@@ -505,2 +674,3 @@ let p = start,

// check if a point lies within a convex triangle
/** @param {number} ax @param {number} ay @param {number} bx @param {number} by @param {number} cx @param {number} cy @param {number} px @param {number} py @returns {boolean} */
function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {

@@ -512,12 +682,8 @@ return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&

// check if a point lies within a convex triangle but false if its equal to the first point of the triangle
function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
}
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
/** @param {Node} a @param {Node} b @returns {boolean} true when the diagonal is valid */
function isValidDiagonal(a, b) {
return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
return a.next.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
(locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
(area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
(area(a.prev, a, b.prev) !== 0 || area(a, b.prev, b) !== 0) || // does not create opposite-facing sectors
equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case

@@ -527,2 +693,3 @@ }

// signed area of a triangle
/** @param {Node} p @param {Node} q @param {Node} r @returns {number} */
function area(p, q, r) {

@@ -533,2 +700,3 @@ return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);

// check if two points are equal
/** @param {Node} p1 @param {Node} p2 @returns {boolean} */
function equals(p1, p2) {

@@ -538,11 +706,14 @@ return p1.x === p2.x && p1.y === p2.y;

// check if two segments intersect
function intersects(p1, q1, p2, q2) {
const o1 = sign(area(p1, q1, p2));
const o2 = sign(area(p1, q1, q2));
const o3 = sign(area(p2, q2, p1));
const o4 = sign(area(p2, q2, q1));
// check if two segments intersect; by default includes collinear boundary touches
/** @param {Node} p1 @param {Node} q1 @param {Node} p2 @param {Node} q2 @param {boolean} [includeBoundary] @returns {boolean} */
function intersects(p1, q1, p2, q2, includeBoundary = true) {
const o1 = area(p1, q1, p2);
const o2 = area(p1, q1, q2);
const o3 = area(p2, q2, p1);
const o4 = area(p2, q2, q1);
if (o1 !== o2 && o3 !== o4) return true; // general case
if (((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) && ((o3 > 0 && o4 < 0) || (o3 < 0 && o4 > 0))) return true;
if (!includeBoundary) return false;
if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1

@@ -557,2 +728,3 @@ if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1

// for collinear points p, q, r, check if point q lies on segment pr
/** @param {Node} p @param {Node} q @param {Node} r @returns {boolean} */
function onSegment(p, q, r) {

@@ -562,13 +734,23 @@ return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);

function sign(num) {
return num > 0 ? 1 : num < 0 ? -1 : 0;
}
// check if a polygon diagonal intersects any polygon segments
/** @param {Node} a @param {Node} b @returns {boolean} */
function intersectsPolygon(a, b) {
// diagonal bbox; an edge whose bbox can't overlap it can't intersect it, so
// skip the orientation test for those (the common case — the diagonal is short)
const minX = Math.min(a.x, b.x);
const maxX = Math.max(a.x, b.x);
const minY = Math.min(a.y, b.y);
const maxY = Math.max(a.y, b.y);
let p = a;
do {
if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
intersects(p, p.next, a, b)) return true;
p = p.next;
const n = p.next;
if ((p.x > maxX && n.x > maxX) || (p.x < minX && n.x < minX) ||
(p.y > maxY && n.y > maxY) || (p.y < minY && n.y < minY)) {
p = n;
continue;
}
if (p.i !== a.i && n.i !== a.i && p.i !== b.i && n.i !== b.i &&
intersects(p, n, a, b)) return true;
p = n;
} while (p !== a);

@@ -580,2 +762,3 @@

// check if a polygon diagonal is locally inside the polygon
/** @param {Node} a @param {Node} b @returns {boolean} */
function locallyInside(a, b) {

@@ -588,2 +771,3 @@ return area(a.prev, a, a.next) < 0 ?

// check if the middle point of a polygon diagonal is inside the polygon
/** @param {Node} a @param {Node} b @returns {boolean} */
function middleInside(a, b) {

@@ -595,6 +779,6 @@ let p = a;

do {
if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
(px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
const n = p.next;
if (((p.y > py) !== (n.y > py)) && (px < (n.x - p.x) * (py - p.y) / (n.y - p.y) + p.x))
inside = !inside;
p = p.next;
p = n;
} while (p !== a);

@@ -607,2 +791,3 @@

// if one belongs to the outer ring and another to a hole, it merges it into a single ring
/** @param {Node} a @param {Node} b @returns {Node} */
function splitPolygon(a, b) {

@@ -630,2 +815,3 @@ const a2 = createNode(a.i, a.x, a.y),

// create a node and optionally link it with previous one (in a circular doubly linked list)
/** @param {number} i @param {number} x @param {number} y @param {Node | null} last @returns {Node} */
function insertNode(i, x, y, last) {

@@ -647,2 +833,3 @@ const p = createNode(i, x, y);

/** @param {Node} p */
function removeNode(p) {

@@ -654,6 +841,11 @@ p.next.prev = p.prev;

if (p.nextZ) p.nextZ.prevZ = p.prevZ;
// keep the hole-bridge index's block bboxes covering the healed prev->next edge
if (indexActive) growBlock(p.prev, p.next);
}
/** @param {number} i @param {number} x @param {number} y @returns {Node} */
function createNode(i, x, y) {
return {
// prev/next are assigned by the caller before any read, so the null init is cast away here
return /** @type {Node} */ (/** @type {unknown} */ ({
i, // vertex index in coordinates array

@@ -663,11 +855,19 @@ x, y, // vertex coordinates

next: null,
z: 0, // z-order curve value
z: 0, // z-order curve value; doubles as owning block in the hole-bridge index during eliminateHoles
prevZ: null, // previous and next nodes in z-order
nextZ: null,
steiner: false // indicates whether this is a steiner point
};
nextZ: null
}));
}
// return a percentage difference between the polygon area and its triangulation area;
// used to verify correctness of triangulation
/**
* Return the relative difference between the polygon area and the area of its triangulation —
* a value near 0 means a correct triangulation. Useful for verifying output in tests.
*
* @param {ArrayLike<number>} data
* @param {ArrayLike<number> | null} holeIndices
* @param {number} dim number of coordinates per vertex in `data`
* @param {ArrayLike<number>} triangles output of {@link earcut}
* @returns {number}
* @example deviation(data, holes, dim, earcut(data, holes, dim)); // ~0 if correct
*/
export function deviation(data, holeIndices, dim, triangles) {

@@ -700,2 +900,3 @@ const hasHoles = holeIndices && holeIndices.length;

/** @param {ArrayLike<number>} data @param {number} start @param {number} end @param {number} dim @returns {number} */
function signedArea(data, start, end, dim) {

@@ -710,3 +911,9 @@ let sum = 0;

// turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
/**
* Turn a polygon in multi-dimensional array form (e.g. as in GeoJSON) into the flat form Earcut accepts.
*
* @param {ReadonlyArray<ReadonlyArray<ArrayLike<number>>>} data array of rings; the first ring is the outer contour, the rest are holes
* @returns {{vertices: number[], holes: number[], dimensions: number}}
* @example const {vertices, holes, dimensions} = flatten(geojson.coordinates);
*/
export function flatten(data) {

@@ -713,0 +920,0 @@ const vertices = [];