Skip to content

deviation() returns a spurious 1 for degenerate (zero-area) polygons #208

Description

@mrshannon

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions