From e55b280ddab14a5747b721db7c2dac10515be6fd Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 26 May 2026 18:22:52 +0300 Subject: [PATCH] fix(paper): handle NaN sentinels when building hatch fill path `HatchModel.boundary` stores multi-region hatches as a single Vec with NaN-NaN sentinels separating each disconnected sub-path (documented on the field). The paper-canvas hatch fill ignored the sentinels and forwarded every vertex straight to the iced canvas path builder. When a layout contained a multi-region hatch, lyon received a polygon containing NaN coordinates and panicked with `Tessellate path.: Internal(IncorrectActiveEdgeOrder(4))` from inside `iced_wgpu::geometry::Frame::fill`, taking the whole app down the first time Layout 1 painted. Walk the boundary instead, opening a fresh sub-path on each sentinel and closing whichever was active. Each region is now filled as its own closed polygon. Reproduces with #22. Co-Authored-By: Claude Opus 4.7 --- src/scene/paper_canvas.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/scene/paper_canvas.rs b/src/scene/paper_canvas.rs index 9232d6ea8..fb529f90b 100644 --- a/src/scene/paper_canvas.rs +++ b/src/scene/paper_canvas.rs @@ -174,13 +174,32 @@ fn draw_hatch(frame: &mut canvas::Frame, hatch: &HatchModel, to_px: &impl Fn(f32 // Reconstruct offset-rel WCS from stored small offsets + f64 anchor. let ox = hatch.world_origin[0] as f32; let oy = hatch.world_origin[1] as f32; + // `boundary` may carry multiple disconnected sub-paths separated by + // NaN-NaN sentinels (multi-region hatches with islands / holes). + // Forwarding NaN to lyon's path builder panics + // (`IncorrectActiveEdgeOrder`) — start a fresh sub-path on each + // sentinel so each region is closed and filled independently. let path = canvas::Path::new(|builder| { - let first = to_px(hatch.boundary[0][0] + ox, hatch.boundary[0][1] + oy); - builder.move_to(first); - for &[x, y] in &hatch.boundary[1..] { - builder.line_to(to_px(x + ox, y + oy)); + let mut active = false; + for &[x, y] in hatch.boundary.iter() { + if x.is_nan() || y.is_nan() { + if active { + builder.close(); + active = false; + } + continue; + } + let p = to_px(x + ox, y + oy); + if !active { + builder.move_to(p); + active = true; + } else { + builder.line_to(p); + } + } + if active { + builder.close(); } - builder.close(); }); match &hatch.pattern {