From cfce3362151e4f9e15d0a00ff4d561d774888cf2 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 13 Nov 2026 00:47:30 +0700 Subject: [PATCH] Dashes fitted to corners take a subpath with no segments An HP-GL wedge of almost no radius drawn with an adaptive line type (LT-1;EW0.000001,0,0;) leaves a lone moveto. Fitting the dashes to its corners indexed the empty segment list and panicked while rendering. Such a subpath now uses the exact plan, which has no runs to fit. Found by import_fuzz::random_hpgl_instructions_never_panic. --- crates/effects/src/stroke/dash.rs | 4 ++++ crates/effects/src/stroke/tests_fit.rs | 14 ++++++++++++++ crates/engine/tests/import_fuzz.rs | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/crates/effects/src/stroke/dash.rs b/crates/effects/src/stroke/dash.rs index 400f3b58b9..f174069c4e 100644 --- a/crates/effects/src/stroke/dash.rs +++ b/crates/effects/src/stroke/dash.rs @@ -134,6 +134,10 @@ impl Plan { fn fitted(mut segs: Vec, mut lens: Vec, closed: bool, period: f64) -> Self { let n = segs.len(); + // A subpath with no segments (a lone moveto) has no runs to fit. + if n == 0 { + return Self::exact(segs, lens, closed, period); + } // Corners: where segments with length meet at a kink (zero-length ones have no direction). let live: Vec = (0..n).filter(|&i| lens[i] > EPS).collect(); let mut corners: Vec = live.windows(2).filter(|w| kink(&segs[w[0]], &segs[w[1]])).map(|w| w[1]).collect(); diff --git a/crates/effects/src/stroke/tests_fit.rs b/crates/effects/src/stroke/tests_fit.rs index b389cab853..cc69606aee 100644 --- a/crates/effects/src/stroke/tests_fit.rs +++ b/crates/effects/src/stroke/tests_fit.rs @@ -37,6 +37,20 @@ fn near(a: f64, b: f64, tol: f64) -> bool { // ---------------------------------------------------------------- dashes fitted to corners +/// A subpath with no segments (a lone moveto, as an HP-GL wedge of almost no radius gives) is +/// dashed fitted to corners without a panic, and the other subpaths keep their dashes. +#[test] +fn fitted_dashes_take_a_subpath_with_no_segments() { + let mut b = BezPath::new(); + b.move_to((5.0, 5.0)); + b.extend(line(100.0)); + let d = dash(&b, &fitted(&[10.0, 5.0], 0.0)).expect("dashed"); + assert!(moves(&d.path) > 1 && length(&d.path) > 0.0, "{:?}", d.path); + let mut lone = BezPath::new(); + lone.move_to((0.0, 0.0)); + assert!(dash(&lone, &fitted(&[10.0, 5.0], 0.0)).is_some_and(|d| d.path.elements().is_empty())); +} + #[test] fn every_corner_of_a_rectangle_falls_mid_dash() { // 100 × 50 with 12/6: the long sides hold 6 periods (scaled by 100/108), the short ones 3 diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 4542496fc5..4688845de4 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -2958,6 +2958,13 @@ fn the_hpgl_sample_opens() { assert!(paths > 15, "{paths} paths, warnings {:?}", l.warnings); } +/// A wedge drawn with the adaptive line type `LT-1` and a near-zero radius (a case the fuzz test +/// found) opens and places without a panic. +#[test] +fn an_adaptive_line_type_wedge_of_almost_no_radius_opens() { + survive_metafile("HP-GL tiny adaptive wedge", "x.plt", b"IN;SP1;LT-1;EW0.000001,0,0;").unwrap(); +} + /// A random instruction: a mnemonic the reader plays (or one it doesn't) with hostile numbers. fn arb_hpgl_instruction() -> impl Strategy { let op = prop::sample::select(vec![