diff --git a/crates/effects/src/stroke/dash.rs b/crates/effects/src/stroke/dash.rs index b4eb7f2b..a5d8ce00 100644 --- a/crates/effects/src/stroke/dash.rs +++ b/crates/effects/src/stroke/dash.rs @@ -9,6 +9,9 @@ use super::{ARCLEN_ACCURACY, push_seg, segments, subpaths, tangent}; /// More dashes than this (a tiny pattern on a long path) draws the line solid instead. const MAX_DASHES: f64 = 200_000.0; +/// A pattern whose longest entry is shorter than this can't advance along the path (each entry +/// ends within the generator's tolerance of where it starts), so it draws the line solid. +const MIN_ENTRY: f64 = 1e-6; /// A zero-length dash: where it sits and the path's direction there. #[derive(Clone, Copy, Debug, PartialEq)] @@ -25,13 +28,13 @@ pub struct Dashed { } /// Apply dash pattern `d` to `bp`. `None` when the pattern has no positive length (the line is -/// solid) or is too fine to draw. +/// solid) or is too fine to draw (then the line is drawn solid too). pub fn dash(bp: &BezPath, d: &Dash) -> Option { if d.pattern.iter().any(|v| !v.is_finite()) || !d.offset.is_finite() { return None; } let mut pat: Vec = d.pattern.iter().map(|v| v.max(0.0)).collect(); - if !pat.iter().any(|v| *v > 0.0) { + if !pat.iter().any(|v| *v > MIN_ENTRY) { return None; } // An odd pattern repeats with dashes and gaps swapped (as in SVG and PDF). diff --git a/crates/effects/src/stroke/tests.rs b/crates/effects/src/stroke/tests.rs index 578a36b0..6811e810 100644 --- a/crates/effects/src/stroke/tests.rs +++ b/crates/effects/src/stroke/tests.rs @@ -253,3 +253,53 @@ fn round_caps_never_poke_past_the_tip() { } } } + +// ---------------------------------------------------------------- robustness + +#[test] +fn degenerate_paths_and_patterns_neither_panic_nor_hang() { + let finite = |b: &BezPath| b.elements().iter().filter_map(|e| e.end_point()).all(|q| q.x.is_finite() && q.y.is_finite()); + let mut point = BezPath::new(); + point.move_to((0.0, 0.0)); + let lone = point.clone(); + point.line_to((0.0, 0.0)); + let mut still_curve = BezPath::new(); + still_curve.move_to((0.0, 0.0)); + still_curve.curve_to((0.0, 0.0), (0.0, 0.0), (0.0, 0.0)); + // Drawing on after a ClosePath, and curves whose handles sit on their end points. + let mut mixed = BezPath::new(); + mixed.move_to((0.0, 0.0)); + mixed.line_to((10.0, 0.0)); + mixed.close_path(); + mixed.line_to((5.0, 5.0)); + mixed.move_to((20.0, 0.0)); + mixed.curve_to((20.0, 0.0), (50.0, 50.0), (50.0, 50.0)); + mixed.quad_to((50.0, 50.0), (90.0, 0.0)); + let paths = [BezPath::new(), lone, point, still_curve, mixed, square(20.0)]; + // Negative, non-finite and sub-tolerance entries (the last used to loop forever on a + // zero-length path), and offsets far outside the pattern. + let patterns: [&[f64]; 6] = [&[0.0, 6.0], &[-1.0, 3.0], &[f64::NAN, 1.0], &[1e-12, 1e-12], &[0.0], &[3.0, 0.0]]; + for bp in &paths { + for kind in Arrowhead::ALL { + for align in [ArrowAlign::Extend, ArrowAlign::Tip] { + for cap in [LineCap::Butt, LineCap::Round, LineCap::Square] { + for pat in patterns { + for off in [0.0, -7.0, 1e12, f64::INFINITY] { + let st = stroke(2.0, |s| { + s.start_arrow = Some(kind); + s.end_arrow = Some(kind); + s.arrow_align = align; + s.cap = cap; + s.dash = Some(dashed(pat, off)); + }); + let p = stroke_pieces(bp, &st); + assert!(p.heads.iter().all(|h| finite(&h.outline)), "{bp:?} {kind:?}"); + assert!(finite(&line_outline(&p.line, &st, 2.0, 0.01)), "{bp:?} {pat:?} {off}"); + } + } + } + } + } + } + assert!(dash(&line(0.0, 1e-9), &dashed(&[1e-12, 1e-12], 0.0)).is_none(), "too fine to advance: solid"); +}