diff --git a/src/earcut.js b/src/earcut.js index 7136b61..aa1b733 100644 --- a/src/earcut.js +++ b/src/earcut.js @@ -933,7 +933,14 @@ export function refine(triangles, coords, dim = 2) { if (hStamp[h] !== gen) { hTable[h] = e; hStamp[h] = gen; } // first occurrence: insert } - while (i > 0) { + // Cap total flips: with non-robust inCircle, roundoff can make a quad and its flip both test + // as illegal, so a cascade can cycle forever (a legalized edge gets re-queued and flipped back). + // A converging cascade needs O(triangles) flips in practice, so a generous multiple of the + // half-edge count bounds legitimate work while breaking float-induced cycles; the worst case is + // then a few not-quite-Delaunay edges, never a hang. + let budget = 25 * n; + + while (i > 0 && budget-- > 0) { const a = edgeStack[--i]; edgeStamp[a] = 0; const b = he[a]; diff --git a/test/test.js b/test/test.js index 69b2ccb..41c0ba0 100644 --- a/test/test.js +++ b/test/test.js @@ -187,6 +187,20 @@ test('refine preserves a concave polygon', () => { assert.equal(deviation(vertices, null, 2, triangles), 0); }); +test('refine terminates on near-cocircular points', {timeout: 5000}, () => { + const vertices = [ + 127.65906365022843, 9.336137742499535, 124.21725103117963, 30.888097161477972, + 91.35514946628345, 89.65621376119454, 40.10446780041529, 121.5550560957686, + -110.83205604043928, 64.03323632184248, -127.20394987965459, -14.253249980770189, + 61.074962259031416, -112.48932831632469, 127.37846573978545, -12.598669206638515, + 127.77010311801033, -7.668164657400608]; + const triangles = earcut(vertices); + const length = triangles.length; + refine(triangles, vertices); + assert.equal(triangles.length, length); + assert.ok(deviation(vertices, null, 2, triangles) < 1e-9); +}); + test('refine legalizes all convex interior edges in earcut fixture', () => { const coords = JSON.parse(fs.readFileSync(new URL('fixtures/earcut.json', import.meta.url))); const data = flatten(coords);