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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-26 18:22:52 +03:00
1 parent a3e9b05130
commit e55b280dda
1 file changed
+24 -5
+24 -5
View File
@@ -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 {