diff --git a/crates/metafile/src/scene.rs b/crates/metafile/src/scene.rs index 5654d44f3..6f9ed1059 100644 --- a/crates/metafile/src/scene.rs +++ b/crates/metafile/src/scene.rs @@ -17,6 +17,9 @@ use crate::dib::Rgba; /// How deep symbols, brush art and pattern tiles may nest in one another (deeper art is left out). const MAX_NEST: u32 = 8; +/// Most pattern tiles written in all. One fill tiles at most 250,000 (`PatternDef::offsets_covering`), +/// but a tile whose art is filled with a pattern again multiplies that at every level of nesting. +const MAX_TILES: usize = 250_000; /// Most pixels along a side of a gradient's image (its colours are smooth). const MAX_GRADIENT_PX: f64 = 512.0; /// Opacity at or above this is written as opaque. @@ -29,6 +32,7 @@ const RASTER_FX: &str = "raster effects (shadows, glows, blurs, feathering) are const CLIPPING: &str = "clipping masks are not applied: the art they clip is written whole"; const FLAT_GRADIENTS: &str = "gradients are written as one colour, the average of their stops"; const FLAT_PATTERNS: &str = "pattern fills are written as one colour"; +const TOO_MANY_TILES: &str = "pattern fills past 250,000 tiles in all are written as one colour"; /// A stroke as a pen draws it. #[derive(Clone, Debug, PartialEq)] @@ -115,6 +119,8 @@ pub(crate) struct Scene<'a> { opacity: f32, /// Symbols, brush art and pattern tiles being written inside one another. nest: u32, + /// Pattern tiles left to write ([`MAX_TILES`]). + tiles_left: usize, } impl<'a> Scene<'a> { @@ -130,6 +136,7 @@ impl<'a> Scene<'a> { brushes: None, opacity: 1.0, nest: 0, + tiles_left: MAX_TILES, } } @@ -296,9 +303,16 @@ impl<'a> Scene<'a> { match paint { Paint::None => {} Paint::Gradient(g) if self.clips => self.gradient(bp, rule, g, opacity, bounds), - Paint::Pattern { pattern, xf } if self.clips && self.nest < MAX_NEST && self.doc.pattern(pattern).is_some() => { + Paint::Pattern { pattern, xf } if self.clips && self.nest < MAX_NEST && self.tiles_left > 0 && self.doc.pattern(pattern).is_some() => { let area = bp.bounding_box().intersect(self.region); let tiles = self.doc.pattern(pattern).map(|def| def.instances_in(*xf, area)).unwrap_or_default(); + if tiles.len() > self.tiles_left { + self.tiles_left = 0; + self.warn(TOO_MANY_TILES); + self.flat_fill(bp, rule, paint, opacity); + return; + } + self.tiles_left -= tiles.len(); let outer = self.opacity; self.opacity *= opacity.clamp(0.0, 1.0); self.ops.push(Op::Clip { path: bp.clone(), rule }); @@ -308,14 +322,17 @@ impl<'a> Scene<'a> { self.ops.push(Op::Unclip); self.opacity = outer; } - p => { - let Some(rgb) = self.flat(p) else { return }; - self.check_opacity(opacity); - self.ops.push(Op::Fill { path: bp.clone(), rule, rgb }); - } + p => self.flat_fill(bp, rule, p, opacity), } } + /// Fill `bp` with one colour for `paint` at `opacity`. + fn flat_fill(&mut self, bp: &BezPath, rule: FillRule, paint: &Paint, opacity: f32) { + let Some(rgb) = self.flat(paint) else { return }; + self.check_opacity(opacity); + self.ops.push(Op::Fill { path: bp.clone(), rule, rgb }); + } + /// One colour for a paint: a solid colour, the average of a gradient's stops, or a pattern's /// first solid fill. fn flat(&mut self, p: &Paint) -> Option<[u8; 3]> { diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 013ea44b8..330de5e7b 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -4,7 +4,9 @@ use std::sync::Arc; use vectorcraft_color::{Color, Gradient, GradientPaint, Paint}; -use vectorcraft_doc::{Appearance, CharStyle, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, TextObject}; +use vectorcraft_doc::{ + Appearance, CharStyle, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, PatternDef, TextObject, +}; use vectorcraft_geom::{Affine, PathData, Point, Rect, shapes}; use crate::*; @@ -340,3 +342,34 @@ fn an_instance_whose_art_reaches_the_artboard_is_written() { let k = kinds(&emf_records(&emf(&d).bytes)); assert!(k.contains(&39), "a brush fills the art: {k:?}"); } + +/// A pattern `name` repeating `art` every point. +fn point_tiles(name: &str, art: Node) -> PatternDef { + let mut def = PatternDef::new(name, vec![Arc::new(art)]); + def.tile = Rect::new(0.0, 0.0, 1.0, 1.0); + def +} + +fn pattern(name: &str) -> Paint { + Paint::Pattern { pattern: name.into(), xf: Affine::IDENTITY } +} + +#[test] +fn patterns_inside_pattern_tiles_share_one_tile_budget() { + // About 3,700 tiles of a 12 pt square, each filled with about 140 tiles of a dot: over half a + // million fills when every tile is written. + let square = path(shapes::rectangle(Rect::new(0.0, 0.0, 12.0, 12.0)), pattern("dots"), Paint::None, 0.0); + let mut d = doc_with(50.0, 50.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), pattern("squares"), Paint::None, 0.0)]); + d.patterns.push(point_tiles("dots", path(shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), red(), Paint::None, 0.0))); + d.patterns.push(point_tiles("squares", square)); + let (ops, warnings) = crate::scene::Scene::new(&d, d.artboards[0].rect, true).run(); + assert!(ops.len() < 300_000, "{} operations", ops.len()); + assert!(warnings.iter().any(|w| w.contains("one colour")), "{warnings:?}"); + + // A tile filled with its own pattern multiplies at every level down to the nesting limit. + let square = path(shapes::rectangle(Rect::new(0.0, 0.0, 4.0, 4.0)), pattern("self"), Paint::None, 0.0); + let mut d = doc_with(10.0, 10.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), pattern("self"), Paint::None, 0.0)]); + d.patterns.push(point_tiles("self", square)); + let (ops, _) = crate::scene::Scene::new(&d, d.artboards[0].rect, true).run(); + assert!(ops.len() < 300_000, "{} operations", ops.len()); +}