diff --git a/crates/metafile/src/cliprect.rs b/crates/metafile/src/cliprect.rs index bc7221541..5ecfdfc2f 100644 --- a/crates/metafile/src/cliprect.rs +++ b/crates/metafile/src/cliprect.rs @@ -104,14 +104,22 @@ fn is_rect(clip: &[P]) -> bool { /// Sutherland–Hodgman: `poly` cut by convex `clip`. pub(crate) fn clip_polygon(poly: &[P], clip: &[P]) -> Vec

{ - if poly.len().saturating_mul(clip.len()) > MAX_CLIP_WORK { - return Vec::new(); + clip_polygon_within(poly, clip, &mut { MAX_CLIP_WORK }).unwrap_or_default() +} + +/// [`clip_polygon`] charging each point visited against each clip edge to `budget`: the polygon +/// can gain a vertex at every edge, so its cost isn't known from its input size alone. `None` +/// once the budget is spent. +fn clip_polygon_within(poly: &[P], clip: &[P], budget: &mut usize) -> Option> { + if poly.len().saturating_mul(clip.len()) > *budget { + return None; } let mut poly = poly.to_vec(); for i in 0..clip.len() { - if poly.len().saturating_mul(clip.len() - i) > MAX_CLIP_WORK { - return Vec::new(); + if poly.len().saturating_mul(clip.len() - i) > *budget { + return None; } + *budget -= poly.len(); let (a, b) = (clip[i], clip[(i + 1) % clip.len()]); let keep = |p: P| cross(a, b, p) >= 0.0; let cut = |p: P, q: P| { @@ -137,7 +145,7 @@ pub(crate) fn clip_polygon(poly: &[P], clip: &[P]) -> Vec

{ break; } } - poly + Some(poly) } fn disjoint(b: [f64; 4], c: [f64; 4]) -> bool { @@ -167,7 +175,7 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec { } let mut out = Vec::new(); if fill.is_some() { - let f = clip_fill(&subs, clip); + let Some(f) = clip_fill(&subs, clip) else { return Vec::new() }; if !f.is_empty() { out.push(Raw::Path { ops: f, fill, stroke: None, even_odd, region: region.clone() }); } @@ -192,7 +200,7 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec { if too_much_work(&subs, clip) { return Vec::new(); } - let f = clip_fill(&subs, clip); + let f = clip_fill(&subs, clip).unwrap_or_default(); if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile, region }] } } Raw::Bitmap { rect, img, region } => crop(rect, img, clip).map(|(rect, img)| vec![Raw::Bitmap { rect, img, region }]).unwrap_or_default(), @@ -247,17 +255,20 @@ 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 } -fn clip_fill(subs: &[(Vec

, bool)], clip: &[P]) -> Vec { +/// The clipped fill of every subpath, its clipping work shared by all of them: `None` (the whole +/// item goes) once [`MAX_CLIP_WORK`] is spent. +fn clip_fill(subs: &[(Vec

, bool)], clip: &[P]) -> Option> { let mut ops = Vec::new(); + let mut budget = MAX_CLIP_WORK; for (pts, _) in subs { - let poly = clip_polygon(pts, clip); + let poly = clip_polygon_within(pts, clip, &mut budget)?; if poly.len() >= 3 { ops.push(Op::M(poly[0])); ops.extend(poly[1..].iter().map(|p| Op::L(*p))); ops.push(Op::Z); } } - ops + Some(ops) } fn clip_stroke(subs: &[(Vec

, bool)], clip: &[P]) -> Vec { diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index ff6559ec1..3ad3a7886 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -2089,3 +2089,20 @@ fn clip_work_is_bounded_across_subpaths() { assert!(crate::cliprect::clip_raw(raw(many), &clip).is_empty(), "the whole item's clipping work is bounded"); assert_eq!(crate::cliprect::clip_raw(raw(tri(100.0).to_vec()), &clip).len(), 2, "a small crossing shape is still clipped"); } + +/// Clipping a fill can add a vertex at every clip edge, so a polygon's work isn't its input size: +/// shapes that pass the up-front estimate but grow while clipped are charged as they grow, against +/// one budget for the whole item. +#[test] +fn growing_fill_clips_share_one_budget() { + use crate::canvas::Raw; + use crate::geom::Op; + 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(); + // A triangle around the whole circle: clipped, it becomes the 512-gon, about 130,000 steps. + let tri = [Op::M((-1000.0, -1000.0)), Op::L((1000.0, -1000.0)), Op::L((100.0, 1000.0)), Op::Z]; + let raw = |n: usize| Raw::Path { ops: tri.repeat(n), fill: Some([0, 0, 0, 255]), stroke: None, even_odd: false, region: None }; + // 400 of them pass the estimate (400 x 4 x 512 is under a million) but cost about 52 million. + assert!(crate::cliprect::clip_raw(raw(400), &clip).is_empty(), "the growing work is charged"); + assert_eq!(crate::cliprect::clip_raw(raw(2), &clip).len(), 1, "a couple of them are still clipped"); +}