Skip to content

Add Polygons#normalizeWinding for non-zero fill consumers#17

Open
MasKusuno wants to merge 1 commit into
kurgm:masterfrom
MasKusuno:feat/polygons-normalize-winding
Open

Add Polygons#normalizeWinding for non-zero fill consumers#17
MasKusuno wants to merge 1 commit into
kurgm:masterfrom
MasKusuno:feat/polygons-normalize-winding

Conversation

@MasKusuno

Copy link
Copy Markdown

Summary

This PR addresses #15 by adding an opt-in normalizeWinding method to the Polygons class. It flips the vertex order of each contour so all contours share a single winding orientation, eliminating the white-out artefacts that mixed winding produces under non-zero filling.

Background

Polygons.push accumulates each rendered stroke as an independent closed polygon, but the engine never normalizes the polygons' winding directions. This is invisible at the small render sizes used by the GlyphWiki web viewer with evenodd filling, but becomes prominent when the output is fed into a font (TrueType glyf table) or scaled SVG <path> without an explicit fill-rule=\"evenodd\". The intersection of two overlapping strokes ends up unfilled — the inner overlap area is treated as a hole.

API

class Polygons {
  // ...existing methods...

  /**
   * Reverses the vertex order of any contour whose signed area does not match
   * the requested direction, so all contours share a single winding orientation.
   *
   * Useful before passing the polygons to a renderer that uses non-zero filling
   * (default for SVG \`<path>\` and TrueType \`glyf\`), where mixed winding produces
   * white-out artefacts at stroke intersections.
   */
  public normalizeWinding(direction: WindingDirection = "cw"): void;
}

export type WindingDirection = "cw" | "ccw";

In KAGE's y-axis-down coordinate system:

  • positive signed area → clockwise vertex order
  • negative signed area → counter-clockwise vertex order
  • zero → degenerate polygon (left untouched)

Why default "cw"?

When KAGE polygons are flipped to a y-up coordinate system (e.g. when emitting a TrueType glyf), "cw" in y-down corresponds to "ccw" in y-up — which is the orientation TrueType uses for outer contours. So polygons.normalizeWinding() with the default produces glyf-ready polygons after the y-flip. SVG callers using non-zero filling can pick either direction; the default is fine there too.

Why this is non-breaking

  • New method only; existing call sites of push / generateSVG / generateEPS are unchanged.
  • The constructor and array field are untouched.
  • WindingDirection is a new exported type; existing imports are unaffected.

Tests

test/index.js gains a Polygons#normalizeWinding block covering:

  • Default "cw" normalization makes every polygon's signed area >= 0.
  • "ccw" makes every polygon's signed area <= 0.
  • Idempotency: applying twice yields the same result as once.
  • Empty / degenerate polygons are accepted without error.

Existing tests continue to pass.

Real-world usage

We hit this while building a free-license PUA font from GlyphWiki dumps. Without normalization, characters with dense overlapping strokes (e.g. CJK Unified kanji with crossbars over verticals) leave white gashes at the crossings. With polygons.normalizeWinding() called once before emitting the glyf table, the artefacts disappear cleanly.

Note

The implementation lives entirely on the consumer side of the polygon list — it doesn't touch the stroke generation pipeline or the existing push validation logic. So the engine's render output is unchanged unless callers explicitly opt in.

Refs #15

Adds a `normalizeWinding(direction?)` method to the `Polygons` class that
flips the vertex order of each contour so all contours share a single
winding orientation. KAGE pushes each stroke as an independent closed
polygon without normalizing winding, which is invisible under evenodd
filling but produces white-out artefacts at stroke intersections under
non-zero filling — the default for both SVG `<path>` (when no fill-rule
is set) and TrueType `glyf`. Calling this method before passing the
polygons to such a renderer ensures overlapping strokes render as a
single filled shape.

The new `WindingDirection` type alias is also exported. The method
defaults to `"cw"`, which matches the convention used by TrueType `glyf`
outer contours when coordinates are flipped to a y-up system.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant