| /** | ||
| * 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})}); |
+1
-1
| 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 |
+15
-16
| { | ||
| "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. | ||
|  | ||
| [](https://github.com/mapbox/earcut/actions/workflows/node.yml) | ||
@@ -10,6 +18,13 @@ [](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–7 and another with 8–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 — 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> | ||
| ``` | ||
|  | ||
| #### 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) |
+367
-160
| /** | ||
| * 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 = []; |
90989
58.76%7
16.67%1611
42.19%137
12.3%