From 2baeff3452f2c49a0017d2a826b3bdf9715c3bad Mon Sep 17 00:00:00 2001 From: Ridzzz0Alam Date: Sat, 24 Oct 2026 12:44:55 +0200 Subject: [PATCH] M1: dash limit counts the pattern restarting at each subpath (#1060) kurbo restarts the dash pattern at every MoveTo, so many short subpaths with a pattern of tiny dashes before a long gap made ~1M dashes (5M path elements) while the length-based estimate allowed them. stroke_center now adds one pattern's length per subpath to the estimate: an upper bound on the dashes kurbo makes. Test: each_subpath_restarting_the_dash_pattern_counts_toward_the_limit (5M elements before). Reported by CodeRabbit on #1278. --- crates/pathops/src/lib.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/pathops/src/lib.rs b/crates/pathops/src/lib.rs index 45d5122f..7fe7a64e 100644 --- a/crates/pathops/src/lib.rs +++ b/crates/pathops/src/lib.rs @@ -292,7 +292,9 @@ pub fn stroke_center(path: &BezPath, width: f64, props: &StrokeProps) -> BezPath .with_miter_limit(props.miter_limit.clamp(1.0, 100.0)) .with_start_cap(kcap(props.cap_start)) .with_end_cap(kcap(props.cap_end)); - if let Some(pat) = width::dash_pattern(props).filter(|p| width::dashes_fit(path.perimeter(1.0), p)) { + // Each subpath restarts the pattern, so it can add up to one more pattern's worth of dashes. + let subpaths = path.elements().iter().filter(|e| matches!(e, PathEl::MoveTo(_))).count() as f64; + if let Some(pat) = width::dash_pattern(props).filter(|p| width::dashes_fit(path.perimeter(1.0) + subpaths * p.iter().sum::(), p)) { style = style.with_dashes(0.0, pat); } kurbo::stroke(path.iter(), &style, &kurbo::StrokeOpts::default(), 0.05) @@ -732,6 +734,22 @@ mod tests { assert!((s.area().abs() - 20_000.0).abs() < 10.0, "{}", s.area()); } + #[test] + fn each_subpath_restarting_the_dash_pattern_counts_toward_the_limit() { + // The pattern restarts at every subpath: on 10,000 short lines its 199 tiny dashes come + // round each time (~1M dashes), though the length alone suggests a few hundred. + let mut path = BezPath::new(); + for i in 0..10_000 { + path.move_to((0.0, f64::from(i) * 4.0)); + path.line_to((2.0, f64::from(i) * 4.0)); + } + let mut dashes = vec![0.01; 199]; + dashes.push(10_000.0); + let props = StrokeProps { weight: 1.0, dashes, align: StrokeAlign::Center, ..Default::default() }; + let s = stroke_center(&path, 1.0, &props); + assert!(s.elements().len() < 100_000, "{} elements", s.elements().len()); + } + #[test] fn inside_and_outside_strokes_respect_an_even_odd_hole() { // A ring: both loops run the same way, so only the even-odd rule leaves the hole open.