M3.19: dash patterns too fine to advance draw solid instead of hanging on zero-length paths

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-04 17:40:39 +07:00
1 parent 5ae7bfb8ca
commit 4283820a30
2 files changed
+55 -2

No files matched your search

+5 -2
View File
@@ -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<Dashed> {
if d.pattern.iter().any(|v| !v.is_finite()) || !d.offset.is_finite() {
return None;
}
let mut pat: Vec<f64> = 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).
+50
View File
@@ -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");
}