diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index b3b4394a2..ff5b4b38c 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -84,8 +84,9 @@ impl Default for HitOptions { /// The shapes that an image's own fills and strokes paint, each with its fill rule and the fill or /// stroke that paints it (none for fills and strokes that paint nothing). Effects are evaluated -/// above this crate, so the engine registers one with [`set_image_paint`]. -pub type ImagePaint = fn(&Node) -> Vec<(BezPath, FillRule, AppearanceItem)>; +/// above this crate, so the engine registers one with [`set_image_paint`]. It looks up swatches and +/// patterns in the document it is given. +pub type ImagePaint = fn(Option<&Document>, &Node) -> Vec<(BezPath, FillRule, AppearanceItem)>; static IMAGE_PAINT: OnceLock = OnceLock::new(); @@ -98,8 +99,8 @@ pub fn set_image_paint(f: ImagePaint) { /// The shapes that the image `n`'s own fills and strokes paint ([`set_image_paint`]), none while /// nothing is registered. -pub(crate) fn image_paint(n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { - IMAGE_PAINT.get().map_or_else(Vec::new, |f| f(n)) +pub(crate) fn image_paint(doc: Option<&Document>, n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { + IMAGE_PAINT.get().map_or_else(Vec::new, |f| f(doc, n)) } /// One level of evaluation of a live object in a document, as the canvas draws it: type in an @@ -860,7 +861,7 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> Optio if opt.outline { return None; } - image_paint(n).iter().rev().find_map(|(bp, rule, item)| match item { + image_paint(Some(symbols.document()), n).iter().rev().find_map(|(bp, rule, item)| match item { AppearanceItem::Stroke(s) => s.hits(bp, *rule, p, opt.tol).then_some(HitKind::Stroke), AppearanceItem::Fill(_) => (!opt.path_only && fill_contains(bp, *rule, p)).then_some(HitKind::Fill), }) diff --git a/crates/doc/src/node/ops.rs b/crates/doc/src/node/ops.rs index 2b2bef296..30e325c54 100644 --- a/crates/doc/src/node/ops.rs +++ b/crates/doc/src/node/ops.rs @@ -239,10 +239,12 @@ impl Node { NodeKind::Intertwine { children } => children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.reach(symbols))), // The frame and each shape the image's own fills and strokes paint, a stroke's grown by // the farthest it can reach from its shape. - NodeKind::Image(_) => crate::hit::image_paint(self).iter().fold(self.geometric(symbols), |acc, (bp, _, item)| { - let o = if let crate::AppearanceItem::Stroke(s) = item { s.reach() } else { 0.0 }; - vectorcraft_geom::union_opt(acc, (!bp.elements().is_empty()).then(|| bp.bounding_box().inflate(o, o))) - }), + NodeKind::Image(_) => { + crate::hit::image_paint(symbols.map(SymbolBoxes::document), self).iter().fold(self.geometric(symbols), |acc, (bp, _, item)| { + let o = if let crate::AppearanceItem::Stroke(s) = item { s.reach() } else { 0.0 }; + vectorcraft_geom::union_opt(acc, (!bp.elements().is_empty()).then(|| bp.bounding_box().inflate(o, o))) + }) + } // Keys and steps placed on a spine, turned to it with Align to Path, can reach past the // keys and the spine. NodeKind::Blend { children, spec } => { diff --git a/crates/effects/src/geometry.rs b/crates/effects/src/geometry.rs index f6f007b5c..afb3b268f 100644 --- a/crates/effects/src/geometry.rs +++ b/crates/effects/src/geometry.rs @@ -3,6 +3,7 @@ use super::{ Appearance, AppearanceItem, BezPath, Effect, FillRule, Node, NodeKind, PathData, Rect, StrokeLayer, Value, WarpStyle, distort, stroke, stylize, util, warp, }; +use vectorcraft_doc::Document; /// Context the geometry effects may need from the object's appearance. #[derive(Clone, Copy, Debug, Default, PartialEq)] @@ -22,6 +23,9 @@ pub struct GeomContext<'a> { /// The index in `appearance.items` of the fill or stroke whose own effects these are; `None`: /// the object's effects. pub item: Option, + /// The document that the swatches and patterns named by a live-effect plug-in that paints its + /// pieces are looked up in. Without one, a result that names any is refused. + pub doc: Option<&'a Document>, } impl<'a> GeomContext<'a> { @@ -32,7 +36,7 @@ impl<'a> GeomContext<'a> { _ => FillRule::NonZero, }; let stroke = n.appearance.stroke().filter(|s| !s.paint.is_none() && s.width > 0.0); - Self { stroke, rule, seed: n.id.0, appearance: Some(&n.appearance), item: None } + Self { stroke, rule, seed: n.id.0, appearance: Some(&n.appearance), item: None, doc: None } } /// The context of the effects on `item`, one of the same object's appearance items: a @@ -224,7 +228,7 @@ fn apply_unchecked(id: &str, p: &Value, path: &PathData, b: Rect, ctx: &GeomCont use util::{join, num}; if let Some(plugin) = vectorcraft_plugins::effect::plugin_id(id) { let paints = vectorcraft_plugins::effect::Paints { appearance: ctx.appearance, item: ctx.item }; - return vectorcraft_plugins::effect::apply(plugin, p, path, b, ctx.rule, paints).unwrap_or_else(|| path.clone()); + return vectorcraft_plugins::effect::apply(plugin, p, path, b, ctx.rule, paints, ctx.doc).unwrap_or_else(|| path.clone()); } match id { "distort.freeDistort" => distort::free_distort(path, b, p, max), diff --git a/crates/effects/src/group.rs b/crates/effects/src/group.rs index d34a7fcef..60145c55d 100644 --- a/crates/effects/src/group.rs +++ b/crates/effects/src/group.rs @@ -203,12 +203,12 @@ fn reshaped_art(doc: Option<&Document>, n: &Node, hook: Option, nod // with its own paints, as the renderer draws it. NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } if !fx.is_empty() => { let painted = crate::reshape::has_painted_effects(n).then(|| crate::reshape::reshape(doc, n, None)).flatten(); - Some(painted.unwrap_or_else(|| reshaped(n, n))) + Some(painted.unwrap_or_else(|| reshaped(n, n, doc))) } NodeKind::Group { .. } | NodeKind::Layer { .. } if !fx.is_empty() => match compound_shape_art_in(doc, n, hook, nodes) { - Some(area) => Some(reshaped(&area, n)), + Some(area) => Some(reshaped(&area, n, doc)), None => { - let mut m = crate::reshape::outlined_members_with(None, n, outline, nodes); + let mut m = crate::reshape::outlined_members_with(None, doc, n, outline, nodes); crate::reshape::reshape_leaves(&mut m, &fx, doc); without_geometry_effects(&mut m); Some(m) @@ -229,19 +229,20 @@ fn reshaped_art(doc: Option<&Document>, n: &Node, hook: Option, nod } NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => None, _ if crate::needs_outline(n) && (vectorcraft_doc::live::is_live(n) || !fx.is_empty()) => { - crate::reshape::as_art_with(None, n, None, outline, nodes) + crate::reshape::as_art_with(None, doc, n, None, outline, nodes) } _ => None, } } /// `src`, a path, compound path or compound shape's area, as one path reshaped by the visible -/// geometry effects of `n`, without those effects. -fn reshaped(src: &Node, n: &Node) -> Node { +/// geometry effects of `n`, without those effects. The swatches and patterns a live-effect plug-in +/// that paints its pieces names are looked up in `doc`. +fn reshaped(src: &Node, n: &Node, doc: Option<&Document>) -> Node { let mut m = src.clone(); if let Some((path, rule)) = geometry(src) { let bounds = vectorcraft_geom::Shape::bounding_box(&path.to_bezpath()); - let g = crate::apply_geometry_with(&n.appearance.effects, &path, bounds, &crate::GeomContext::of(n)); + let g = crate::apply_geometry_with(&n.appearance.effects, &path, bounds, &crate::GeomContext { doc, ..crate::GeomContext::of(n) }); m.kind = NodeKind::Path { path: g, rule, live: None, clipping: false, guide: false }; } without_geometry_effects(&mut m); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 1d85d9814..bdf334ae1 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -101,6 +101,6 @@ pub use geometry::{GeomContext, apply_geometry, apply_geometry_bez, apply_geomet #[cfg(test)] pub(crate) use geometry::{MAX_EFFECT_LINES, apply_geometry_within}; pub use params::{ - default_params, has_geometry, has_painted_plugin_effect, has_plugin_effect, is_geometry, is_raster, merged_params, new_effect, plugin_revision, - scale_effect, upgrade_params, + any_plugin_paints_output, default_params, has_geometry, has_painted_plugin_effect, has_plugin_effect, is_geometry, is_raster, merged_params, + new_effect, plugin_revision, scale_effect, upgrade_params, }; diff --git a/crates/effects/src/params.rs b/crates/effects/src/params.rs index 75272c3e1..c12147162 100644 --- a/crates/effects/src/params.rs +++ b/crates/effects/src/params.rs @@ -155,6 +155,11 @@ pub fn has_painted_plugin_effect(n: &Node) -> bool { n.appearance.effects.iter().any(painted) || n.appearance.items.iter().any(|i| i.effects().iter().any(painted)) } +/// Is a live-effect plug-in that paints the pieces it returns installed? +pub fn any_plugin_paints_output() -> bool { + vectorcraft_plugins::registry::list().iter().any(|p| vectorcraft_plugins::effect::paints_output(&p.manifest().id)) +} + /// The plug-in registry's revision, which changes whenever a plug-in is installed or removed. pub fn plugin_revision() -> u64 { vectorcraft_plugins::registry::revision() diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index af3d5d655..3f743993a 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -173,17 +173,18 @@ fn image_rows(items: &[AppearanceItem]) -> Vec { /// The shapes that the image `n`'s own fills and strokes paint ([`image_rows`]), each with its fill /// rule and the fill or stroke that paints it: the paths (or compound paths) along its frame that /// [`reshape`] makes, with each fill's or stroke's geometry effects applied as the renderer applies -/// them. Empty for anything but an image with such fills or strokes. -pub fn image_paint(n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { +/// them. Empty for anything but an image with such fills or strokes. The swatches and patterns that +/// live-effect plug-ins painting their pieces name are looked up in `doc`. +pub fn image_paint(doc: Option<&Document>, n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { if !matches!(n.kind, NodeKind::Image(_)) || !has_item_geometry(n) { return vec![]; } - let Some(art) = reshape(None, n, None) else { return vec![] }; + let Some(art) = reshape(doc, n, None) else { return vec![] }; let mut out = vec![]; art.walk(&mut |c| { let (NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. }) = &c.kind else { return }; let Some(g) = c.stroke_path() else { return }; - let ctx = GeomContext::of(c); + let ctx = GeomContext { doc, ..GeomContext::of(c) }; for item in c.appearance.items.iter().filter(|i| crate::paints(i)) { let fx = item.effects(); let shape = if crate::has_geometry(fx) && !g.elements().is_empty() { @@ -296,14 +297,17 @@ fn with_id(art: &Node, id: NodeId) -> Node { /// through the guides could hold more than [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects. A Repeat counts the Repeats in the art of /// `doc`'s symbols that its instances show toward its instance budget. pub fn outline_art(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { - outline_art_with(doc, n, symbol_art, &outline_text, &SymbolNodes::default(), false) + outline_art_with(doc, doc, n, symbol_art, &outline_text, &SymbolNodes::default(), false) } /// [`outline_art`] with live objects evaluated through `outline` ([`evaluated_with`]); with /// `apart`, also a path or compound path whose Transform effect makes copies -/// ([`has_transform_copies`]), as the one piece the copies are made from. +/// ([`has_transform_copies`]), as the one piece the copies are made from. `symbols` is the document +/// the symbols of the art come from; `paint_doc` is the one the swatches and patterns of painted +/// pieces are looked up in. fn outline_art_with( - doc: Option<&Document>, + symbols: Option<&Document>, + paint_doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>, outline: OutlineHook, @@ -313,7 +317,7 @@ fn outline_art_with( let mut art = match &n.kind { NodeKind::Text(_) => outline_text(n)?, NodeKind::SymbolInstance { symbol, xf } => { - let a = match doc { + let a = match symbols { Some(d) => d.place_symbol_art(symbol, symbol_art?, *xf)?, None => { let mut a = symbol_art?.clone(); @@ -333,10 +337,10 @@ fn outline_art_with( one.appearance.effects.clear(); Node::group(n.id, vec![Arc::new(one)]) } - _ if vectorcraft_doc::live::is_live(n) => evaluated_with(doc, n, outline, nodes), + _ if vectorcraft_doc::live::is_live(n) => evaluated_with(symbols, n, outline, nodes), _ => return None, }; - outline_nested_text(doc, &mut art, false); + outline_nested_text(paint_doc, &mut art, false); art.opacity = 1.0; art.blend = Default::default(); art.isolate = false; @@ -491,7 +495,7 @@ pub fn expand_outlined(doc: Option<&Document>, n: &Node, symbol_art: Option<&Nod // effect in a group of their own (copies of copies nest), as the reference app expands them // (#5612). if has_transform_art(n) || has_transform_copies(n) { - return as_art_opts(doc, n, symbol_art, &outline_text, &SymbolNodes::default(), true); + return as_art_opts(doc, doc, n, symbol_art, &outline_text, &SymbolNodes::default(), true); } let own_paint = match n.kind { NodeKind::Text(_) => !n.appearance.items.is_empty(), @@ -505,17 +509,34 @@ pub fn expand_outlined(doc: Option<&Document>, n: &Node, symbol_art: Option<&Nod /// [`reshape`] without its checks (no geometry effects: just the art). pub(crate) fn as_art(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { - as_art_with(doc, n, symbol_art, &outline_text, &SymbolNodes::default()) + as_art_with(doc, doc, n, symbol_art, &outline_text, &SymbolNodes::default()) } -/// [`as_art`] with live objects evaluated through `outline` ([`evaluated_with`]). -pub(crate) fn as_art_with(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>, outline: OutlineHook, nodes: &SymbolNodes) -> Option { - as_art_opts(doc, n, symbol_art, outline, nodes, false) +/// [`as_art`] with live objects evaluated through `outline` ([`evaluated_with`]). `symbols` is the +/// document the symbols of the art come from; `paint_doc` is the one the swatches and patterns of +/// painted pieces are looked up in. +pub(crate) fn as_art_with( + symbols: Option<&Document>, + paint_doc: Option<&Document>, + n: &Node, + symbol_art: Option<&Node>, + outline: OutlineHook, + nodes: &SymbolNodes, +) -> Option { + as_art_opts(symbols, paint_doc, n, symbol_art, outline, nodes, false) } /// [`as_art_with`]; with `apart`, a path's or compound path's Transform effects make each copy an /// object of its own ([`copy_leaf`]) rather than one path of every copy. -fn as_art_opts(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>, outline: OutlineHook, nodes: &SymbolNodes, apart: bool) -> Option { +fn as_art_opts( + symbols: Option<&Document>, + paint_doc: Option<&Document>, + n: &Node, + symbol_art: Option<&Node>, + outline: OutlineHook, + nodes: &SymbolNodes, + apart: bool, +) -> Option { let fx = geometry_effects(n); let mut art = match &n.kind { NodeKind::Image(im) => { @@ -543,13 +564,13 @@ fn as_art_opts(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>, outl Node::group(n.id, rows(below).into_iter().chain([Arc::new(image)]).chain(rows(above)).collect()) } } - _ => outline_art_with(doc, n, symbol_art, outline, nodes, apart)?, + _ => outline_art_with(symbols, paint_doc, n, symbol_art, outline, nodes, apart)?, }; - reshape_leaves_apart(&mut art, &fx, Budget::EFFECTS, apart, doc); + reshape_leaves_apart(&mut art, &fx, Budget::EFFECTS, apart, paint_doc); if apart { hoist_copies(&mut art); } - transform_items(&mut art, doc); + transform_items(&mut art, paint_doc); // Type and symbols in perspective: the art projected onto their plane. if let Some(h) = n.projection() { project_leaves(&mut art, &h); @@ -1079,7 +1100,7 @@ fn transform_piece(p: &mut Node, seed: u64, max: Budget, used: &mut Budget, doc: AppearanceItem::Stroke(s) => Some(s), AppearanceItem::Fill(_) => None, }; - let ctx = GeomContext { stroke, rule, seed, appearance: Some(&appearance), item: Some(0) }; + let ctx = GeomContext { stroke, rule, seed, appearance: Some(&appearance), item: Some(0), doc }; let out = crate::geometry::apply_geometry_within(std::slice::from_ref(&e), &path, bounds, &ctx, room); *used = used.minus(before).plus(Budget::of(&out)); set_geometry(leaf, out, rule); @@ -1237,14 +1258,21 @@ fn placed_count(n: &Node) -> usize { /// `n` with type anywhere inside it outlined and live objects evaluated, so geometry effects on /// it reach every member. pub(crate) fn outlined_members(doc: Option<&Document>, n: &Node) -> Node { - outlined_members_with(doc, n, &outline_text, &SymbolNodes::default()) + outlined_members_with(doc, doc, n, &outline_text, &SymbolNodes::default()) } /// [`outlined_members`] with live objects and symbol instances given as art through `outline` -/// ([`evaluated_with`]). -pub(crate) fn outlined_members_with(doc: Option<&Document>, n: &Node, outline: OutlineHook, nodes: &SymbolNodes) -> Node { - let mut art = evaluated_with(doc, n, outline, nodes); - outline_nested_text(doc, &mut art, false); +/// ([`evaluated_with`]), with the symbols of the art from `symbols` and the swatches and patterns +/// of painted pieces looked up in `paint_doc`. +pub(crate) fn outlined_members_with( + symbols: Option<&Document>, + paint_doc: Option<&Document>, + n: &Node, + outline: OutlineHook, + nodes: &SymbolNodes, +) -> Node { + let mut art = evaluated_with(symbols, n, outline, nodes); + outline_nested_text(paint_doc, &mut art, false); art } diff --git a/crates/engine/src/cmd/expand.rs b/crates/engine/src/cmd/expand.rs index 54a2012c8..1f28107f0 100644 --- a/crates/engine/src/cmd/expand.rs +++ b/crates/engine/src/cmd/expand.rs @@ -398,7 +398,7 @@ fn expand_fills(d: &mut Document, l: &Node, to: GradientTo) -> Option { if !matches!(l.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) || !l.appearance.items.iter().any(|i| gradient_fill(i).is_some()) { return None; } - let reshaped = reshaped(l); + let reshaped = reshaped(d, l); let l = reshaped.as_ref().unwrap_or(l); let (path, _) = node_path(l)?; let bounds = path.bounds()?; @@ -408,7 +408,7 @@ fn expand_fills(d: &mut Document, l: &Node, to: GradientTo) -> Option { for item in &l.appearance.items { // Items that paint nothing go; a gradient that can't be expanded stays. let art = gradient_fill(item).and_then(|(f, g)| { - let (fl, path, bounds) = fill_shape(l, item, &path, bounds)?; + let (fl, path, bounds) = fill_shape(d, l, item, &path, bounds)?; gradient_art(d, &fl, &path, bounds, f, g, to) }); let Some(art) = art else { @@ -468,14 +468,15 @@ fn gradient_art(d: &mut Document, l: &Node, path: &PathData, bounds: Rect, f: &F /// Path `l` as the canvas paints its fills and strokes: its visible geometry effects applied to its /// path (a plain path then), and every geometry effect of its own removed. `None` when it has no -/// visible geometry effect. -fn reshaped(l: &Node) -> Option { +/// visible geometry effect. The swatches and patterns that live-effect plug-ins painting their +/// pieces name are looked up in `doc`. +fn reshaped(doc: &Document, l: &Node) -> Option { use vectorcraft_render::effects::{GeomContext, apply_geometry_with, has_geometry, is_geometry}; if !has_geometry(&l.appearance.effects) { return None; } let (path, _) = node_path(l)?; - let g = apply_geometry_with(&l.appearance.effects, &path, path.bounds()?, &GeomContext::of(l)); + let g = apply_geometry_with(&l.appearance.effects, &path, path.bounds()?, &GeomContext { doc: Some(doc), ..GeomContext::of(l) }); let mut m = with_path(l, g); m.appearance.effects.retain(|e| !is_geometry(&e.id)); Some(m) @@ -484,13 +485,13 @@ fn reshaped(l: &Node) -> Option { /// Path `l` (outline `path`, box `bounds`) as its fill `item` paints it on the canvas, with its /// outline and box: when the fill has visible geometry effects, `l` with the outline they make of /// `path` as a plain path, and that outline's box. -fn fill_shape(l: &Node, item: &AppearanceItem, path: &PathData, bounds: Rect) -> Option<(Node, PathData, Rect)> { +fn fill_shape(doc: &Document, l: &Node, item: &AppearanceItem, path: &PathData, bounds: Rect) -> Option<(Node, PathData, Rect)> { use vectorcraft_render::effects::{GeomContext, apply_geometry_with, has_geometry}; if !has_geometry(item.effects()) { return Some((l.clone(), path.clone(), bounds)); } // In the fill's context, as drawing runs them: a plug-in effect is told which fill it is on. - let outline = apply_geometry_with(item.effects(), path, bounds, &GeomContext::of(l).item(item)); + let outline = apply_geometry_with(item.effects(), path, bounds, &GeomContext { doc: Some(doc), ..GeomContext::of(l) }.item(item)); let b = outline.bounds()?; Some((with_path(l, outline.clone()), outline, b)) } @@ -777,7 +778,7 @@ fn expand_patterns_where(d: &mut Document, l: &Node, which: impl Fn(&Document, & { return None; } - let reshaped = reshaped(l); + let reshaped = reshaped(d, l); let l = reshaped.as_ref().unwrap_or(l); let (path, _) = node_path(l)?; let bounds = path.bounds()?; @@ -809,7 +810,7 @@ fn expand_patterns_where(d: &mut Document, l: &Node, which: impl Fn(&Document, & later -= usize::from(fill.is_some()); let room = if later > 0 { left.saturating_sub(l.count() + 1) } else { left }; let tiles = fill.filter(|_| !full).and_then(|(f, def, xf)| { - let (fl, _, bounds) = fill_shape(l, item, &path, bounds)?; + let (fl, _, bounds) = fill_shape(d, l, item, &path, bounds)?; let Some(t) = pattern_tiles(d, &def, xf, bounds, room) else { full = true; return None; diff --git a/crates/engine/src/tests/plugins.rs b/crates/engine/src/tests/plugins.rs index b0681516f..8d232f2ca 100644 --- a/crates/engine/src/tests/plugins.rs +++ b/crates/engine/src/tests/plugins.rs @@ -451,6 +451,136 @@ fn pathfinder_and_baked_appearance_take_painted_pieces_with_swatch_links() { assert_eq!(fills, [(square_at(40.0).bounds(), Some(leaf))]); } +/// A swatch-linked piece on screen is drawn when the path it comes from is off screen: the +/// renderer's bounds of the path take in the piece. +#[test] +fn a_swatch_linked_piece_is_not_culled_with_its_off_screen_path() { + let mut s = session(); + let id = rect(&mut s, 500.0, "#ff0000"); + run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"})); + install_wat(&mut s, &tpl::returning(&painting("test.engine.culled", true), &leaf_square())); + run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.culled"})); + assert_eq!(drawn(&s).pixel(50, 50), GREEN); +} + +/// A retained renderer's kept bounds follow the document's swatches: a swatch-linked piece on +/// screen appears when its swatch is added, though the path it comes from is off screen. +#[test] +fn a_retained_renderer_shows_a_piece_once_its_swatch_exists() { + let mut s = session(); + let id = rect(&mut s, 500.0, "#ff0000"); + install_wat(&mut s, &tpl::returning(&painting("test.engine.lateleaf", true), &leaf_square())); + run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.lateleaf"})); + let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() }; + let mut r = vectorcraft_render::Renderer::new(); + let at_piece = |r: &mut vectorcraft_render::Renderer, s: &Session| { + r.render(&s.doc().unwrap().doc, 200, 200, vectorcraft_geom::Affine::IDENTITY, &opts).pixel(50, 50) + }; + assert_eq!(at_piece(&mut r, &s), WHITE, "the piece is refused"); + run(&mut s, "select.none", json!({})); + run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"})); + assert_eq!(at_piece(&mut r, &s), GREEN, "the piece is drawn with its swatch"); +} + +/// Object › Expand of a gradient fill takes the geometry of a piece linked to the document's swatch. +#[test] +fn expanding_a_gradient_fill_takes_a_swatch_linked_piece() { + let mut s = session(); + let id = rect(&mut s, 10.0, "#ff0000"); + run(&mut s, "paint.setFill", json!({"ids": [id.0], "gradient": {"kind": "linear"}})); + run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"})); + install_wat(&mut s, &tpl::returning(&painting("test.engine.expandref", true), &leaf_square())); + run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.expandref"})); + run(&mut s, "select.set", json!({"ids": [id.0]})); + run(&mut s, "object.expand", json!({"object": false, "fill": true, "stroke": false})); + let img = drawn(&s); + assert_eq!(img.pixel(15, 15), WHITE, "the input path is gone"); + assert_ne!(img.pixel(50, 50), WHITE, "the piece's geometry is filled"); +} + +/// The geometry of a painted result that names a swatch counts only when the document has that +/// swatch: without a document, and with one that lacks it, the effect keeps its input. +#[test] +fn painted_geometry_is_used_only_when_its_references_are_in_the_document() { + use vectorcraft_render::effects::{GeomContext, apply_geometry_with}; + let mut s = session(); + let id = rect(&mut s, 10.0, "#ff0000"); + install_wat(&mut s, &tpl::returning(&painting("test.engine.geomref", true), &leaf_square())); + run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.geomref"})); + let n = node(&s, id); + let path = n.path_data().unwrap().clone(); + let input = path.bounds().unwrap(); + let geometry = |doc: Option<&vectorcraft_doc::Document>| { + apply_geometry_with(&n.appearance.effects, &path, input, &GeomContext { doc, ..GeomContext::of(&n) }).bounds() + }; + assert_eq!(geometry(None), Some(input), "no document"); + assert_eq!(geometry(Some(&s.doc().unwrap().doc)), Some(input), "no such swatch"); + run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"})); + assert_eq!(geometry(Some(&s.doc().unwrap().doc)), square_at(40.0).bounds(), "the piece's geometry"); +} + +/// Type with a painted effect, in a group with a live Pathfinder effect: the operation takes the +/// piece linked to the document's swatch. +#[test] +fn pathfinder_takes_the_swatch_linked_pieces_of_type() { + let mut s = session(); + let t = NodeId(run(&mut s, "text.create", json!({"x": 10, "y": 150, "text": "Hi", "size": 20}))["id"].as_u64().unwrap()); + run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"})); + install_wat(&mut s, &tpl::returning(&painting("test.engine.typepaint", true), &leaf_square())); + run(&mut s, "effect.apply", json!({"ids": [t.0], "effect": "plugin.test.engine.typepaint"})); + run(&mut s, "select.set", json!({"ids": [t.0]})); + let g = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap()); + run(&mut s, "effect.apply", json!({"ids": [g.0], "effect": "pathfinder.divide"})); + assert_eq!(drawn(&s).pixel(50, 50), GREEN); +} + +/// Type whose fill has a painted effect, in a group with a geometry effect inside a live Pathfinder +/// effect: the operation takes the piece linked to the document's swatch. +#[test] +fn pathfinder_takes_swatch_linked_pieces_of_type_in_a_reshaped_group() { + let mut s = session(); + let t = NodeId(run(&mut s, "text.create", json!({"x": 10, "y": 150, "text": "Hi", "size": 20}))["id"].as_u64().unwrap()); + run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"})); + install_wat(&mut s, &tpl::returning(&painting("test.engine.memberpaint", true), &leaf_square())); + run(&mut s, "select.set", json!({"ids": [t.0]})); + run(&mut s, "appearance.addFill", json!({})); + run(&mut s, "paint.setFill", json!({"color": "#ff0000", "item": 0})); + run(&mut s, "effect.apply", json!({"effect": "plugin.test.engine.memberpaint", "item": 0})); + let inner = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap()); + run(&mut s, "effect.apply", json!({"ids": [inner.0], "effect": "path.offsetPath", "params": {"offset": 0}})); + let outer = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap()); + run(&mut s, "effect.apply", json!({"ids": [outer.0], "effect": "pathfinder.divide"})); + assert_eq!(drawn(&s).pixel(50, 50), GREEN); +} + +/// A painted effect on an image's own fill that links its piece to a swatch: hit tests and bounds +/// use the piece the canvas draws. +#[test] +fn an_image_s_painted_fill_is_hit_and_measured_as_drawn() { + let mut s = session(); + let mut png = vec![]; + image::RgbaImage::from_pixel(30, 30, image::Rgba([255, 0, 0, 255])) + .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) + .unwrap(); + let r = run(&mut s, "file.place", json!({"name": "red.png", "dataBase64": vectorcraft_format::base64_encode(&png), "at": [65, 65]})); + let im = NodeId(r["ids"][0].as_u64().unwrap()); + run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"})); + let mut leaf = solid("#00ff00"); + leaf["swatch"] = json!("Leaf"); + let out = json!({"objects": [{"path": square_at(120.0), "fills": [leaf]}]}).to_string(); + install_wat(&mut s, &tpl::returning(&painting("test.engine.imagepaint", true), &out)); + run(&mut s, "select.set", json!({"ids": [im.0]})); + run(&mut s, "appearance.addFill", json!({})); + run(&mut s, "paint.setFill", json!({"color": "#ff0000", "item": 0})); + run(&mut s, "effect.apply", json!({"effect": "convertToShape.rectangle", "item": 0})); + run(&mut s, "effect.apply", json!({"effect": "plugin.test.engine.imagepaint", "item": 0})); + assert_eq!(drawn(&s).pixel(130, 50), GREEN, "the canvas draws the piece"); + let doc = &s.doc().unwrap().doc; + assert!(vectorcraft_doc::hit::hit_test(doc, Point::new(130.0, 50.0), Default::default()).is_some(), "a click on the piece hits the image"); + let boxes = doc.symbol_boxes(); + assert!(doc.node(im).unwrap().reach_bounds_with(&boxes).unwrap().x1 >= 140.0, "the bounds take in the piece"); +} + /// A path with a painted effect takes part in a Pathfinder operation as its pieces, each with its /// own paint and the path's opacity: Divide on a group holding it makes one face per piece, /// painted as the piece is. diff --git a/crates/plugins/src/effect.rs b/crates/plugins/src/effect.rs index 0246f609a..db56ca364 100644 --- a/crates/plugins/src/effect.rs +++ b/crates/plugins/src/effect.rs @@ -195,9 +195,14 @@ pub fn paints_output(id: &str) -> bool { /// Runs live-effect plug-in `id` on `path` (whose reference box is `bounds`), of an object painted /// with `paints`: the new geometry, or `None` when the plug-in isn't installed, isn't an effect, or /// fails (the caller then keeps the geometry). For an effect that returns painted pieces, the -/// geometry is every piece's path in order. -pub fn apply(id: &str, params: &Value, path: &PathData, bounds: Rect, rule: FillRule, paints: Paints) -> Option { - evaluate(id, params, path, bounds, rule, paints).map(|(_, out)| out.geometry()) +/// geometry is every piece's path in order, and `None` when the swatches and patterns they name +/// are refused as [`apply_painted`] refuses them. +pub fn apply(id: &str, params: &Value, path: &PathData, bounds: Rect, rule: FillRule, paints: Paints, doc: Option<&Document>) -> Option { + let (plugin, out) = evaluate(id, params, path, bounds, rule, paints)?; + if let Out::Pieces(pieces) = &out { + references_found(id, &plugin, pieces, doc)?; + } + Some(out.geometry()) } /// Runs live-effect plug-in `id`, which declares `paintOutput`, as [`apply`] does: its painted @@ -217,15 +222,21 @@ pub fn apply_painted( ) -> Option>> { let (plugin, out) = evaluate(id, params, path, bounds, rule, paints)?; let Out::Pieces(pieces) = out else { return None }; - // The document's swatches and patterns can change while the result stays cached. - let checked = painted::check_references(&pieces, doc); - if doc.is_some() { - note(id, &plugin, checked.as_ref().map(|_| ())); - } - checked.ok()?; + references_found(id, &plugin, &pieces, doc)?; Some(pieces) } +/// `Some` when every swatch and pattern `pieces` name is in `doc` ([`painted::check_references`]), +/// recording the outcome for [`last_error`] when there is a document. The document's swatches and +/// patterns can change while a result stays cached, so each use checks them. +fn references_found(id: &str, plugin: &crate::Plugin, pieces: &[Piece], doc: Option<&Document>) -> Option<()> { + let checked = painted::check_references(pieces, doc); + if doc.is_some() { + note(id, plugin, checked.as_ref().map(|_| ())); + } + checked.ok() +} + /// Runs plug-in `id` as [`apply`] says, or takes its result from the cache: the plug-in and what /// it gave. fn evaluate(id: &str, params: &Value, path: &PathData, bounds: Rect, rule: FillRule, paints: Paints) -> Option<(Arc, Out)> { diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index abdf1666c..bfb937e6a 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -74,6 +74,7 @@ pub(crate) use bounds::{cull_bounds, drawn_outline, drawn_outlines, painted_reac #[cfg(test)] pub(crate) use entries::Drawn; pub(crate) use entries::{DocInputs, FxGeom, Reads, ShadowEntry}; +pub(crate) use pixels::paint_names; pub(crate) use predicates::{has_fx, has_object_fx}; pub(crate) use raster::PixelEntry; #[cfg(test)] diff --git a/crates/render/src/fx/bounds.rs b/crates/render/src/fx/bounds.rs index 3e2692179..c1b96c591 100644 --- a/crates/render/src/fx/bounds.rs +++ b/crates/render/src/fx/bounds.rs @@ -28,9 +28,10 @@ pub(super) fn apply(effects: &[Effect], bp: &BezPath, ctx: &GeomContext) -> BezP effects::apply_geometry_bez(effects, bp, bp.bounding_box(), ctx) } -/// The object-level effected geometry of `n` (base path `bp`). -pub(crate) fn effected_path(n: &Node, bp: &BezPath) -> BezPath { - apply(&n.appearance.effects, bp, &GeomContext::of(n)) +/// The object-level effected geometry of `n` (base path `bp`), with the swatches and patterns that +/// live-effect plug-ins painting their pieces name looked up in `doc`. +pub(crate) fn effected_path(doc: &Document, n: &Node, bp: &BezPath) -> BezPath { + apply(&n.appearance.effects, bp, &GeomContext { doc: Some(doc), ..GeomContext::of(n) }) } pub(super) fn item_effects(item: &AppearanceItem) -> &[Effect] { @@ -50,8 +51,8 @@ pub(crate) fn visual_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { }); } let bp = node_bezpath(n)?; - let ctx = GeomContext::of(n); - let g = effected_path(n, &bp); + let ctx = GeomContext { doc: Some(symbols.document()), ..GeomContext::of(n) }; + let g = effected_path(symbols.document(), n, &bp); let mut r: Option = (!g.elements().is_empty()).then(|| g.bounding_box()); for item in &n.appearance.items { let fx = item_effects(item); @@ -86,21 +87,21 @@ fn paints(item: &AppearanceItem) -> bool { pub(crate) fn drawn_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { let b = art_bounds(symbols, n)?; let items = n.appearance.items.iter().filter(|i| paints(i) && !effects::raster_effects(item_effects(i)).is_empty()); - Some(items.map(|i| spread(symbols, item_effects(i), item_bounds(n, i).unwrap_or(b))).fold(b, |acc, r| acc.union(r))) + Some(items.map(|i| spread(symbols, item_effects(i), item_bounds(symbols.document(), n, i).unwrap_or(b))).fold(b, |acc, r| acc.union(r))) } /// The bounds of what `item`, a fill or stroke of the path or compound path `n`, paints: the path /// with `n`'s and `item`'s geometry effects, and for a stroke its width. `None` for other objects, /// 3D art, a brushed stroke and an item that draws no path. -fn item_bounds(n: &Node, item: &AppearanceItem) -> Option { +fn item_bounds(doc: &Document, n: &Node, item: &AppearanceItem) -> Option { let stroke = matches!(item, AppearanceItem::Stroke(_)); if !has_fx(n) || effects::has_revolve(n) || effects::has_solid(n) || (stroke && vectorcraft_brush::has_brush(n)) { return None; } - let g = effected_path(n, &node_bezpath(n)?); + let g = effected_path(doc, n, &node_bezpath(n)?); let fx = item_effects(item); if effects::has_geometry(fx) { - let ig = apply(fx, &g, &GeomContext::of(n).item(item)); + let ig = apply(fx, &g, &GeomContext { doc: Some(doc), ..GeomContext::of(n) }.item(item)); let o = if let AppearanceItem::Stroke(s) = item { s.reach() } else { 0.0 }; return (!ig.elements().is_empty()).then(|| ig.bounding_box().inflate(o, o)); } @@ -155,14 +156,14 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { if let Some(art) = effects::revolve_art(n).or_else(|| effects::solid_art(n)) { return drawn_bounds(symbols, &art); } - let g = effected_path(n, &node_bezpath(n)?); + let g = effected_path(symbols.document(), n, &node_bezpath(n)?); // The path as the fills and strokes without geometry effects of their own paint it // (as every fill and stroke would when none paints). let mut plain = n.appearance.clone(); plain.items.retain(|i| !(paints(i) && effects::has_geometry(item_effects(i)))); let on_path = !n.appearance.items.iter().any(paints) || plain.items.iter().any(paints); let mut r = (on_path && !g.elements().is_empty()).then(|| plain.stroked_bounds(g.bounding_box(), || g.clone())); - let ctx = GeomContext::of(n); + let ctx = GeomContext { doc: Some(symbols.document()), ..GeomContext::of(n) }; for item in n.appearance.items.iter().filter(|i| paints(i) && effects::has_geometry(item_effects(i))) { let ig = apply(item_effects(item), &g, &ctx.item(item)); let o = if let AppearanceItem::Stroke(s) = item { s.reach() } else { 0.0 }; @@ -227,7 +228,7 @@ pub(crate) fn drawn_outline(doc: &Document, n: &Node) -> Option<(BezPath, FillRu } match &n.kind { NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. } if effects::has_geometry(&n.appearance.effects) => { - Some((effected_path(n, &node_bezpath(n)?), *rule)) + Some((effected_path(doc, n, &node_bezpath(n)?), *rule)) } _ => effects::clip_outline(n), } diff --git a/crates/render/src/fx/pixels.rs b/crates/render/src/fx/pixels.rs index 688796327..67600cb96 100644 --- a/crates/render/src/fx/pixels.rs +++ b/crates/render/src/fx/pixels.rs @@ -11,6 +11,14 @@ use super::{ RasterFx, Rect, RenderContext, Renderer, Shape, blend_mode, effects, fill_rule, paint, peniko, }; +/// A hash of the names of `doc`'s swatches and patterns. +pub(crate) fn paint_names(doc: &Document) -> u64 { + let mut h = DefaultHasher::new(); + doc.swatches_iter().for_each(|s| s.name.hash(&mut h)); + doc.patterns.iter().for_each(|p| p.name.hash(&mut h)); + h.finish() +} + impl Renderer { /// Paint `content` through the content-changing effects of `rfx` (Gaussian Blur, Feather, the /// Photoshop-style filters and Rasterize) in stack order on its pixels: rendered offscreen over @@ -472,10 +480,7 @@ impl Renderer { (key, Arc::as_ptr(&blob.bytes) as usize, blob.proxy.as_ref().map(|p| Arc::as_ptr(p) as usize)).hash(h); } }); - let names = hash(&mut |h| { - doc.swatches_iter().for_each(|s| s.name.hash(h)); - doc.patterns.iter().for_each(|p| p.name.hash(h)); - }); + let names = paint_names(doc); let found = (patterns, symbols, images, names, Arc::as_ptr(&cms) as usize); let d = match &self.doc_inputs { Some((_, d)) if (d.patterns, d.symbols, d.images, d.names, d.cms) == found => d.clone(), diff --git a/crates/render/src/fx/renderer.rs b/crates/render/src/fx/renderer.rs index 6b7aa49f7..a702c05e4 100644 --- a/crates/render/src/fx/renderer.rs +++ b/crates/render/src/fx/renderer.rs @@ -339,13 +339,13 @@ impl Renderer { let stamp = self.stamp; let e = match self.fx_geom.remove(&key) { Some(e) if Arc::ptr_eq(&e.node, &o) => e, - _ => FxGeom { g: Arc::new(effected_path(n, bp)), items: vec![None; n.appearance.items.len()], node: o, stamp }, + _ => FxGeom { g: Arc::new(effected_path(f.symbols.document(), n, bp)), items: vec![None; n.appearance.items.len()], node: o, stamp }, }; (key, FxGeom { stamp, ..e }) }); let g = match &cached { Some((_, e)) => e.g.clone(), - None => Arc::new(effected_path(n, bp)), + None => Arc::new(effected_path(f.symbols.document(), n, bp)), }; self.draw_shape_fx_geom(ctx, f, n, &g, rule, cached.as_mut().map(|(_, e)| &mut e.items)); if let Some((key, e)) = cached { @@ -370,7 +370,7 @@ impl Renderer { self.hairline(ctx, f, g, [0, 0, 0, 255]); return; } - let gctx = GeomContext::of(n); + let gctx = GeomContext { doc: Some(f.symbols.document()), ..GeomContext::of(n) }; let rfx = effects::raster_effects(&n.appearance.effects); // Document-space reach of the painted geometry (strokes, arrowheads…). let outset = n.appearance.outset(); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 68020dcfa..507b4152f 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -75,6 +75,12 @@ pub struct Renderer { /// The plug-in revision ([`effects::plugin_revision`]) that `geom`, `fx_geom` and `fx_arts` were /// last checked against (see [`Self::check_plugins`]). plugin_revision: u64, + /// Is a live-effect plug-in that paints its pieces installed (found when `plugin_revision` + /// changes)? + painting_plugins: bool, + /// The hash of the document's swatch and pattern names at the last check, kept while + /// `painting_plugins` holds (see [`Self::check_plugins`]). + paint_names: u64, /// Expanded stroke outlines keyed by ([`Self::stroke_owner`], stroke layer address, width bits, /// tolerance level): panning re-renders reuse them instead of re-expanding every stroke. strokes: PtrMap, diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index f23786b49..3835bf57d 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -185,7 +185,7 @@ fn rasterize( let opts = RenderOptions { mark_undecoded_images, ..Default::default() }; r.image_left_out = Some(false); let pattern_images_left_out = images_left_out(); - r.check_plugins(); + r.check_plugins(doc); r.check_symbols(doc); let symbols = doc.symbol_boxes().quick(); r.stamp += 1; diff --git a/crates/render/src/renderer.rs b/crates/render/src/renderer.rs index 490703aa2..b1d9c6f8a 100644 --- a/crates/render/src/renderer.rs +++ b/crates/render/src/renderer.rs @@ -43,6 +43,8 @@ impl Renderer { kept: PtrMap::default(), symbols: vec![], plugin_revision: 0, + painting_plugins: false, + paint_names: 0, strokes: PtrMap::default(), moved_strokes: PtrMap::default(), char_strokes: FrameCache::new(lru::CHAR_STROKE_BUDGET, 0), @@ -113,7 +115,7 @@ impl Renderer { return Rendered { width: w, height: h, pixels }; } } - self.check_plugins(); + self.check_plugins(doc); // The document's own symbols: proofing rewrites the colors and blend modes of their art. self.check_symbols(doc); let prepared = proof::prepare(doc, opts, &mut self.proof_art); @@ -334,7 +336,7 @@ impl Renderer { /// [`Self::render_node`] with `opts` (e.g. [`RenderOptions::mark_undecoded_images`]). pub fn render_node_with(&mut self, doc: &Document, n: &Arc, w: u16, h: u16, view: Affine, opts: &RenderOptions) -> Option { - self.check_plugins(); + self.check_plugins(doc); self.check_symbols(doc); let inv = view.inverse(); let visible = inv.transform_rect_bbox(Rect::new(0.0, 0.0, w as f64, h as f64)); @@ -373,7 +375,7 @@ impl Renderer { background: Option<[u8; 4]>, opts: &RenderOptions, ) -> Option { - self.check_plugins(); + self.check_plugins(doc); self.check_symbols(doc); let b = brush_fx::cull_bounds(&doc.symbol_boxes(), n)?; let w = size.clamp(1, MAX_CONTEXT_SIDE) as u16; diff --git a/crates/render/src/renderer/nodes.rs b/crates/render/src/renderer/nodes.rs index 176739c31..f5c9cfdb0 100644 --- a/crates/render/src/renderer/nodes.rs +++ b/crates/render/src/renderer/nodes.rs @@ -98,15 +98,28 @@ impl Renderer { /// objects with a plug-in effect. Rasters of raster effects and evaluations of live objects hold /// the revision in their keys when what they draw has a plug-in effect (`fx::Inputs`, /// `live::resources_key`). - pub(crate) fn check_plugins(&mut self) { + /// + /// The same caches are dropped when a live-effect plug-in that paints its pieces is installed + /// and the names of `doc`'s swatches or patterns have changed since the last check: whether its + /// pieces are used depends on those names. + pub(crate) fn check_plugins(&mut self, doc: &Document) { let revision = effects::plugin_revision(); - if revision == self.plugin_revision { - return; + let mut stale = false; + if revision != self.plugin_revision { + self.plugin_revision = revision; + self.painting_plugins = effects::any_plugin_paints_output(); + stale = true; + } + if self.painting_plugins { + let names = fx::paint_names(doc); + stale |= names != self.paint_names; + self.paint_names = names; + } + if stale { + self.fx_geom.retain(|_, e| !effects::has_plugin_effect(&e.node)); + self.geom.retain(|_, e| !holds_plugin_effect(&e.node) && !holds_instance(&e.node)); + self.fx_arts.retain(|(node, ..)| !effects::has_plugin_effect(node)); } - self.plugin_revision = revision; - self.fx_geom.retain(|_, e| !effects::has_plugin_effect(&e.node)); - self.geom.retain(|_, e| !holds_plugin_effect(&e.node) && !holds_instance(&e.node)); - self.fx_arts.retain(|(node, ..)| !effects::has_plugin_effect(node)); } /// Cached region a clip group's clipping path `a` clips to ([`effects::clip_outline`]): diff --git a/docs/plugins.md b/docs/plugins.md index 88bc10065..d37d3c37b 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -424,7 +424,8 @@ is a piece drawn by itself, in the order returned, with paints of its own: - The effects after it in the stack apply to the pieces together, the reference box being the bounds of all of them. Bounds, hit tests, raster effects, Pathfinder effects, compound shapes and Envelope Options › Distort Appearance use the pieces as they are drawn. For an effect on a fill or stroke, Pathfinder effects and compound shapes use the - object's path, as they do for every other effect on a fill or stroke. Each piece counts against the budget of the + object's path, as they do for every other effect on a fill or stroke. A 3D effect later in the stack uses the + object's input path when the pieces name a swatch or pattern. Each piece counts against the budget of the object's geometry effects as well as its anchors, so all the painted effects in a stack make at most 100,000 pieces together; a run whose pieces would pass the budget draws its input as it was. - The result is checked whole before any of it is drawn: the limits of a filter's output (100,000 objects, 4,000,000