From 6fb5c89a7bdb09164a61e19de85b575a1fa9d0d7 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 25 Jun 2026 16:11:15 +0300 Subject: [PATCH] fix(hatch): preserve all island loops in complex hatches (#148) The hatch boundary was flat-truncated to 1024 vertices before upload. The active renderer is the batched path, which stores boundaries in a GPU storage buffer with no length limit, so the cap only ever served the per-hatch uniform path (now wipeout-only, and self-clamping). On a real drawing a 36-loop, depth-3 nested hatch (1482 verts) was cut mid-boundary: trailing island loops were dropped and the final partial loop was left open, flipping the even-odd parity so the fill bled across the rest of the shape and islands rendered incorrectly. Drop the flat truncate. Keep every loop; only guard pathological vertex counts (16384), and when trimming, cut at a NaN sentinel (whole-loop boundary) so no sub-loop is ever left open. Co-Authored-By: Claude Opus 4.8 --- src/scene/mod.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 188562ea9..29a8c5139 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -5063,7 +5063,26 @@ impl Scene { if boundary.is_empty() { return None; } - boundary.truncate(model::hatch_model::MAX_HATCH_BOUNDARY_VERTS); + // The batched hatch renderer keeps boundaries in a GPU storage + // buffer (no fixed length), so a hatch with many island loops must + // retain *every* loop or even-odd island detection breaks. The old + // flat `truncate(1024)` cut complex multi-loop hatches mid-boundary: + // trailing islands were dropped and the final partial loop was left + // open, flipping the even-odd parity so the fill bled across the + // rest of the shape. Only guard against pathological vertex counts, + // and when trimming, cut at a whole-loop (NaN sentinel) boundary so + // no sub-loop is ever left open. (#148) + const MAX_HATCH_MODEL_VERTS: usize = 16_384; + if boundary.len() > MAX_HATCH_MODEL_VERTS { + // Drop only whole trailing loops: cut at the last NaN sentinel + // at/before the cap. If the first loop alone exceeds the cap, + // keep it whole rather than leaving it open. + let cut = boundary[..MAX_HATCH_MODEL_VERTS] + .iter() + .rposition(|&[x, y]| x.is_nan() || y.is_nan()) + .unwrap_or(boundary.len()); + boundary.truncate(cut); + } let pattern = if dxf.gradient_color.is_enabled() { let color2 = dxf