diff --git a/src/earcut.js b/src/earcut.js index 183ccd3..83a25b1 100644 --- a/src/earcut.js +++ b/src/earcut.js @@ -812,12 +812,16 @@ export function deviation(data, holeIndices, dim, triangles) { const hasHoles = holeIndices && holeIndices.length; const outerLen = hasHoles ? holeIndices[0] * dim : data.length; - let polygonArea = Math.abs(signedArea(data, 0, outerLen, dim)); + const outer = signedAreaWithError(data, 0, outerLen, dim); + let polygonArea = Math.abs(outer.sum); + let polygonAreaError = outer.error; if (hasHoles) { for (let i = 0, len = holeIndices.length; i < len; i++) { const start = holeIndices[i] * dim; const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; - polygonArea -= Math.abs(signedArea(data, start, end, dim)); + const hole = signedAreaWithError(data, start, end, dim); + polygonArea -= Math.abs(hole.sum); + polygonAreaError += hole.error; } } @@ -831,7 +835,7 @@ export function deviation(data, holeIndices, dim, triangles) { (data[a] - data[b]) * (data[c + 1] - data[a + 1])); } - return polygonArea === 0 && trianglesArea === 0 ? 0 : + return Math.abs(polygonArea) <= polygonAreaError && trianglesArea === 0 ? 0 : Math.abs((trianglesArea - polygonArea) / polygonArea); } @@ -845,6 +849,20 @@ function signedArea(data, start, end, dim) { return sum; } +/** @param {ArrayLike} data @param {number} start @param {number} end @param {number} dim @returns {{sum: number, error: number}} signed area and its floating-point error bound */ +function signedAreaWithError(data, start, end, dim) { + let sum = 0, scale = 0; + for (let i = start, j = end - dim; i < end; i += dim) { + const term = (data[j] - data[i]) * (data[i + 1] + data[j + 1]); + sum += term; + scale += Math.abs(term); + j = i; + } + // worst-case shoelace roundoff bound: (n + 2)·u, with u = EPSILON/2 and n = (end - start) / dim + const error = scale * ((end - start) / dim + 2) * Number.EPSILON / 2; + return {sum, error}; +} + /** * Turn a polygon in multi-dimensional array form (e.g. as in GeoJSON) into the flat form Earcut accepts. * diff --git a/test/test.js b/test/test.js index a0c5a3c..0532f22 100644 --- a/test/test.js +++ b/test/test.js @@ -152,6 +152,18 @@ test('infinite-loop', () => { earcut([1, 2, 2, 2, 1, 2, 1, 1, 1, 2, 4, 1, 5, 1, 3, 2, 4, 2, 4, 1], [5], 2); }); +test('deviation is zero for a degenerate collinear polygon', () => { + // Four collinear points (on the line y = 2x) form a zero-area ring, so earcut + // produces no triangles and the deviation is 0. The non-robust shoelace leaves a + // tiny residual (~1.78e-15) for a non-axis-aligned collinear ring, so deviation() + // treats a polygon area within that floating-point noise as zero instead of + // dividing by it. + const vertices = [0.1, 0.2, 1.3, 2.6, 2.5, 5.0, 3.7, 7.4]; + const triangles = earcut(vertices); + assert.equal(triangles.length, 0); + assert.equal(deviation(vertices, null, 2, triangles), 0); +}); + test('refine improves a bad quad diagonal', () => { const vertices = [0, 0, 3, 0, 10, 1, 0, 2]; const triangles = [2, 3, 0, 2, 0, 1];