diff --git a/crates/metafile/src/cliprect.rs b/crates/metafile/src/cliprect.rs index d4940475a..d85d2a167 100644 --- a/crates/metafile/src/cliprect.rs +++ b/crates/metafile/src/cliprect.rs @@ -168,7 +168,7 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec { return Vec::new(); } let subs = flatten(&ops); - if too_much_work(&subs, clip) { + if too_much_work(&subs, clip, usize::from(fill.is_some()) + usize::from(stroke.is_some())) { return Vec::new(); } let mut out = Vec::new(); @@ -195,7 +195,7 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec { return Vec::new(); } let subs = flatten(&ops); - if too_much_work(&subs, clip) { + if too_much_work(&subs, clip, 1) { return Vec::new(); } let f = clip_fill(&subs, clip).unwrap_or_default(); @@ -248,9 +248,11 @@ pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec

, bool)> { } /// Whether cutting every subpath of one item by `clip` would exceed [`MAX_CLIP_WORK`]; the -/// per-polygon check alone lets many small subpaths each spend the whole allowance. -fn too_much_work(subs: &[(Vec

, bool)], clip: &[P]) -> bool { - subs.iter().map(|s| s.0.len() + 1).sum::().saturating_mul(clip.len()) > MAX_CLIP_WORK +/// per-polygon check alone lets many small subpaths each spend the whole allowance. `passes` is +/// how many clipping passes the item gets (fill and stroke each cut every subpath), so one item +/// shares a single allowance between them. +fn too_much_work(subs: &[(Vec

, bool)], clip: &[P], passes: usize) -> bool { + subs.iter().map(|s| s.0.len() + 1).sum::().saturating_mul(clip.len()).saturating_mul(passes) > MAX_CLIP_WORK } /// The clipped fill of every subpath, its clipping work shared by all of them: `None` (the whole diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index ba4d826fd..e8db613ef 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -2086,6 +2086,23 @@ fn clip_polygon_charges_actual_work_not_projected_growth() { assert!(clipped.len() > 100, "the intersection is kept: {}", clipped.len()); } +#[test] +fn clip_work_is_shared_by_fill_and_stroke() { + use crate::canvas::Raw; + use crate::geom::Op; + // One zigzag subpath crossing a 512-gon's edge: about 510k point-edge tests per pass, so a + // fill alone fits the allowance but a fill plus a stroke would spend it twice. + let clip: Vec<(f64, f64)> = + (0..512).map(|i| (i as f64 / 512.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (100.0 + 100.0 * c, 100.0 + 100.0 * s)).collect(); + let mut ops = vec![Op::M((0.0, 95.0))]; + ops.extend((1..1000).map(|i| Op::L((i as f64 * 0.19, if i % 2 == 0 { 95.0 } else { 300.0 })))); + ops.push(Op::Z); + let raw = |fill, stroke| Raw::Path { ops: ops.clone(), fill, stroke, even_odd: false, region: None }; + let black = [0, 0, 0, 255]; + assert!(!crate::cliprect::clip_raw(raw(Some(black), None), &clip).is_empty(), "a fill alone is within the allowance"); + assert!(crate::cliprect::clip_raw(raw(Some(black), Some((black, 1.0))), &clip).is_empty(), "fill and stroke share one allowance"); +} + #[test] fn clip_work_is_bounded_across_subpaths() { use crate::canvas::Raw;