diff --git a/crates/pathops/src/edit.rs b/crates/pathops/src/edit.rs index da8e5e4d8..17b231a5d 100644 --- a/crates/pathops/src/edit.rs +++ b/crates/pathops/src/edit.rs @@ -146,8 +146,9 @@ fn simplify_subpath(sp: &SubPath, opts: &SimplifyOptions) -> Option { // ends and those lines enclose an area: with straight lines they are the result, and without // them the loop stays as it is. When no split within those limits encloses an area, the loop // stays as it is, or with straight lines each curved segment becomes the lines between its - // samples, up to `MAX_RUN_RESULT` lines in all. A loop whose points are all in line keeps the - // lines of the refit. + // samples, up to `MAX_RUN_RESULT` lines in all with the line segments; when that leaves no + // line for some curved segment, the loop stays as it is. A loop whose points are all in line + // keeps the lines of the refit. let near = |c: &CubicBez| [c.p1, c.p2].iter().all(|&p| Line::new(c.p0, c.p3).nearest(p, 1e-9).distance_sq.sqrt() <= opts.tolerance); let curved = segs.iter().filter(|s| !s.line).count(); for k in 1..=10u32 { @@ -174,7 +175,9 @@ fn simplify_subpath(sp: &SubPath, opts: &SimplifyOptions) -> Option { if !opts.straight_lines { return Some(sp.clone()); } - let room = (MAX_RUN_RESULT / curved.max(1)).min(MAX_SEGMENT_SAMPLES); + let Some(room) = sample_room(curved, n - curved) else { + return Some(sp.clone()); + }; let mut lines: Vec = Vec::new(); for s in &segs { if s.line { @@ -192,6 +195,17 @@ fn simplify_subpath(sp: &SubPath, opts: &SimplifyOptions) -> Option { Some(if opts.straight_lines { s } else { no_worse(sp, s) }) } +/// Most lines between samples of each of `curved` segments, so that they and the `kept` line segments +/// make at most [`MAX_RUN_RESULT`] lines, and each has at most [`MAX_SEGMENT_SAMPLES`]. `None` when +/// that leaves no line for some curved segment. +fn sample_room(curved: usize, kept: usize) -> Option { + let left = MAX_RUN_RESULT.checked_sub(kept)?; + if curved > left { + return None; + } + Some((left / curved.max(1)).min(MAX_SEGMENT_SAMPLES)) +} + /// How far a segment may be from the lines between its samples, as a share of the tolerance. const SAMPLE_GAP: f64 = 0.005; @@ -1156,6 +1170,18 @@ mod tests { assert!(work <= 400 * pts.len(), "{work} steps for {} samples", pts.len()); } + #[test] + fn the_samples_of_a_flat_loop_and_its_line_segments_stay_within_the_result_limit() { + for (curved, kept) in [(1, 0), (32, 1), (40, 1_000_000), (1_000, MAX_RUN_RESULT - 1_000), (3, MAX_RUN_RESULT - 7)] { + let room = sample_room(curved, kept).unwrap_or_else(|| panic!("{curved} curved, {kept} kept: no room")); + assert!((1..=MAX_SEGMENT_SAMPLES).contains(&room), "{curved} curved, {kept} kept: room {room}"); + assert!(curved * room + kept <= MAX_RUN_RESULT, "{curved} curved, {kept} kept: room {room}"); + } + assert_eq!(sample_room(32, 0), Some(MAX_SEGMENT_SAMPLES)); + assert_eq!(sample_room(2, MAX_RUN_RESULT - 1), None); + assert_eq!(sample_room(1, MAX_RUN_RESULT + 5), None); + } + #[test] fn smoothing_points_whose_arc_length_overflows_returns_every_point() { let pts: Vec = (0..40).map(|i| Point::new(if i % 2 == 0 { -1e308 } else { 1e308 }, f64::from(i))).collect();