diff --git a/src/earcut.js b/src/earcut.js index e711fd3..aea2f70 100644 --- a/src/earcut.js +++ b/src/earcut.js @@ -71,7 +71,7 @@ function filterPoints(data, start, end) { do { again = false; - if (!node.steiner && (equals(data, node.i, node.next.i) || orient(data, node.prev.i, node.i, node.next.i) === 0)) { + if (!node.steiner && (equals(data, node.i, node.next.i) || area(data, node.prev.i, node.i, node.next.i) === 0)) { removeNode(node); node = end = node.prev; if (node === node.next) return null; @@ -143,27 +143,18 @@ function isEar(data, ear, minX, minY, size) { var a = ear.prev.i, b = ear.i, - c = ear.next.i, + c = ear.next.i; - ax = data[a], ay = data[a + 1], - bx = data[b], by = data[b + 1], - cx = data[c], cy = data[c + 1], - - abd = ax * by - ay * bx, - acd = ax * cy - ay * cx, - cbd = cx * by - cy * bx, - A = abd - acd - cbd; + if (area(data, a, b, c) >= 0) return false; // reflex, can't be an ear - if (A <= 0) return false; // reflex, can't be an ear + var ax = data[a], ay = data[a + 1], + bx = data[b], by = data[b + 1], + cx = data[c], cy = data[c + 1]; // now make sure we don't have other points inside the potential ear; // the code below is a bit verbose and repetitive but this is done for performance - var cay = cy - ay, - acx = ax - cx, - aby = ay - by, - bax = bx - ax, - i, px, py, s, t, k, node; + var i, node; // if we use z-order curve hashing, iterate through the curve if (minX !== undefined) { @@ -183,20 +174,10 @@ function isEar(data, ear, minX, minY, size) { while (node && node.z <= maxZ) { i = node.i; + if (node !== ear.prev && node !== ear.next && + pointInTriangle(ax, ay, bx, by, cx, cy, data[i], data[i + 1]) && + area(data, node.prev.i, i, node.next.i) >= 0) return false; node = node.nextZ; - if (i === a || i === c) continue; - - px = data[i]; - py = data[i + 1]; - - s = cay * px + acx * py - acd; - if (s >= 0) { - t = aby * px + bax * py + abd; - if (t >= 0) { - k = A - s - t; - if ((k >= 0) && ((s && t) || (s && k) || (t && k))) return false; - } - } } // then look for points in decreasing z-order @@ -204,20 +185,10 @@ function isEar(data, ear, minX, minY, size) { while (node && node.z >= minZ) { i = node.i; + if (node !== ear.prev && node !== ear.next && + pointInTriangle(ax, ay, bx, by, cx, cy, data[i], data[i + 1]) && + area(data, node.prev.i, i, node.next.i) >= 0) return false; node = node.prevZ; - if (i === a || i === c) continue; - - px = data[i]; - py = data[i + 1]; - - s = cay * px + acx * py - acd; - if (s >= 0) { - t = aby * px + bax * py + abd; - if (t >= 0) { - k = A - s - t; - if ((k >= 0) && ((s && t) || (s && k) || (t && k))) return false; - } - } } // if we don't use z-order curve hash, simply iterate through all other points @@ -226,19 +197,9 @@ function isEar(data, ear, minX, minY, size) { while (node !== ear.prev) { i = node.i; + if (pointInTriangle(ax, ay, bx, by, cx, cy, data[i], data[i + 1]) && + area(data, node.prev.i, i, node.next.i) >= 0) return false; node = node.next; - - px = data[i]; - py = data[i + 1]; - - s = cay * px + acx * py - acd; - if (s >= 0) { - t = aby * px + bax * py + abd; - if (t >= 0) { - k = A - s - t; - if ((k >= 0) && ((s && t) || (s && k) || (t && k))) return false; - } - } } } @@ -253,9 +214,8 @@ function cureLocalIntersections(data, start, triangles, dim) { b = node.next.next; // a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2]) - if (a.i !== b.i && intersects(data, a.i, node.i, node.next.i, b.i) && - locallyInside(data, a, b) && locallyInside(data, b, a) && - orient(data, a.i, node.i, b.i) && orient(data, a.i, node.next.i, b.i)) { + if (intersects(data, a.i, node.i, node.next.i, b.i) && + locallyInside(data, a, b) && locallyInside(data, b, a)) { triangles.push(a.i / dim); triangles.push(node.i / dim); @@ -341,7 +301,7 @@ function findHoleBridge(data, holeNode, outerNode) { i = holeNode.i, px = data[i], py = data[i + 1], - qMax = -Infinity, + qx = -Infinity, mNode, a, b; // find a segment intersected by a ray from the hole's leftmost point to the left; @@ -351,9 +311,9 @@ function findHoleBridge(data, holeNode, outerNode) { b = node.next.i; if (py <= data[a + 1] && py >= data[b + 1]) { - var qx = data[a] + (py - data[a + 1]) * (data[b] - data[a]) / (data[b + 1] - data[a + 1]); - if (qx <= px && qx > qMax) { - qMax = qx; + var x = data[a] + (py - data[a + 1]) * (data[b] - data[a]) / (data[b + 1] - data[a + 1]); + if (x <= px && x > qx) { + qx = x; mNode = data[a] < data[b] ? node : node.next; } } @@ -362,23 +322,15 @@ function findHoleBridge(data, holeNode, outerNode) { if (!mNode) return null; - // look for points strictly inside the triangle of hole point, segment intersection and 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 var bx = data[mNode.i], by = data[mNode.i + 1], - pbd = px * by - py * bx, - pcd = px * py - py * qMax, - cpy = py - py, - pcx = px - qMax, - pby = py - by, - bpx = bx - px, - A = pbd - pcd - (qMax * by - py * bx), - sign = A <= 0 ? -1 : 1, stop = mNode, tanMin = Infinity, - mx, my, amx, s, t, tan; + mx, my, tan; node = mNode.next; @@ -386,21 +338,13 @@ function findHoleBridge(data, holeNode, outerNode) { mx = data[node.i]; my = data[node.i + 1]; - amx = px - mx; - - if (amx >= 0 && mx >= bx) { - s = (cpy * mx + pcx * my - pcd) * sign; - if (s >= 0) { - t = (pby * mx + bpx * my + pbd) * sign; - - if (t >= 0 && A * sign - s - t >= 0) { - tan = Math.abs(py - my) / amx; // tangential - if ((tan < tanMin || (tan === tanMin && mx > bx)) && - locallyInside(data, node, holeNode)) { - mNode = node; - tanMin = tan; - } - } + + if (px >= mx && mx >= bx && pointInTriangle(py < by ? px : qx, py, bx, by, py < by ? qx : px, py, mx, my)) { + tan = Math.abs(py - my) / (px - mx); // tangential + + if ((tan < tanMin || (tan === tanMin && mx > bx)) && locallyInside(data, node, holeNode)) { + mNode = node; + tanMin = tan; } } @@ -520,19 +464,25 @@ function getLeftmost(data, 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(data, a, b) { - return a.next.i !== b.i && a.prev.i !== b.i && + return equals(data, a.i, b.i) || + a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(data, a, a.i, b.i) && locallyInside(data, a, b) && locallyInside(data, b, a) && middleInside(data, a, a.i, b.i); } -// winding order of triangle formed by 3 given points -function orient(data, p, q, r) { - var o = (data[q + 1] - data[p + 1]) * (data[r] - data[q]) - (data[q] - data[p]) * (data[r + 1] - data[q + 1]); - return o > 0 ? 1 : - o < 0 ? -1 : 0; +// signed area of a triangle +function area(data, p, q, r) { + return (data[q + 1] - data[p + 1]) * (data[r] - data[q]) - (data[q] - data[p]) * (data[r + 1] - data[q + 1]); } // check if two points are equal @@ -542,8 +492,8 @@ function equals(data, p1, p2) { // check if two segments intersect function intersects(data, p1, q1, p2, q2) { - return orient(data, p1, q1, p2) !== orient(data, p1, q1, q2) && - orient(data, p2, q2, p1) !== orient(data, p2, q2, q1); + return area(data, p1, q1, p2) > 0 !== area(data, p1, q1, q2) > 0 && + area(data, p2, q2, p1) > 0 !== area(data, p2, q2, q1) > 0; } // check if a polygon diagonal intersects any polygon segments @@ -563,9 +513,9 @@ function intersectsPolygon(data, start, a, b) { // check if a polygon diagonal is locally inside the polygon function locallyInside(data, a, b) { - return orient(data, a.prev.i, a.i, a.next.i) === -1 ? - orient(data, a.i, b.i, a.next.i) !== -1 && orient(data, a.i, a.prev.i, b.i) !== -1 : - orient(data, a.i, b.i, a.prev.i) === -1 || orient(data, a.i, a.next.i, b.i) === -1; + return area(data, a.prev.i, a.i, a.next.i) < 0 ? + area(data, a.i, b.i, a.next.i) >= 0 && area(data, a.i, a.prev.i, b.i) >= 0 : + area(data, a.i, b.i, a.prev.i) < 0 || area(data, a.i, a.next.i, b.i) < 0; } // check if the middle point of a polygon diagonal is inside the polygon diff --git a/test/fixtures/hole-touching-outer.json b/test/fixtures/hole-touching-outer.json new file mode 100644 index 0000000..a730597 --- /dev/null +++ b/test/fixtures/hole-touching-outer.json @@ -0,0 +1 @@ +[[[-64,-64],[253,-64],[491,358],[697,298],[928,197],[929,505],[1346,507],[1347,303],[1771,306],[1770,512],[2191,509],[2198,933],[2621,932],[2623,1115],[2577,1120],[2494,1183],[2390,1329],[2326,1590],[2287,1678],[2286,1407],[2229,1407],[2182,1493],[2106,1494],[2068,1460],[2019,1460],[2016,1775],[1889,1923],[1953,1989],[2097,1866],[2198,1925],[2203,1973],[2311,1976],[2320,1831],[2352,1824],[2358,1797],[2378,1780],[3350,1782],[3307,2086],[3139,2088],[3143,2203],[3493,2205],[3543,2187],[3540,2260],[3661,2264],[3665,1906],[3630,1902],[3626,1784],[4160,1786],[4160,2631],[4076,2631],[4021,2683],[3930,2701],[3915,2693],[3898,2639],[2630,2630],[2635,3476],[2287,3478],[2118,3203],[2180,3145],[2327,3087],[2610,2643],[2613,2536],[2658,2495],[2650,2203],[1829,2189],[1732,2241],[1551,2245],[933,1183],[890,1152],[455,401],[398,412],[89,547],[-64,606],[-64,-64]],[[1762,928],[1770,512],[1343,513],[1345,715],[931,719],[932,930],[1762,928]]] diff --git a/test/fixtures/touching-holes.json b/test/fixtures/touching-holes.json new file mode 100644 index 0000000..63e140b --- /dev/null +++ b/test/fixtures/touching-holes.json @@ -0,0 +1 @@ +[[[3694,2061],[3794,2035],[3812,2123],[3784,2123],[3708,2139],[3694,2061]],[[3752,2109],[3740,2102],[3712,2109],[3715,2125],[3723,2128],[3740,2124],[3742,2112],[3752,2109]],[[3797,2101],[3787,2096],[3780,2106],[3788,2114],[3797,2101]],[[3734,2099],[3732,2091],[3719,2094],[3721,2102],[3734,2099]],[[3777,2082],[3774,2071],[3772,2086],[3765,2091],[3748,2088],[3749,2062],[3738,2081],[3745,2095],[3761,2099],[3777,2082]],[[3719,2079],[3712,2079],[3706,2091],[3712,2097],[3721,2080],[3719,2079]],[[3773,2067],[3761,2053],[3753,2061],[3753,2071],[3756,2075],[3773,2067]],[[3708,2079],[3712,2079],[3714,2076],[3719,2079],[3722,2079],[3718,2088],[3723,2089],[3734,2075],[3730,2068],[3717,2065],[3708,2079]]] diff --git a/test/test.js b/test/test.js index affc489..7db4814 100644 --- a/test/test.js +++ b/test/test.js @@ -7,13 +7,13 @@ var test = require('tape'), areaTest('building', 12); areaTest('dude', 106); -areaTest('water', 2482, 0.0019); +areaTest('water', 2482, 0.0008); areaTest('water2', 1211); areaTest('water3', 197); areaTest('water3b', 25); areaTest('water4', 705); -areaTest('water-huge', 5164, 0.0015); -areaTest('water-huge2', 4462, 0.0019); +areaTest('water-huge', 5159, 0.008); +areaTest('water-huge2', 4458, 0.0019); areaTest('degenerate', 0); areaTest('bad-hole', 34, 0.0420); areaTest('empty-square', 0); @@ -26,6 +26,8 @@ areaTest('issue35', 844); areaTest('self-touching', 124, 3.4e-14); areaTest('outside-ring', 64); areaTest('simplified-us-border', 120); +areaTest('touching-holes', 57); +areaTest('hole-touching-outer', 77); test('indices-2d', function (t) { var indices = earcut([10, 0, 0, 50, 60, 60, 70, 10]);