groupRings
groupRings(
subpaths):RingGroup[]
Defined in: core/rings.ts:96
Group a flat list of closed rings into filled polygons with holes, at ARBITRARY nesting depth (issue #73 — an island in a lake in land, and deeper).
PathRecorder emits independent subpaths and discards which ring is an outer
boundary vs. a hole, so we recover it from the geometry. The rule is the NONZERO
winding rule — the same rule Canvas (ctx.fill()) and SVG (fill-rule: nonzero)
apply natively to the very same subpaths, so all three backends agree by
construction (AGENTS.md “Backend compositing equivalence”). For each ring we
compute the winding number of the region just OUTSIDE it (the signed sum of the
rings that enclose it) and just INSIDE it (that sum plus the ring’s own
direction). A ring is then:
- an outer (starts a new
RingGroup) when the fill turns on across it — outside 0, inside ≠ 0; - a hole of the nearest enclosing outer when the fill turns off — outside ≠ 0, inside 0;
- dropped when both sides are filled or both empty, since it bounds nothing.
With the documented winding convention (exterior rings clockwise in [lon, lat],
holes wound the opposite way, so nesting levels alternate — see AGENTS.md “GeoJSON
winding”) this is exactly alternating solid/hole by depth: land → lake → island →
pond. Rings that do NOT alternate (two nested rings wound the same way) stay solid,
which is what the Canvas/SVG nonzero fill already draws.
Open or degenerate (<3 vertex, zero-area) subpaths are skipped — a fill needs a closed ring.
Cost: rings are prepared once (area + bbox), sorted by descending area, and each ring is tested against the larger ones. The bbox test rejects non-containers before the O(vertices) ray cast, so the ray cast runs only for the handful of rings that actually overlap the probe point — the common geo case (thousands of disjoint island rings in one MultiPolygon) stays at cheap coordinate compares.
Parameters
Section titled “Parameters”subpaths
Section titled “subpaths”readonly Subpath[]