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:
- 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.
- 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
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...
/**
*/
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:
Related
name@Nconvention) #14 (separate problem we hit during the same font build, also about silent failures)