diff --git a/src/geom/polygon/Earcut.js b/src/geom/polygon/Earcut.js index e4babbf1e..3e55e3a82 100644 --- a/src/geom/polygon/Earcut.js +++ b/src/geom/polygon/Earcut.js @@ -4,17 +4,17 @@ * @license {@link https://opensource.org/licenses/MIT|MIT License} */ -// Earcut 2.1.4 (December 4th 2018) +// Earcut 2.2.2 (January 21st 2020) /* * 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, @@ -85,7 +85,7 @@ function linkedList(data, start, end, dim, clockwise) { return last; } -// eliminate collinear or duplicate points +// eliminate colinear or duplicate points function filterPoints(start, end) { if (!start) return start; if (!end) end = start; @@ -149,7 +149,7 @@ function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) { // if this didn't work, try curing all small self-intersections locally } else if (pass === 1) { - ear = cureLocalIntersections(ear, triangles, dim); + ear = cureLocalIntersections(filterPoints(ear), triangles, dim); earcutLinked(ear, triangles, dim, minX, minY, invSize, 2); // as a last resort, try splitting the remaining polygon into two @@ -256,7 +256,7 @@ function cureLocalIntersections(start, triangles, dim) { p = p.next; } while (p !== start); - return p; + return filterPoints(p); } // try splitting polygon into two and triangulate them independently @@ -270,7 +270,7 @@ function splitEarcut(start, triangles, dim, minX, minY, invSize) { // split the polygon in two by the diagonal var c = splitPolygon(a, b); - // filter collinear points around the cuts + // filter colinear points around the cuts a = filterPoints(a, a.next); c = filterPoints(c, c.next); @@ -318,6 +318,9 @@ function eliminateHole(hole, outerNode) { outerNode = findHoleBridge(hole, outerNode); if (outerNode) { var b = splitPolygon(outerNode, hole); + + // filter collinear points around the cuts + filterPoints(outerNode, outerNode.next); filterPoints(b, b.next); } } @@ -349,7 +352,7 @@ function findHoleBridge(hole, outerNode) { if (!m) return null; - if (hx === qx) return m.prev; // hole touches outer segment; pick lower endpoint + 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; @@ -361,26 +364,32 @@ function findHoleBridge(hole, outerNode) { tanMin = Infinity, tan; - p = m.next; + p = m; - while (p !== stop) { + 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 ((tan < tanMin || (tan === tanMin && p.x > m.x)) && locallyInside(p, hole)) { + 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) { var p = start; @@ -474,7 +483,7 @@ function getLeftmost(start) { var p = start, leftmost = start; do { - if (p.x < leftmost.x) leftmost = p; + if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p; p = p.next; } while (p !== start); @@ -490,8 +499,10 @@ function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) { // 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) && - locallyInside(a, b) && locallyInside(b, a) && middleInside(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 @@ -506,10 +517,28 @@ function equals(p1, p2) { // check if two segments intersect function intersects(p1, q1, p2, q2) { - if ((equals(p1, q1) && equals(p2, q2)) || - (equals(p1, q2) && equals(p2, q1))) return true; - return area(p1, q1, p2) > 0 !== area(p1, q1, q2) > 0 && - area(p2, q2, p1) > 0 !== area(p2, q2, q1) > 0; + var o1 = sign(area(p1, q1, p2)); + var o2 = sign(area(p1, q1, q2)); + var o3 = sign(area(p2, q2, p1)); + var 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