Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More reliable ear test and point in triangle arithmetic #40

Merged
merged 4 commits into from
Oct 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 47 additions & 97 deletions src/earcut.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -183,41 +174,21 @@ 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
node = ear.prevZ;

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
Expand All @@ -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;
}
}
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand All @@ -362,45 +322,29 @@ 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;

while (node !== stop) {

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;
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/hole-touching-outer.json
Original file line number Diff line number Diff line change
@@ -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]]]
1 change: 1 addition & 0 deletions test/fixtures/touching-holes.json
Original file line number Diff line number Diff line change
@@ -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]]]
8 changes: 5 additions & 3 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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]);
Expand Down