Skip to content

Polygons output overlapping strokes break under nonzero fill (font generation use case) #15

Description

@MasKusuno

Summary

`Polygons` accumulates each rendered stroke as an independent closed polygon, but the polygons' winding directions are not normalized. Under nonzero fill (the default for both SVG `` and TrueType `glyf`), this causes overlapping strokes to produce white-out artefacts at intersections — the inner overlap area is treated as a hole.

The bundled `Polygons.generateSVG()` doesn't manifest this prominently because online glyph viewers use small render sizes where the artefact is invisible. It becomes obvious when the output is fed into a font (TTF `glyf` table or scalable SVG) where the same path is rendered at high resolution.

Reproduction

The issue surfaces clearly when feeding KAGE output into a TrueType font. Even within the engine's own SVG output, you can confirm the winding inconsistency:

```js
import { Kage, Polygons } from "@kurgm/kage-engine";

const kage = new Kage();
kage.kUseCurve = true;
const polygons = new Polygons();
kage.makeGlyph(polygons, "u9f8d"); // 龍 — many overlapping strokes

// signed area per polygon — should be all-positive or all-negative
// for clean nonzero filling, but it's mixed.
function signedArea(pts) {
let s = 0;
for (let i = 0; i < pts.length; i++) {
const a = pts[i], b = pts[(i + 1) % pts.length];
s += (b.x - a.x) * (b.y + a.y);
}
return s;
}

for (const p of polygons.array) {
console.log(signedArea(p.array).toFixed(0));
}
// Output mixes positive and negative values → winding inconsistent
```

Context

We hit this while building a free-license PUA font from GlyphWiki dumps using KAGE (https://github.com/digital-go-jp/gjs). Strokes that visually overlap (e.g. crossbars over verticals in characters with dense central radicals) leave white gashes at the crossing.

We worked around it by post-processing in our font build pipeline: compute each contour's signed area and reverse its point order if it's not in the desired winding direction.

Proposal

Add a `Polygons.normalizeWinding(direction?: "cw" | "ccw")` method that flips polygon vertex order so all polygons share the same winding. This is non-breaking (additive API) and benefits any consumer that passes `Polygons` output to a renderer using nonzero fill — which is the practical default.

Sketch:

```ts
class Polygons {
// ...existing methods...

/**

  • Reverses the vertex order of any polygon that does not match the
  • requested winding direction. Useful when feeding the output to a
  • renderer that uses non-zero fill (default for SVG and TrueType
  • glyf), where mixed windings cause overlapping strokes to white-out.
    */
    normalizeWinding(direction: "cw" | "ccw" = "ccw"): void {
    for (const polygon of this.array) {
    const s = signedArea(polygon.array);
    const isCcw = s > 0;
    if ((direction === "ccw") !== isCcw) {
    polygon.array.reverse();
    }
    }
    }
    }
    ```

Happy to send a PR if this direction looks acceptable. Two questions:

  1. Should this be opt-in (a method) or opt-out (a default behavior in `push`)? I lean toward a method since it's cheap and explicit, and `push` shouldn't surprise existing callers.
  2. The fix could also live in `generateSVG()` by emitting `fill-rule="evenodd"` instead — but that changes the rendering of legitimate hole geometry. The winding normalization seems cleaner.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions