Summary
deviation() — the helper for verifying a triangulation's correctness — returns 1 ("100% wrong") for a valid, correctly-triangulated degenerate polygon (a zero-area / collinear ring). earcut triangulates such a ring to zero triangles, which is correct, yet deviation() reports the result as maximally wrong.
Reproduction
import earcut, {deviation} from 'earcut';
// four collinear points on the line y = 2x → a zero-area ring
const polygon = [0.1, 0.2, 1.3, 2.6, 2.5, 5.0, 3.7, 7.4];
const triangles = earcut(polygon); // [] (correct: zero-area ring, nothing to fill)
console.log(triangles.length); // 0
console.log(deviation(polygon, null, 2, triangles)); // 1 ← expected 0
Expected: 0 (the triangulation is correct).
Actual: 1.
Root cause
deviation() computes a relative error and special-cases the zero-area case with an exact comparison:
return polygonArea === 0 && trianglesArea === 0 ? 0 :
Math.abs((trianglesArea - polygonArea) / polygonArea);
polygonArea comes from the non-robust shoelace sum. For a zero-area ring the true area is exactly 0, but the shoelace adds several large terms that cancel, and floating-point rounding leaves a tiny residual (~1.78e-15 for the example) instead of 0. So:
polygonArea === 0 is false (the value is 1.78e-15),
- the guard is skipped, and
- it evaluates
|0 − 1.78e-15| / 1.78e-15 = 1.
It is data-dependent: an axis-aligned collinear ring cancels to exactly 0 (the guard fires, returns 0), but a non-axis-aligned / decimal one leaves a residual and returns 1. That is why the existing degenerate fixtures (degenerate, empty-square, …) don't catch it — they all happen to cancel to exact 0.
Impact
deviation() is the sanctioned way to verify earcut's (deliberately non-guaranteed) output. Pipelines that gate on deviation(...) > threshold hard-fail on valid degenerate input.
Suggested fix
Replace the exact polygonArea === 0 test with a scale-relative floating-point tolerance - treat |polygonArea| within its rounding-error bound as zero. See #209
Summary
deviation()— the helper for verifying a triangulation's correctness — returns1("100% wrong") for a valid, correctly-triangulated degenerate polygon (a zero-area / collinear ring). earcut triangulates such a ring to zero triangles, which is correct, yetdeviation()reports the result as maximally wrong.Reproduction
Expected:
0(the triangulation is correct).Actual:
1.Root cause
deviation()computes a relative error and special-cases the zero-area case with an exact comparison:polygonAreacomes from the non-robust shoelace sum. For a zero-area ring the true area is exactly0, but the shoelace adds several large terms that cancel, and floating-point rounding leaves a tiny residual (~1.78e-15for the example) instead of0. So:polygonArea === 0isfalse(the value is1.78e-15),|0 − 1.78e-15| / 1.78e-15 = 1.It is data-dependent: an axis-aligned collinear ring cancels to exactly
0(the guard fires, returns0), but a non-axis-aligned / decimal one leaves a residual and returns1. That is why the existing degenerate fixtures (degenerate,empty-square, …) don't catch it — they all happen to cancel to exact0.Impact
deviation()is the sanctioned way to verify earcut's (deliberately non-guaranteed) output. Pipelines that gate ondeviation(...) > thresholdhard-fail on valid degenerate input.Suggested fix
Replace the exact
polygonArea === 0test with a scale-relative floating-point tolerance - treat|polygonArea|within its rounding-error bound as zero. See #209