/* eslint-disable */ /** * This is a specialized version of earcut, updated to take into account links between outer edges and holes whenever possible. * Modified by Foundry LLC * * ISC License * * Copyright (c) 2016, Mapbox * * Permission to use, copy, modify, and/or distribute this software for any purpose * with or without fee is hereby granted, provided that the above copyright notice * and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF * THIS SOFTWARE. */ (function() { function earcutEdges(data, holeIndices) { const dim = 3; const hasHoles = holeIndices && holeIndices.length let outerLen = hasHoles ? holeIndices[0] * dim : data.length; let outerNode = linkedList(data, 0, outerLen, dim, true); const triangles = []; if ( !outerNode || outerNode.next === outerNode.prev ) return triangles; let minX, minY, maxX, maxY, x, y, z, invSize; if ( hasHoles ) outerNode = eliminateHoles(data, holeIndices, outerNode, dim); // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox if ( data.length > 240 ) { minX = maxX = data[0]; minY = maxY = data[1]; for ( let i = dim; i < outerLen; i += dim ) { x = data[i]; y = data[i + 1]; z = data[i + 2]; if ( x < minX ) minX = x; if ( y < minY ) minY = y; if ( x > maxX ) maxX = x; if ( y > maxY ) maxY = y; } // minX, minY and invSize are later used to transform coords into integers for z-order calculation invSize = Math.max(maxX - minX, maxY - minY); invSize = invSize !== 0 ? 1 / invSize : 0; } earcutLinked(outerNode, triangles, dim, minX, minY, invSize); return triangles; } // create a circular doubly linked list from polygon points in the specified winding order function linkedList(data, start, end, dim, clockwise) { let i, last; if ( clockwise === (signedArea(data, start, end, dim) > 0) ) { for ( i = start; i < end; i += dim ) last = insertNode(i, data[i], data[i + 1], data[i + 2], last); } else { for ( i = end - dim; i >= start; i -= dim ) last = insertNode(i, data[i], data[i + 1], data[i + 2], last); } if ( last && equals(last, last.next) ) { removeNode(last); last = last.next; } return last; } // eliminate colinear or duplicate points function filterPoints(start, end) { if ( !start ) return start; if ( !end ) end = start; let p = start, again; do { again = false; if ( !p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0) ) { removeNode(p); p = end = p.prev; if ( p === p.next ) break; again = true; } else { p = p.next; } } while ( again || p !== end ); return end; } // main ear slicing loop which triangulates a polygon (given as a linked list) function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass = 0, verifyEdges = true) { if ( !ear ) return; // interlink polygon nodes in z-order if ( !pass && invSize ) indexCurve(ear, minX, minY, invSize); let stop = ear, prev, next; // iterate through ears, slicing them one by one while ( ear.prev !== ear.next ) { prev = ear.prev; next = ear.next; if ( invSize ? isEarHashed(ear, minX, minY, invSize, verifyEdges) : isEar(ear, verifyEdges) ) { triangles.push(prev.i / dim); triangles.push(ear.i / dim); triangles.push(next.i / dim); removeNode(ear); // skipping the next vertex leads to less sliver triangles ear = next.next; stop = next.next; continue; } ear = next; // if we looped through the whole remaining polygon and can't find any more ears if ( ear === stop ) { // try filtering points and slicing again if ( !pass ) { earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1, verifyEdges); } else if ( pass === 1 ) { // if this didn't work, try curing all small self-intersections locally ear = cureLocalIntersections(filterPoints(ear), triangles, dim); earcutLinked(ear, triangles, dim, minX, minY, invSize, 2, false); } else if ( pass === 2 ) { splitEarcut(ear, triangles, dim, minX, minY, invSize); } break; } } } // check whether a polygon node forms a valid ear with adjacent nodes function isEar(ear, verifyEdges) { let a = ear.prev, b = ear, c = ear.next; if ( (a.z === b.z) && (a.z === c.z) && (b.z === c.z) && verifyEdges ) return false; if ( area(a, b, c) >= 0 ) return false; // reflex, can't be an ear // now make sure we don't have other points inside the potential ear let p = ear.next.next; while ( p !== ear.prev ) { if ( pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next) >= 0 ) return false; p = p.next; } return true; } function isEarHashed(ear, minX, minY, invSize, verifyEdges) { let a = ear.prev, b = ear, c = ear.next; if ( (a.z === b.z) && (a.z === c.z) && (b.z === c.z) && verifyEdges ) return false; // checking if edges depth are matching if ( area(a, b, c) >= 0 ) return false; // reflex, can't be an ear // triangle bbox; min & max are calculated like this for speed let minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x), minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y), maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x), maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y); // z-order range for the current triangle bbox; let minZ = zOrder(minTX, minTY, minX, minY, invSize), maxZ = zOrder(maxTX, maxTY, minX, minY, invSize); let p = ear.prevZ, n = ear.nextZ; // look for points inside the triangle in both directions while ( p && p.zc >= minZ && n && n.zc <= maxZ ) { if ( p !== ear.prev && p !== ear.next && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next) >= 0 ) return false; p = p.prevZ; if ( n !== ear.prev && n !== ear.next && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) && area(n.prev, n, n.next) >= 0 ) return false; n = n.nextZ; } // look for remaining points in decreasing z-order while ( p && p.zc >= minZ ) { if ( p !== ear.prev && p !== ear.next && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next) >= 0 ) return false; p = p.prevZ; } // look for remaining points in increasing z-order while ( n && n.zc <= maxZ ) { if ( n !== ear.prev && n !== ear.next && pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) && area(n.prev, n, n.next) >= 0 ) return false; n = n.nextZ; } return true; } // go through all polygon nodes and cure small local self-intersections function cureLocalIntersections(start, triangles, dim) { let p = start; do { let a = p.prev, b = p.next.next; if ( !equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a) && !((a.z === b.z) && (a.z === p.z) && (b.z === p.z)) ) { triangles.push(a.i / dim); triangles.push(p.i / dim); triangles.push(b.i / dim); // remove two nodes involved removeNode(p); removeNode(p.next); p = start = b; } p = p.next; } while ( p !== start ); return filterPoints(p); } // try splitting polygon into two and triangulate them independently function splitEarcut(start, triangles, dim, minX, minY, invSize) { // look for a valid diagonal that divides the polygon into two let a = start; do { let b = a.next.next; while ( b !== a.prev ) { if ( a.i !== b.i && isValidDiagonal(a, b) ) { // split the polygon in two by the diagonal let c = splitPolygon(a, b); // filter colinear points around the cuts a = filterPoints(a, a.next); c = filterPoints(c, c.next); // run earcut on each half earcutLinked(a, triangles, dim, minX, minY, invSize, false); earcutLinked(c, triangles, dim, minX, minY, invSize, false); return; } b = b.next; } a = a.next; } while ( a !== start ); } // link every hole into the outer loop, producing a single-ring polygon without holes function eliminateHoles(data, holeIndices, outerNode, dim) { let queue = [], i, len, start, end, list; for ( i = 0, len = holeIndices.length; i < len; i++ ) { start = holeIndices[i] * dim; end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; list = linkedList(data, start, end, dim, false); if ( list === list.next ) list.steiner = true; queue.push(getLeftmost(list)); } queue.sort(compareX); // process holes from left to right for ( i = 0; i < queue.length; i++ ) { outerNode = eliminateHole(queue[i], outerNode); outerNode = filterPoints(outerNode, outerNode.next); } return outerNode; } function compareX(a, b) { return a.x - b.x; } // find a bridge between vertices that connects hole with an outer ring and and link it function eliminateHole(hole, outerNode) { let bridge = findHoleBridge(hole, outerNode); if ( !bridge ) { return outerNode; } let bridgeReverse = splitPolygon(bridge, hole); // filter collinear points around the cuts let filteredBridge = filterPoints(bridge, bridge.next); filterPoints(bridgeReverse, bridgeReverse.next); // Check if input node was removed by the filtering return outerNode === bridge ? filteredBridge : outerNode; } // David Eberly's algorithm for finding a bridge between hole and outer polygon function findHoleBridge(hole, outerNode) { let p = outerNode, hx = hole.x, hy = hole.y, qx = -Infinity, m; // find a segment intersected by a ray from the hole's leftmost point to the left; // segment's endpoint with lesser x will be potential connection point do { if ( hy <= p.y && hy >= p.next.y && p.next.y !== p.y ) { let x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y); if ( x <= hx && x > qx ) { qx = x; if ( x === hx ) { if ( hy === p.y ) return p; if ( hy === p.next.y ) return p.next; } m = p.x < p.next.x ? p : p.next; } } p = p.next; } while ( p !== outerNode ); if ( !m ) return null; if ( hx === qx ) return m; // hole touches outer segment; pick leftmost endpoint // look for points inside the triangle of hole point, segment intersection and endpoint; // if there are no points found, we have a valid connection; // otherwise choose the point of the minimum angle with the ray as connection point let stop = m, mx = m.x, my = m.y, tanMin = Infinity, tan; p = m; 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) ) { tan = Math.abs(hy - p.y) / (hx - p.x); // tangential if ( locallyInside(p, hole) && (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 ); return m; } // whether sector in vertex m contains sector in vertex p in the same coordinates function sectorContainsSector(m, p) { return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0; } // interlink polygon nodes in z-order function indexCurve(start, minX, minY, invSize) { let p = start; do { if ( p.zc === null ) p.zc = zOrder(p.x, p.y, minX, minY, invSize); p.prevZ = p.prev; p.nextZ = p.next; p = p.next; } while ( p !== start ); p.prevZ.nextZ = null; p.prevZ = null; sortLinked(p); } // Simon Tatham's linked list merge sort algorithm // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html function sortLinked(list) { let i, p, q, e, tail, numMerges, pSize, qSize, inSize = 1; do { p = list; list = null; tail = null; numMerges = 0; while ( p ) { numMerges++; q = p; pSize = 0; for ( i = 0; i < inSize; i++ ) { pSize++; q = q.nextZ; if ( !q ) break; } qSize = inSize; while ( pSize > 0 || (qSize > 0 && q) ) { if ( pSize !== 0 && (qSize === 0 || !q || p.zc <= q.zc) ) { 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; } tail.nextZ = null; inSize *= 2; } while ( numMerges > 1 ); return list; } // z-order of a point given coords and inverse of the longer side of data bbox function zOrder(x, y, minX, minY, invSize) { // coords are transformed into non-negative 15-bit integer range x = 32767 * (x - minX) * invSize; y = 32767 * (y - minY) * invSize; x = (x | (x << 8)) & 0x00FF00FF; x = (x | (x << 4)) & 0x0F0F0F0F; x = (x | (x << 2)) & 0x33333333; x = (x | (x << 1)) & 0x55555555; y = (y | (y << 8)) & 0x00FF00FF; y = (y | (y << 4)) & 0x0F0F0F0F; y = (y | (y << 2)) & 0x33333333; y = (y | (y << 1)) & 0x55555555; return x | (y << 1); } // find the leftmost node of a polygon ring function getLeftmost(start) { let p = start, leftmost = start; do { if ( p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y) ) leftmost = p; p = p.next; } while ( p !== start ); return leftmost; } // check if a point lies within a convex triangle function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) { return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 && (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 && (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0; } // check if a diagonal between two polygon nodes is valid (lies in polygon interior) function isValidDiagonal(a, b) { return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones'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 equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case } // signed area of a triangle function area(p, q, r) { return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); } // check if two points are equal function equals(p1, p2) { return p1.x === p2.x && p1.y === p2.y; } // check if two segments intersect function intersects(p1, q1, p2, q2) { let o1 = sign(area(p1, q1, p2)); let o2 = sign(area(p1, q1, q2)); let o3 = sign(area(p2, q2, p1)); let o4 = sign(area(p2, q2, q1)); if ( o1 !== o2 && o3 !== o4 ) return true; // general case if ( o1 === 0 && onSegment(p1, p2, q1) ) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1 if ( o2 === 0 && onSegment(p1, q2, q1) ) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1 if ( o3 === 0 && onSegment(p2, p1, q2) ) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2 if ( o4 === 0 && onSegment(p2, q1, q2) ) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2 return false; } // for collinear points p, q, r, check if point q lies on segment pr function onSegment(p, q, r) { 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 function intersectsPolygon(a, b) { 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; } while ( p !== a ); return false; } // check if a polygon diagonal is locally inside the polygon function locallyInside(a, b) { return area(a.prev, a, a.next) < 0 ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; } // check if the middle point of a polygon diagonal is inside the polygon function middleInside(a, b) { let p = a, inside = false, px = (a.x + b.x) / 2, py = (a.y + b.y) / 2; 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) ) inside = !inside; p = p.next; } while ( p !== a ); return inside; } // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two; // if one belongs to the outer ring and another to a hole, it merges it into a single ring function splitPolygon(a, b) { let a2 = new Node(a.i, a.x, a.y, a.z), b2 = new Node(b.i, b.x, b.y, b.z), an = a.next, bp = b.prev; a.next = b; b.prev = a; a2.next = an; an.prev = a2; b2.next = a2; a2.prev = b2; bp.next = b2; b2.prev = bp; return b2; } // create a node and optionally link it with previous one (in a circular doubly linked list) function insertNode(i, x, y, z, last) { let p = new Node(i, x, y, z); if ( !last ) { p.prev = p; p.next = p; } else { p.next = last.next; p.prev = last; last.next.prev = p; last.next = p; } return p; } function removeNode(p) { p.next.prev = p.prev; p.prev.next = p.next; if ( p.prevZ ) p.prevZ.nextZ = p.nextZ; if ( p.nextZ ) p.nextZ.prevZ = p.prevZ; } function Node(i, x, y, z) { // vertex index in coordinates array this.i = i; // vertex coordinates this.x = x; this.y = y; this.z = z; // depth // previous and next vertex nodes in a polygon ring this.prev = null; this.next = null; // z-order curve value this.zc = null; // previous and next nodes in z-order this.prevZ = null; this.nextZ = null; // indicates whether this is a steiner point this.steiner = false; } // return a percentage difference between the polygon area and its triangulation area; // used to verify correctness of triangulation earcutEdges.deviation = function(data, holeIndices, dim, triangles) { let hasHoles = holeIndices && holeIndices.length; let outerLen = hasHoles ? holeIndices[0] * dim : data.length; let polygonArea = Math.abs(signedArea(data, 0, outerLen, dim)); if ( hasHoles ) { for ( let i = 0, len = holeIndices.length; i < len; i++ ) { let start = holeIndices[i] * dim; let end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; polygonArea -= Math.abs(signedArea(data, start, end, dim)); } } let trianglesArea = 0; for ( let i = 0; i < triangles.length; i += 3 ) { let a = triangles[i] * dim; let b = triangles[i + 1] * dim; let c = triangles[i + 2] * dim; trianglesArea += Math.abs((data[a] - data[c]) * (data[b + 1] - data[a + 1]) - (data[a] - data[b]) * (data[c + 1] - data[a + 1])); } return polygonArea === 0 && trianglesArea === 0 ? 0 : Math.abs((trianglesArea - polygonArea) / polygonArea); }; function signedArea(data, start, end, dim) { let sum = 0; for ( let i = start, j = end - dim; i < end; i += dim ) { sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); j = i; } return sum; } // turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts earcutEdges.flatten = function(data) { let dim = data[0][0].length, result = {vertices: [], holes: [], dimensions: dim}, holeIndex = 0; for ( let i = 0; i < data.length; i++ ) { for ( let j = 0; j < data[i].length; j++ ) { for ( let d = 0; d < dim; d++ ) result.vertices.push(data[i][j][d]); } if ( i > 0 ) { holeIndex += data[i - 1].length; result.holes.push(holeIndex); } } return result; }; globalThis.earcut = {earcutEdges}; })();