Files
vectorcraft/crates/effects/src/reshape.rs
T

3199 lines
160 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Object-level effects on objects that aren't paths: type, placed images, symbol instances and
//! live objects (blends, envelopes, meshes, repeats).
//!
//! Geometry effects reach them through outlines: [`outline_art`] turns the object into plain paths
//! (glyph outlines, the evaluated live object, the symbol's art) and [`reshape`] runs each effect
//! over all of them as one piece, with the whole art's bounds as the effect's reference box, so a
//! warp bends a line of type as a whole. An image is clipped by its reshaped frame, and a Warp
//! effect bends its pixels with the frame ([`cut_warped_images`]). Raster effects (shadows, glows, blur,
//! feather) stay on the result for the renderer and exporters. Type without
//! inline graphics whose own fills or strokes have geometry effects is outlined too: each of those
//! effects reshapes the whole outline that its fill or stroke paints. An image's own fills and
//! strokes paint only the shape that a Convert to Shape or Outline Object effect on them gives
//! them ([`image_rows`]): Outline Object follows the outline of the image's opaque area
//! ([`opaque_area`]).
use std::collections::{BTreeMap, HashMap, HashSet};
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock, PoisonError, Weak};
use vectorcraft_doc::live::Hooks;
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, ImageObject, Node, NodeId, NodeKind};
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, Shape, shapes};
use crate::{Budget, GeomContext, OutlineHook, apply_one, is_geometry, merged_params};
/// Can object-level effects apply to `n` only through [`outline_art`] / an offscreen layer (it is
/// neither a path nor a container)?
pub fn needs_outline(n: &Node) -> bool {
matches!(
n.kind,
NodeKind::Text(_)
| NodeKind::Image(_)
| NodeKind::SymbolInstance { .. }
| NodeKind::Blend { .. }
| NodeKind::Envelope { .. }
| NodeKind::Mesh(_)
| NodeKind::Repeat(_)
| NodeKind::PlacedDocument(_)
)
}
/// Is `n` a path or compound path with a visible Transform effect that makes copies and a gradient
/// in a fill or stroke? Such an object is drawn through its art ([`outline_art`]) so that each copy
/// paints its gradient by itself ([`copy_leaf`]), as a group's copies do.
pub fn has_gradient_copies(n: &Node) -> bool {
(n.appearance.has_unplaced_gradient() || n.appearance.has_placed_gradient()) && has_transform_copies(n)
}
/// Is `n` a path or compound path with a visible Transform effect that makes copies and fills and
/// strokes that paint the copies differently as one path than as separate objects
/// ([`paints_copies_apart`])? Such an object is drawn through its art ([`outline_art`]), each copy
/// an object of its own below the one before ([`copy_leaf`]), as Expand Appearance makes it
/// (#5920, #6316).
pub(crate) fn has_layered_copies(n: &Node) -> bool {
leaf_geometry(n).is_some_and(|(_, rule)| paints_copies_apart(&n.appearance, rule)) && has_transform_copies(n)
}
/// Do the fills and strokes `a` of a path with fill rule `rule` paint a Transform effect's copies
/// differently when the copies are one path than when each is an object of its own? They do
/// unless they paint once, in full and in Normal mode, with a stroke or a nonzero fill: then the
/// one path paints what the copies paint together. A fill and a stroke paint each copy's stroke
/// under the fills of the copies above it, a second fill or stroke likewise, the even-odd rule
/// leaves holes where copies overlap, and a partly transparent or blended fill or stroke paints
/// overlapping copies twice (#5920).
fn paints_copies_apart(a: &Appearance, rule: FillRule) -> bool {
let mut painting = a.items.iter().filter(|i| crate::paints(i));
let (Some(one), None) = (painting.next(), painting.next()) else {
// None paints (nothing to draw either way), or several do.
return a.items.iter().any(crate::paints);
};
one.opacity() < 1.0 || one.blend() != Default::default() || (one.is_fill() && rule != FillRule::NonZero)
}
/// Is `n` a path or compound path with a visible Transform effect that makes copies? Expand
/// Appearance makes each copy an object of its own ([`expand_outlined`]).
pub fn has_transform_copies(n: &Node) -> bool {
matches!(n.kind, NodeKind::Path { guide: false, clipping: false, .. } | NodeKind::Compound { .. })
&& geometry_effects(n)
.iter()
.any(|e| e.id == "distort.transform" && crate::distort::transform_map(Rect::ZERO, &merged_params(&e.id, &e.params), 0).1 > 0)
}
/// Is `n` a path or compound path with a visible Transform effect whose Transform Patterns or
/// Scale Strokes & Effects is on (#885), its own or one of a fill or stroke that paints
/// ([`item_transform_options`])? Such an effect changes the fills and strokes as well as the
/// geometry, so the object is drawn through its art ([`outline_art`]), each copy an object of its
/// own with its strokes scaled and its patterns moved ([`transform_leaf`], [`transform_items`]).
pub fn has_transform_options(n: &Node) -> bool {
matches!(n.kind, NodeKind::Path { guide: false, clipping: false, .. } | NodeKind::Compound { .. })
&& (geometry_effects(n)
.iter()
// Both options default to off, so the stored params tell without merging the defaults
// (this runs for every object drawn).
.any(|e| e.id == "distort.transform" && crate::distort::TransformOptions::of(&e.params).reach_appearance())
|| n.appearance.items.iter().any(item_transform_options))
}
/// Does `item` paint and carry a Transform effect of its own that [`reaches_appearance`]?
fn item_transform_options(item: &AppearanceItem) -> bool {
crate::paints(item) && item.effects().iter().any(reaches_appearance)
}
/// Is `e` a visible Transform effect whose Transform Patterns or Scale Strokes & Effects is on?
fn reaches_appearance(e: &Effect) -> bool {
e.visible && e.id == "distort.transform" && crate::distort::TransformOptions::of(&e.params).reach_appearance()
}
/// Is `n` a path or compound path that its Transform effects make into art of separate objects
/// ([`has_gradient_copies`], [`has_layered_copies`], [`has_transform_options`]), or whose
/// live-effect plug-ins paint what they return ([`has_painted_effects`])? It is drawn, baked and
/// expanded through that art ([`reshape`]).
pub fn has_transform_art(n: &Node) -> bool {
has_gradient_copies(n) || has_layered_copies(n) || has_transform_options(n) || has_painted_effects(n)
}
/// Is `e` a visible live-effect plug-in whose manifest declares `paintOutput`: its id's plug-in id,
/// else `None`.
fn painted_plugin(e: &Effect) -> Option<&str> {
let id = vectorcraft_plugins::effect::plugin_id(&e.id).filter(|_| e.visible)?;
vectorcraft_plugins::effect::paints_output(id).then_some(id)
}
/// Is `n` a path or compound path with a live-effect plug-in that paints what it returns
/// ([`painted_plugin`]), its own or on a fill or stroke that paints? Each piece the plug-in returns
/// is then an object of its own with its own paints ([`paint_leaf`], [`transform_item_leaf`]).
pub fn has_painted_effects(n: &Node) -> bool {
matches!(n.kind, NodeKind::Path { guide: false, clipping: false, .. } | NodeKind::Compound { .. })
&& (n.appearance.effects.iter().any(|e| painted_plugin(e).is_some())
|| n.appearance.items.iter().any(|i| crate::paints(i) && i.effects().iter().any(|e| painted_plugin(e).is_some())))
}
/// Is `n` type without inline graphics that has a fill or stroke of its own that paints and has a
/// visible geometry effect, an image with a fill or stroke of its own that paints a shape
/// ([`image_rows`]), or a symbol instance whose active fill or stroke has a visible geometry effect
/// ([`active_fill`], [`active_stroke`])? On type, such an effect reshapes the whole outline that its
/// fill or stroke paints ([`outline_text`], which leaves inline graphics out). On a symbol
/// instance, it reshapes the fills or strokes of the art it places ([`add_instance_row_effects`]).
pub fn has_item_geometry(n: &Node) -> bool {
match &n.kind {
NodeKind::Text(t) => {
t.runs.iter().all(|r| r.inline.is_none()) && n.appearance.items.iter().any(|i| crate::paints(i) && crate::has_geometry(i.effects()))
}
NodeKind::Image(_) => n.appearance.items.iter().any(shapes_image),
NodeKind::SymbolInstance { .. } => {
active_fill(n).is_some_and(|f| crate::has_geometry(&f.effects)) || active_stroke(n).is_some_and(|s| crate::has_geometry(&s.effects))
}
_ => false,
}
}
/// The fill of symbol instance `n` whose effects its art takes: its topmost fill, the one that
/// stains the art, when it is visible and its opacity is above 0. A lower fill's effects do not
/// reach the art.
fn active_fill(n: &Node) -> Option<&vectorcraft_doc::FillLayer> {
n.appearance.fill().filter(|f| f.visible && f.opacity > 0.0)
}
/// The stroke of symbol instance `n` whose effects its art takes: its topmost stroke, when it is
/// visible and its opacity is above 0.
fn active_stroke(n: &Node) -> Option<&vectorcraft_doc::StrokeLayer> {
n.appearance.stroke().filter(|s| s.visible && s.opacity > 0.0)
}
/// Is `n` a symbol instance whose active fill or stroke has a visible raster effect (a shadow, glow,
/// blur, feather or other painted effect, [`crate::is_raster`])? Its art takes that effect on each
/// fill or stroke in it that paints ([`add_instance_row_effects`]), and draws it there.
pub fn has_instance_row_raster(n: &Node) -> bool {
let raster = |fx: &[Effect]| fx.iter().any(|e| e.visible && crate::is_raster(&e.id));
matches!(n.kind, NodeKind::SymbolInstance { .. })
&& (active_fill(n).is_some_and(|f| raster(&f.effects)) || active_stroke(n).is_some_and(|s| raster(&s.effects)))
}
/// The visible geometry and raster effects in `fx`, in order: those a symbol instance's active fill
/// or stroke passes on to its art.
fn row_effects_of(fx: &[Effect]) -> Vec<Effect> {
fx.iter().filter(|e| e.visible && (is_geometry(&e.id) || crate::is_raster(&e.id))).cloned().collect()
}
/// `art`, the art symbol instance `n` places, with the visible geometry and raster effects of
/// `n`'s active fill added after the effects of every fill in it that paints, and those of its
/// active stroke after the effects of every stroke in it that paints ([`active_fill`],
/// [`active_stroke`]): a Drop Shadow on the instance's fill shadows each filled shape in the art.
/// Each fill or stroke keeps its paint, and the others keep their shapes. Live objects in it that are
/// reached are evaluated first (with the symbols of `doc`), so the effects reshape what they draw.
/// Clip groups' clipping paths, guides and the art of opacity masks keep their shapes. A symbol
/// instance in the art takes the effects on its own active fill or stroke, or on a new fill or
/// stroke without paint when it has none, and passes them on to its own art in turn; an instance
/// of a symbol whose art (in `doc`) holds an instance of `n`'s symbol, at any depth, keeps its
/// appearance, so the effects never pass round a symbol that holds itself. Without `doc`, no
/// instance in the art takes them.
fn add_instance_row_effects(doc: Option<&Document>, n: &Node, art: &mut Node) {
let NodeKind::SymbolInstance { symbol, .. } = &n.kind else { return };
let fill = active_fill(n).map_or(vec![], |f| row_effects_of(&f.effects));
let stroke = active_stroke(n).map_or(vec![], |s| row_effects_of(&s.effects));
if !fill.is_empty() || !stroke.is_empty() {
add_row_effects(doc, symbol, art, &fill, &stroke);
}
}
/// Does the art of the symbol named `from` in `doc` hold an instance of the symbol named `to`, at
/// any depth of the symbols placed in it? `seen` holds the symbols already looked into.
fn places_symbol(doc: &Document, from: &str, to: &str, seen: &mut HashSet<String>) -> bool {
if !seen.insert(from.to_string()) {
return false;
}
let Some(sym) = doc.symbols.iter().find(|s| s.name == from) else { return false };
let mut names = vec![];
sym.art.walk(&mut |c| {
if let NodeKind::SymbolInstance { symbol, .. } = &c.kind {
names.push(symbol.clone());
}
});
names.iter().any(|name| name == to || places_symbol(doc, name, to, seen))
}
/// [`add_instance_row_effects`] of `fill` and `stroke` to `n` and the art in it, the art of an
/// instance of the symbol named `outer`.
fn add_row_effects(doc: Option<&Document>, outer: &str, n: &mut Node, fill: &[Effect], stroke: &[Effect]) {
match &n.kind {
NodeKind::Path { clipping: true, .. } | NodeKind::Path { guide: true, .. } | NodeKind::RulerGuide { .. } => return,
NodeKind::SymbolInstance { symbol, .. } => {
if let Some(d) = doc
&& !places_symbol(d, symbol, outer, &mut HashSet::new())
{
add_to_instance(n, fill, stroke);
}
return;
}
_ if vectorcraft_doc::live::is_live(n) => *n = evaluated(doc, n),
_ => {}
}
let takes = |i: &AppearanceItem| {
crate::paints(i) && !matches!(i, AppearanceItem::Fill(_) if fill.is_empty()) && !matches!(i, AppearanceItem::Stroke(_) if stroke.is_empty())
};
if n.appearance.items.iter().any(takes) {
for item in n.appearance.items.iter_mut().filter(|i| takes(i)) {
let fx = if matches!(item, AppearanceItem::Fill(_)) { fill } else { stroke };
item.effects_mut().extend_from_slice(fx);
}
}
// A compound path paints its members with its own fills and strokes.
if matches!(n.kind, NodeKind::Compound { .. }) {
return;
}
// A clip group's first member is its clipping path, whatever its kind.
let clips = n.clips();
for (i, c) in n.children_mut().into_iter().flatten().enumerate() {
if !(clips && i == 0) {
add_row_effects(doc, outer, Arc::make_mut(c), fill, stroke);
}
}
}
/// Add `fill` to the effects of symbol instance `n`'s active fill and `stroke` to those of its
/// active stroke ([`active_fill`], [`active_stroke`]); without one, to a new topmost fill or stroke
/// without paint, which neither stains nor strokes its art.
fn add_to_instance(n: &mut Node, fill: &[Effect], stroke: &[Effect]) {
if !fill.is_empty() {
match n.appearance.fill_mut().filter(|f| f.visible && f.opacity > 0.0) {
Some(f) => f.effects.extend_from_slice(fill),
None => {
let mut f = vectorcraft_doc::FillLayer::new(vectorcraft_doc::color::Paint::None);
f.effects = fill.to_vec();
n.appearance.items.push(AppearanceItem::Fill(f));
}
}
}
if !stroke.is_empty() {
match n.appearance.stroke_mut().filter(|s| s.visible && s.opacity > 0.0) {
Some(s) => s.effects.extend_from_slice(stroke),
None => {
let mut s = vectorcraft_doc::StrokeLayer::new(vectorcraft_doc::color::Paint::None, 0.0);
s.effects = stroke.to_vec();
n.appearance.items.push(AppearanceItem::Stroke(s));
}
}
}
}
/// Does effect `e` give an image's fill or stroke a shape to paint (a visible Convert to Shape or
/// Outline Object)?
fn gives_shape(e: &Effect) -> bool {
e.visible
&& matches!(e.id.as_str(), "convertToShape.rectangle" | "convertToShape.roundedRectangle" | "convertToShape.ellipse" | "path.outlineObject")
}
/// Does `item`, a fill or stroke of an image, paint something: does it paint and carry an effect
/// that gives it a shape ([`gives_shape`])?
fn shapes_image(item: &AppearanceItem) -> bool {
crate::paints(item) && gives_image_shape(item.effects())
}
/// Do `effects`, those of an image's fill or stroke, give that fill or stroke a shape to paint
/// ([`gives_shape`])?
pub fn gives_image_shape(effects: &[Effect]) -> bool {
effects.iter().any(gives_shape)
}
/// The fills and strokes of `items` (an image's own) that paint something ([`shapes_image`]), each
/// without the effects before its first Convert to Shape or Outline Object. Convert to Shape sizes
/// its shape from the image's box, and Outline Object gives the outline of the image's opaque
/// area ([`opaque_area`]), or its frame when it has no transparency. The others paint nothing.
fn image_rows(items: &[AppearanceItem]) -> Vec<AppearanceItem> {
items
.iter()
.filter(|i| shapes_image(i))
.map(|i| {
let mut i = i.clone();
let fx = i.effects_mut();
let first = fx.iter().position(gives_shape).unwrap_or(0);
fx.drain(..first);
i
})
.collect()
}
/// Does `item`, one of [`image_rows`], start from the outline of the image's opaque area (is its
/// first effect Outline Object rather than Convert to Shape)?
fn follows_opaque_area(item: &AppearanceItem) -> bool {
item.effects().first().is_some_and(|e| e.id == "path.outlineObject")
}
/// Is `n` an image with a fill or stroke of its own that follows the outline of the image's opaque
/// area ([`image_rows`], [`opaque_area`])? The shape that fill or stroke paints then changes with
/// the image's pixels in the document while `n` stays the same.
pub fn follows_image_pixels(n: &Node) -> bool {
matches!(n.kind, NodeKind::Image(_))
&& n.appearance
.items
.iter()
.any(|i| shapes_image(i) && i.effects().iter().find(|e| gives_shape(e)).is_some_and(|e| e.id == "path.outlineObject"))
}
/// The outline of a set of pixels' opaque area, in the pixel space they decode to, with that size
/// ([`traced_opaque_area`]). `None` for pixels that have no transparency or could not be traced.
type Traced = Option<(Arc<PathData>, (u32, u32))>;
/// One of [`Outlines`]' entries: the pixels, held weakly, and their outline once it is traced.
type Entry = (Weak<Vec<u8>>, Arc<OnceLock<Traced>>);
/// The outlines [`opaque_area`] has traced, by the address of the pixels each was traced from.
struct Outlines {
/// Each outline with its pixels. An entry does not keep its pixels, and while it is kept no
/// other pixels can take their address. Entries whose pixels are gone are dropped before a
/// new outline is traced and every [`PRUNE_EVERY`] lookups.
kept: BTreeMap<usize, Entry>,
/// The lookups since the last pruning.
lookups: usize,
}
static OUTLINES: Mutex<Outlines> = Mutex::new(Outlines { kept: BTreeMap::new(), lookups: 0 });
/// How many lookups [`OUTLINES`] takes between two prunings of its entries for pixels that are gone.
const PRUNE_EVERY: usize = 256;
#[cfg(test)]
thread_local! {
/// The outlines [`traced_opaque_area`] has traced on this thread.
pub(crate) static TRACES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Does [`OUTLINES`] keep an entry for `pixels`?
#[cfg(test)]
pub(crate) fn keeps_outline(pixels: &Weak<Vec<u8>>) -> bool {
OUTLINES.lock().unwrap_or_else(PoisonError::into_inner).kept.values().any(|(w, _)| w.ptr_eq(pixels))
}
/// The outline of the opaque area of the image `im` in the document
/// ([`vectorcraft_trace::opaque_outline`]: its pixels that are at least half opaque, outlined with
/// fitted curves), its pixels taken from `im` itself or from the first of `docs` that holds them.
/// `None` when the image has no transparency, its pixels can't be read or are too large to read
/// ([`vectorcraft_trace::decode_alpha`]), or its outline would be too complex: Outline Object then
/// follows the image's frame. The outline is traced once for each set of pixels and kept while
/// those pixels are: a caller that asks while another thread traces the same pixels waits for that
/// trace.
pub(crate) fn opaque_area(docs: &[Option<&Document>], im: &ImageObject) -> Option<PathData> {
let blob = im.pixels.as_deref().or_else(|| docs.iter().flatten().find_map(|d| d.images.get(&im.key)))?;
let bytes = &blob.bytes;
let cell = {
let mut o = OUTLINES.lock().unwrap_or_else(PoisonError::into_inner);
o.lookups += 1;
if o.lookups >= PRUNE_EVERY {
o.lookups = 0;
o.kept.retain(|_, (w, _)| w.strong_count() > 0);
}
let key = Arc::as_ptr(bytes).addr();
match o.kept.get(&key) {
Some((_, cell)) => Arc::clone(cell),
None => {
o.kept.retain(|_, (w, _)| w.strong_count() > 0);
let cell = Arc::new(OnceLock::new());
o.kept.insert(key, (Arc::downgrade(bytes), Arc::clone(&cell)));
cell
}
}
};
// Traced without the lock: another image's outline need not wait for this one.
let (path, (w, h)) = cell.get_or_init(|| traced_opaque_area(bytes)).clone()?;
let to_image = Affine::scale_non_uniform(f64::from(im.width) / f64::from(w.max(1)), f64::from(im.height) / f64::from(h.max(1)));
Some(path.transformed(im.xf * to_image))
}
/// [`opaque_area`]'s outline of the encoded image `bytes`. An image whose format has no alpha is
/// not decoded.
fn traced_opaque_area(bytes: &[u8]) -> Traced {
#[cfg(test)]
TRACES.with(|t| t.set(t.get() + 1));
let (w, h, alpha) = vectorcraft_trace::decode_alpha(bytes).ok()??;
let outline = vectorcraft_trace::opaque_outline(w, h, alpha).ok()??;
Some((Arc::new(outline), (w, h)))
}
/// 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) that [`reshape`]
/// makes along its frame or, for those whose first effect is Outline Object, along the outline of
/// its opaque area ([`opaque_area`]), 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. The swatches
/// and patterns that live-effect plug-ins painting their pieces name are looked up in `doc`, and
/// the image's pixels in `doc` when the image does not hold them.
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(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 { 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() {
crate::apply_geometry_bez(fx, &g, g.bounding_box(), &ctx.item(item))
} else {
g.clone()
};
out.push((shape, *rule, item.clone()));
}
});
out
}
/// The glyph outlines of type `t` in text space with its underline and strikethrough bars: one
/// path per run, and all of them as one.
pub(crate) fn glyph_outlines(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath) {
let (runs, all, ..) = laid_out(t);
(runs, all)
}
/// [`glyph_outlines`], the bounds of `t`'s layout in text space, and its colour glyphs, which have
/// no outlines, placed in the document with their runs: bitmap glyphs (emoji) as images holding
/// their pixels, then COLR and SVG glyphs as their art, as the renderer paints them.
fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath, Rect, Vec<(usize, Node)>) {
let db = vectorcraft_text::FontDb::global();
let layout = vectorcraft_text::layout(db, t);
let mut runs = vec![kurbo::BezPath::new(); t.runs.len()];
let mut all = kurbo::BezPath::new();
// Ruby (furigana) glyphs are painted with their base text's run, as the renderer paints them.
for g in layout.glyphs.iter().chain(&layout.ruby) {
if let Some(r) = runs.get_mut(g.run) {
r.extend(g.outline.iter());
}
all.extend(g.outline.iter());
}
// Underline and strikethrough bars, painted as their run's type is (#847).
for (run, bar) in vectorcraft_text::decorations(&layout, db, t) {
if let Some(r) = runs.get_mut(run) {
r.extend(bar.iter());
}
all.extend(bar.iter());
}
let images = layout.glyphs.iter().filter_map(|g| Some((g.run, g.placed_image_node(t.xf)?)));
let colours = images.chain(layout.glyphs.iter().filter_map(|g| Some((g.run, g.placed_colour(t.xf)?)))).collect();
(runs, all, layout.bounds, colours)
}
/// Type as glyph outlines in document space: one path per run with the run's fill and stroke,
/// over its colour glyphs (images, art), and (when the object has its own fills/strokes) the whole
/// outline with the object's items below the characters under them and the others over them, as
/// the renderer paints them. Each run's path is placed by the type's transform, which also scales its stroke
/// weights and dashes and transforms its pattern tiles; its unplaced gradients are first fixed to
/// the layout's bounds in text space. The object's own unplaced gradients are fixed to the
/// bounding box of those bounds in the document. The group is marked as type's outlines
/// ([`vectorcraft_doc::NodeExtras::type_outlines`]).
pub fn outline_text(n: &Node) -> Option<Node> {
let NodeKind::Text(t) = &n.kind else { return None };
let (runs, all, bounds, colours) = laid_out(t);
let path = |bp: &kurbo::BezPath| PathData::from_bezpath(bp).transformed(t.xf);
let scaling = vectorcraft_doc::Scaling { strokes: true, patterns: true, ..Default::default() };
let mut children: Vec<Arc<Node>> = vec![];
for (i, (bp, run)) in runs.iter().zip(&t.runs).enumerate() {
// Colour glyphs paint themselves, under their run's fill, as the renderer paints them.
children.extend(colours.iter().filter(|(r, _)| *r == i).map(|(_, art)| Arc::new(with_id(art, n.id))));
if bp.elements().is_empty() {
continue;
}
let mut ap = run.style.appearance();
ap.pin_gradients(bounds);
let mut piece = Node::path(n.id, PathData::from_bezpath(bp), ap);
piece.transform(t.xf, scaling);
children.push(Arc::new(piece));
}
if !n.appearance.items.is_empty() && !all.elements().is_empty() {
let (below, above) = n.appearance.split_contents();
let tb = t.xf.transform_rect_bbox(bounds);
let glyphs = |items: &[AppearanceItem]| {
let mut ap = Appearance { items: items.to_vec(), ..Default::default() };
ap.pin_gradients(tb);
Arc::new(Node::path(n.id, path(&all), ap))
};
if !below.is_empty() {
children.insert(0, glyphs(below));
}
if !above.is_empty() {
children.push(glyphs(above));
}
}
let mut g = Node::group(n.id, children);
g.extras_mut().type_outlines = true;
Some(g)
}
/// `art` with `id` for it and all its members, as the other pieces of outlined type have the type's
/// (Expand Appearance gives them ids of their own).
fn with_id(art: &Node, id: NodeId) -> Node {
let mut art = art.clone();
art.id = id;
for c in art.children_mut().into_iter().flatten() {
*c = Arc::new(with_id(c, id));
}
art
}
/// `n` as plain art that geometry effects can reshape, without its transparency and object
/// effects: type outlined ([`outline_text`]), live objects evaluated (type inside them outlined),
/// a symbol instance as `symbol_art` (the symbol's art, stained as the caller draws it) placed by
/// the instance's transform (through the symbol's 9-slice guides in `doc`, when it has them), as
/// the art its members draw when the instance has geometry effects of its own
/// ([`crate::group::members_as_drawn`]), its fills and strokes carrying the geometry and raster
/// effects of the instance's active fill and stroke ([`add_instance_row_effects`]).
/// `None` for other kinds (images, paths, containers), and for an instance whose art placed
/// 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<Node> {
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. `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(
symbols: Option<&Document>,
paint_doc: Option<&Document>,
n: &Node,
symbol_art: Option<&Node>,
outline: OutlineHook,
nodes: &SymbolNodes,
apart: bool,
) -> Option<Node> {
let mut art = match &n.kind {
NodeKind::Text(_) => outline_text(n)?,
NodeKind::SymbolInstance { symbol, xf } => {
let a = match symbols {
Some(d) => d.place_symbol_art(symbol, symbol_art?, *xf)?,
None => {
let mut a = symbol_art?.clone();
a.transform(*xf, false);
a
}
};
Node::group(n.id, vec![Arc::new(a)])
}
// The object as the one piece the copies of its Transform effect are made from.
_ if has_transform_art(n) || (apart && has_transform_copies(n)) => one_piece(n),
_ if vectorcraft_doc::live::is_live(n) => evaluated_with(symbols, n, outline, nodes),
_ => return None,
};
outline_nested_text(paint_doc, &mut art, false);
if matches!(n.kind, NodeKind::SymbolInstance { .. }) {
// The instance's own geometry effects reshape what the symbol's members draw.
if !geometry_effects(n).is_empty() {
crate::group::members_as_drawn(paint_doc, &mut art);
}
add_instance_row_effects(symbols, n, &mut art);
}
art.opacity = 1.0;
art.blend = Default::default();
art.isolate = false;
// One element: its pieces never knock each other out.
art.knockout = vectorcraft_doc::Knockout::Off;
art.extras_mut().mask = None;
art.appearance.effects.clear();
Some(art)
}
/// `n` with every live object in it evaluated, type inside them outlined ([`outline_text`]). The
/// Repeats among them count the Repeats in the art of `doc`'s symbols that their instances show
/// toward their instance budget ([`vectorcraft_doc::RepeatSpec::expand`]).
pub(crate) fn evaluated(doc: Option<&Document>, n: &Node) -> Node {
evaluated_with(doc, n, &outline_text, &SymbolNodes::default())
}
/// Most objects of symbol art that one compound shape or live Pathfinder operation gives its
/// members in all ([`SymbolNodes`]): four for each segment the operation takes in
/// ([`crate::group::PATHFINDER_MAX_SEGMENTS`]).
pub const SYMBOL_ART_MAX_NODES: u64 = 4 * crate::group::PATHFINDER_MAX_SEGMENTS as u64;
/// The objects of symbol art given to one compound shape or live Pathfinder operation so far,
/// counted against [`SYMBOL_ART_MAX_NODES`]. Instances of symbols that hold several instances of
/// others multiply at each level, which the depth limit does not stop.
#[derive(Debug, Default)]
pub(crate) struct SymbolNodes(std::cell::Cell<u64>);
impl SymbolNodes {
/// Count `art`'s objects: `false` once they would pass [`SYMBOL_ART_MAX_NODES`], and from then
/// on for all art.
pub(crate) fn charge(&self, art: &Node) -> bool {
let total = self.0.get().saturating_add(art.count() as u64);
let fits = total <= SYMBOL_ART_MAX_NODES;
self.0.set(if fits { total } else { u64::MAX });
fits
}
/// Has some symbol art been left out for passing [`SYMBOL_ART_MAX_NODES`]?
pub(crate) fn spent(&self) -> bool {
self.0.get() > SYMBOL_ART_MAX_NODES
}
}
/// [`evaluated`] with type and symbol instances inside live objects given as their art by
/// `outline`, and every symbol instance left in the result (a blend's steps, an instance in a
/// plain group) replaced by its art from `outline`, with the instance's name, visibility and
/// transparency. The live objects and symbol instances inside that art are given as their art in
/// turn, except an instance of a symbol that holds an instance of itself, of a symbol nested
/// more than [`vectorcraft_doc::depth::MAX_DEPTH`] deep, or whose art would take `nodes` past
/// [`SYMBOL_ART_MAX_NODES`], which stays as it is. With [`outline_text`], symbol instances stay as
/// they are.
pub(crate) fn evaluated_with(doc: Option<&Document>, n: &Node, outline: OutlineHook, nodes: &SymbolNodes) -> Node {
resolved(doc, n, outline, nodes, &[])
}
/// `art`, the art `outline` gave an instance of the symbol named `symbol`, with the live objects
/// and symbol instances inside it given as their art as [`evaluated_with`] does.
pub(crate) fn symbol_art_with(art: &Node, outline: OutlineHook, nodes: &SymbolNodes, symbol: &str) -> Node {
resolved(None, art, outline, nodes, &[symbol.to_string()])
}
/// [`evaluated_with`] inside the art of the symbols named in `shown`, outermost first.
fn resolved(doc: Option<&Document>, n: &Node, outline: OutlineHook, nodes: &SymbolNodes, shown: &[String]) -> Node {
let inner = |m: &Node| art_of(doc, m, outline, nodes, shown);
let mut art = vectorcraft_doc::live::expand_deep_hooks(n, Hooks { outline: Some(&inner), symbols: doc, ..Default::default() });
instances_as_art(doc, &mut art, outline, nodes, shown);
art
}
/// `n`'s art from `outline`, inside the art of the symbols named in `shown`. For a symbol instance
/// it is the symbol's art with the live objects and instances inside it given as their art in turn
/// ([`resolved`]); `None` for an instance of a symbol in `shown` (one that holds an instance of
/// itself), for every instance once `shown` holds [`vectorcraft_doc::depth::MAX_DEPTH`] symbols,
/// and for an instance whose symbol's art `nodes` cannot take ([`SymbolNodes::charge`]).
fn art_of(doc: Option<&Document>, n: &Node, outline: OutlineHook, nodes: &SymbolNodes, shown: &[String]) -> Option<Node> {
let NodeKind::SymbolInstance { symbol, .. } = &n.kind else { return outline(n) };
if shown.len() >= vectorcraft_doc::depth::MAX_DEPTH || shown.contains(symbol) {
return None;
}
let art = outline(n).filter(|art| nodes.charge(art))?;
let mut inside = shown.to_vec();
inside.push(symbol.clone());
Some(resolved(doc, &art, outline, nodes, &inside))
}
/// Replace every symbol instance under `n` for which [`art_of`] gives art by that art. `shown`
/// names the symbols whose art `n` is in, outermost first.
fn instances_as_art(doc: Option<&Document>, n: &mut Node, outline: OutlineHook, nodes: &SymbolNodes, shown: &[String]) {
let Some(ch) = n.children_mut() else { return };
for c in ch.iter_mut() {
if matches!(c.kind, NodeKind::SymbolInstance { .. }) {
if let Some(mut o) = art_of(doc, c, outline, nodes, shown) {
o.name = c.name.clone();
(o.visible, o.opacity, o.blend, o.isolate) = (c.visible, c.opacity, c.blend, c.isolate);
*c = Arc::new(o);
}
} else if c.children().is_some() {
instances_as_art(doc, Arc::make_mut(c), outline, nodes, shown);
}
}
}
/// Type `t` in a clip group's clipping path as its outlines ([`outline_text`]) with its
/// visibility, or with geometry effects as a path of the region they give it, which is what
/// [`crate::clip_outline`] clips by.
pub(crate) fn clipping_text_outline(t: &Node) -> Option<Node> {
if let Some(p) = crate::clip::with_clip_effects_applied(t) {
return Some(p);
}
let o = outline_text(t)?;
Some(Node { visible: t.visible, ..o })
}
/// Replace type anywhere inside `n` by its outlines, as art that keeps the type's look
/// ([`as_art`]). Type in a clip group's clipping path (all of `n` when `clipping` is set) becomes
/// its outlines with its visibility and effects ([`clipping_text_outline`]). The swatches and
/// patterns that live-effect plug-ins painting their pieces name are looked up in `doc`.
pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping: bool) {
let clips = n.clips();
let Some(ch) = n.children_mut() else { return };
for (i, c) in ch.iter_mut().enumerate() {
let clipping = clipping || (clips && i == 0);
let art = match &c.kind {
NodeKind::Text(_) if clipping => clipping_text_outline(c),
NodeKind::Text(_) => as_art(doc, c, None),
_ => None,
};
if let Some(o) = art {
*c = Arc::new(o);
} else if c.children().is_some() {
outline_nested_text(doc, Arc::make_mut(c), clipping);
}
}
}
/// `n` (see [`needs_outline`]) with its object-level geometry effects and its perspective applied:
/// its [`outline_art`] reshaped as one piece, or for an image the image clipped by its reshaped
/// frame. Type's own fills and strokes keep their effects on its whole outline. An image's own
/// fills and strokes that paint a shape ([`image_rows`]) become paths below and above it, along
/// its frame or, from Outline Object, along the outline of its opaque area ([`opaque_area`]),
/// reshaped with it, and keep their effects from their first Convert to Shape or Outline Object
/// on. The result is a group with `n`'s id, name, visibility, transparency, opacity mask
/// and remaining (raster) effects; `None` when `n` has no visible geometry effect, no perspective
/// and is not type or an image with a fill or stroke of its own that [`has_item_geometry`] counts
/// or a symbol instance whose active fill or stroke has a raster effect
/// ([`has_instance_row_raster`]), or has no art (a symbol instance without `symbol_art`). A path
/// with [`has_transform_art`] is always reshaped. The fills and strokes in the art whose own
/// Transform effects reach their appearance become objects of their own ([`transform_items`]).
pub fn reshape(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
let own = !geometry_effects(n).is_empty() || n.projection().is_some() || has_item_geometry(n) || has_instance_row_raster(n);
if !has_transform_art(n) && (!own || !needs_outline(n)) {
return None;
}
as_art(doc, n, symbol_art)
}
/// `n` (see [`needs_outline`], or a path with [`has_transform_art`]) as plain art for Expand
/// Appearance: [`reshape`]d when it has
/// geometry effects, else type with fills or strokes of its own outlined, an image with fills or
/// strokes of its own that paint a shape, or a symbol instance's art reshaped by the geometry
/// effects of its active fill or stroke ([`has_item_geometry`]), in the same form. `None` when it
/// has none of these.
pub fn expand_outlined(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
// A path whose Transform effect copies it, or scales its strokes or moves its patterns: each
// copy an object of its own with the path's fills and strokes, the copies of each Transform
// 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, doc, n, symbol_art, &outline_text, &SymbolNodes::default(), true);
}
let own_paint = match n.kind {
NodeKind::Text(_) => !n.appearance.items.is_empty(),
_ => has_item_geometry(n),
};
if !needs_outline(n) || (geometry_effects(n).is_empty() && !own_paint) {
return None;
}
as_art(doc, n, symbol_art)
}
/// [`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<Node> {
as_art_with(doc, doc, n, symbol_art, &outline_text, &SymbolNodes::default())
}
/// [`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<Node> {
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(
symbols: Option<&Document>,
paint_doc: Option<&Document>,
n: &Node,
symbol_art: Option<&Node>,
outline: OutlineHook,
nodes: &SymbolNodes,
apart: bool,
) -> Option<Node> {
let fx = geometry_effects(n);
let mut art = match &n.kind {
NodeKind::Image(im) => {
let frame = shapes::rectangle(Rect::new(0.0, 0.0, im.width as f64, im.height as f64)).transformed(im.xf);
let mut image = Node::new(n.id, n.kind.clone());
// Its pixels draw as the image's do: colorized by a grayscale image's fill color,
// smoothed or not, and with its color mask's masked pixels.
{
let mut e = image.extras_mut();
e.colorize = n.extras().colorize.clone();
e.pixelated = n.extras().pixelated;
e.color_mask = n.extras().color_mask.clone();
}
let clipped = || {
let mut clip = Node::path(n.id, frame.clone(), Appearance::default());
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
*clipping = true;
}
Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image.clone())], clip: true })
};
// The fills and strokes that paint a shape, as paths below and above the image: along
// the frame, or for those whose first effect is Outline Object along the outline of
// the image's opaque area (traced only when one of them needs it). A run of either
// kind is one path; a fill or stroke whose outline is empty (an image with no opaque
// pixel) paints nothing.
let (below, above) = n.appearance.split_contents();
let (below, above) = (image_rows(below), image_rows(above));
if below.is_empty() && above.is_empty() {
clipped()
} else {
let opaque = std::cell::OnceCell::new();
let rows = |items: Vec<AppearanceItem>| {
let mut out = vec![];
let mut items = items.into_iter().peekable();
while let Some(first) = items.next() {
let follows = follows_opaque_area(&first);
let mut run = vec![first];
while let Some(next) = items.next_if(|i| follows_opaque_area(i) == follows) {
run.push(next);
}
let path = if follows { opaque.get_or_init(|| opaque_area(&[symbols, paint_doc], im)).as_ref() } else { None };
let path = path.unwrap_or(&frame);
if !path.is_empty() {
out.push(Arc::new(Node::path(n.id, path.clone(), Appearance { items: run, ..Default::default() })));
}
}
out
};
// Without geometry effects, the image is not clipped.
let image = if fx.is_empty() { image.clone() } else { clipped() };
Node::group(n.id, rows(below).into_iter().chain([Arc::new(image)]).chain(rows(above)).collect())
}
}
_ => outline_art_with(symbols, paint_doc, n, symbol_art, outline, nodes, apart)?,
};
reshape_leaves_apart(&mut art, &fx, &n.appearance.effects, Budget::EFFECTS, apart, paint_doc);
if apart {
hoist_copies(&mut art);
}
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, paint_doc);
}
// The art is a group with `n`'s id (for an image without fills or strokes that paint, a clip
// group).
Some(dressed(art, n))
}
/// `art`, the art made of `n`, with `n`'s name, visibility, transparency, opacity mask and
/// remaining (raster) effects.
pub(crate) fn dressed(mut art: Node, n: &Node) -> Node {
art.name = n.name.clone();
art.visible = n.visible;
art.opacity = n.opacity;
art.blend = n.blend;
art.isolate = n.isolate;
// One element: its pieces never knock each other out.
art.knockout = vectorcraft_doc::Knockout::Off;
art.knockout_shape = n.knockout_shape;
art.extras_mut().mask = n.extras().mask.clone();
art.appearance.effects = crate::raster::without_geometry(&n.appearance.effects);
art
}
/// A group with `n`'s id holding `n` without its transparency, opacity mask and effects: the one
/// piece that `n`'s own geometry effects reshape.
fn one_piece(n: &Node) -> Node {
let mut one = n.clone();
one.opacity = 1.0;
one.blend = Default::default();
one.isolate = false;
one.extras_mut().mask = None;
one.appearance.effects.clear();
Node::group(n.id, vec![Arc::new(one)])
}
/// Path or compound path `n` with its own geometry effects applied, as the art it draws: with
/// geometry effects in a visible fill or stroke of its own, a group of one path for each fill and
/// stroke, its effects and that fill's or stroke's applied ([`crate::bake_type_appearance`]);
/// otherwise a group with `n`'s id holding the path reshaped by its effects ([`reshape_leaves`]).
/// The art keeps `n`'s id, name, visibility, transparency, opacity mask and raster effects. `None`
/// for a guide, for other kinds of objects, for a path with a linked opacity mask or a 3D effect,
/// and when `n` has no visible geometry effect of its own or in a visible fill or stroke.
pub(crate) fn own_effects_art(doc: Option<&Document>, n: &Node) -> Option<Node> {
if !matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. })
|| n.extras().mask.as_ref().is_some_and(|m| m.linked)
|| crate::has_solid(n)
|| crate::has_revolve(n)
{
return None;
}
if n.appearance.items.iter().any(|i| crate::paints(i) && crate::has_geometry(i.effects())) {
// One element: its pieces never knock each other out.
return crate::bake_type_appearance(doc, n).map(|art| Node { knockout: vectorcraft_doc::Knockout::Off, ..art });
}
let fx = geometry_effects(n);
if fx.is_empty() {
return None;
}
let mut art = one_piece(n);
let carry = reshape_leaves_carrying(&mut art, &fx, &n.appearance.effects, doc);
transform_items(&mut art, doc);
let mut art = dressed(art, n);
// The raster effects before a Transform effect the path did not take stay as the path does.
if carry.skipped() {
let mut ran = n.appearance.effects.clone();
carry.hide_skipped(&mut ran);
art.appearance.effects = crate::raster::without_geometry(&ran);
}
Some(art)
}
/// Map every path under `art` (and its gradients), and every path in the art of the linked opacity
/// masks of the objects under it, through the projective map `h`, curves split finely enough to
/// follow it. The type and live objects in the art of those masks are outlined and evaluated
/// first ([`outlined_members`], with the symbols of `doc`).
fn project_leaves(art: &mut Node, h: &vectorcraft_geom::Homography, doc: Option<&Document>) {
project_paths(art, h);
each_linked_mask_mut(art, false, 0, &mut |m| {
*m = outlined_mask_art(doc, m);
project_paths(m, h);
});
}
/// `art`, the art of an opacity mask, with type and live objects in it outlined and evaluated
/// ([`outlined_members`]), and as art that keeps its look ([`as_art`]) when it is type itself.
fn outlined_mask_art(doc: Option<&Document>, art: &Node) -> Node {
match art.kind {
NodeKind::Text(_) => as_art(doc, art, None).unwrap_or_else(|| art.clone()),
_ => outlined_members(doc, art),
}
}
/// The paths of [`project_leaves`] outside the art of masks.
fn project_paths(art: &mut Node, h: &vectorcraft_geom::Homography) {
let f = |p: vectorcraft_geom::Point| h.apply(p).unwrap_or(p);
let piece = art.geometric_bounds().map_or(1.0, |b| (b.width().max(b.height()) / 64.0).max(0.25));
let project = |path: &mut PathData| *path = vectorcraft_doc::live::map_nonlinear(path, piece, f);
for_each_leaf(art, &mut |leaf| {
leaf.pin_gradients();
leaf.appearance.warp_gradients(&|p| h.affine_at(p).unwrap_or(vectorcraft_geom::Affine::IDENTITY));
match &mut leaf.kind {
NodeKind::Path { path, live, .. } => {
project(path);
*live = None;
}
NodeKind::Compound { children, .. } => {
for c in children.iter_mut() {
if let NodeKind::Path { path, live, .. } = &mut Arc::make_mut(c).kind {
project(path);
*live = None;
}
}
}
_ => {}
}
});
}
/// The visible geometry effects of `n`'s own effect list, in order.
pub(crate) fn geometry_effects(n: &Node) -> Vec<&Effect> {
n.appearance.effects.iter().filter(|e| e.visible && is_geometry(&e.id)).collect()
}
/// Apply `fx` to every path under `art` as one piece: each effect takes the whole art's bounds as
/// its reference box and the art's seed (a random Transform moves the pieces together). Images and
/// symbol instances follow a Transform effect the same way ([`transform_placed`]), and images
/// follow a Warp effect as a raster mesh warp ([`cut_warped_images`]); other geometry effects leave them
/// as they are. The paths keep to one [`Budget::EFFECTS`] together (or to what they cost, when that is more): a path
/// whose effect would pass what the others leave of it keeps its shape. The swatches and patterns
/// that live-effect plug-ins painting their pieces name are looked up in `doc`, and a deferred one
/// is listed by its index in `owner`, the effect list `fx` was taken from ([`reshape_leaves_apart`]).
pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect], owner: &[Effect], doc: Option<&Document>) {
reshape_leaves_apart(art, fx, owner, Budget::EFFECTS, false, doc);
}
/// [`reshape_leaves`], and how gradients that paint the art as a whole (a group's own fills and
/// strokes) follow the effects ([`crate::Carry`]).
pub(crate) fn reshape_leaves_carrying(art: &mut Node, fx: &[&Effect], owner: &[Effect], doc: Option<&Document>) -> crate::Carry {
reshape_leaves_apart(art, fx, owner, Budget::EFFECTS, false, doc)
}
/// [`reshape_leaves_carrying`] whose paths keep to `budget` together, or to what they cost.
#[cfg(test)]
pub(crate) fn reshape_leaves_within(art: &mut Node, fx: &[&Effect], budget: Budget, doc: Option<&Document>) -> crate::Carry {
reshape_leaves_apart(art, fx, &[], budget, false, doc)
}
/// `art`, the group of the one piece a path's copies are made from ([`outline_art_with`]), holding
/// the group of its copies ([`copy_leaf`]) as its only member: the copies become its members, so
/// the expanded path is one group of its copies rather than a group in a group.
fn hoist_copies(art: &mut Node) {
let id = art.id;
let Some(children) = art.children_mut() else { return };
let [only] = children.as_slice() else { return };
if only.id != id || !matches!(only.kind, NodeKind::Group { clip: false, .. }) {
return;
}
let copies = only.children().cloned().unwrap_or_default();
if !copies.is_empty() {
*children = copies;
}
}
/// [`reshape_leaves_carrying`] whose paths keep to `budget` together, or to what they cost; with
/// `apart`, a Transform effect's copies of each path are objects
/// of their own ([`copy_leaf`]) whatever paints them, as Expand Appearance makes them. A live-effect
/// plug-in that paints what it returns makes each path a group of its pieces ([`paint_leaf`]),
/// whose swatches and patterns must be in `doc` (without one, pieces that name one are refused).
/// Returns how gradients that paint the art as a whole follow the effects, and the Transform
/// effects that nothing in the art took past the budget ([`crate::Carry::skipped`]). A deferred live-effect
/// plug-in is listed by its index in `owner`, the effect list `fx` was taken from, or in `fx` when
/// `owner` does not hold it. The art of the linked opacity masks of the objects in `art` (not of
/// `art` itself) takes each effect as the paths it masks do ([`Step::reshape_masks`]), with the
/// type and live objects in it outlined and evaluated first ([`outlined_members`]). When a mask
/// stays as it was, or a Transform effect makes fewer copies than it asks for, art with linked
/// masks keeps its shape under that effect.
fn reshape_leaves_apart(art: &mut Node, fx: &[&Effect], owner: &[Effect], budget: Budget, apart: bool, doc: Option<&Document>) -> crate::Carry {
let mut whole = crate::Carry::default();
if fx.is_empty() {
return whole;
}
let masked = has_linked_mask(art, false);
if masked {
each_linked_mask_mut(art, false, 0, &mut |m| *m = outlined_mask_art(doc, m));
}
let mut masks_safe = true;
each_linked_mask(art, false, 0, &mut |_, m| masks_safe &= leaves_safe(m));
let art_stroke = art.appearance.stroke().is_some_and(|s| !s.paint.is_none() && s.width > 0.0);
let style_safe = leaves_safe(art) && masks_safe && !art_stroke;
let listed = fx;
let opt_fx = crate::geometry::optimize_geometry_effects(fx, style_safe);
let fx = opt_fx.as_slice();
let seed = art.id.0;
let mut used = art_cost(art);
each_linked_mask(art, false, 0, &mut |_, m| used = used.plus(art_cost(m)));
let max = budget.at_least(used);
// Read before the effects run: a path's own copies don't make it art of several objects. The
// same holds for the art of each mask, listed by the object it masks.
let copy_whole = copies_whole(art);
let mut wholes = HashMap::new();
each_linked_mask(art, false, 0, &mut |id, m| {
wholes.entry(id).or_insert_with(|| copies_whole(m));
});
let mut warped = WarpedImages::new();
// The raster effects of what the art holds follow each Transform effect that moves it once,
// whatever its options, unless one of the effects makes copies or moves at random
// ([`crate::raster::contents_step`], [`follow_art`]).
let held = listed.iter().any(|e| crate::raster::holds_back(e));
for e in fx {
let Some(bounds) = warped.bounds(art) else { break };
let params = merged_params(&e.id, &e.params);
// Where a live-effect plug-in sits, which it is listed by when it is deferred.
let effect = owner.iter().position(|x| std::ptr::eq(x, *e)).or_else(|| listed.iter().position(|x| std::ptr::eq(*x, *e)));
let step = Step { e, params: &params, bounds, seed, effect, apart, doc, wholes: &wholes, held };
// Art with linked masks keeps its shape under an effect that a mask in it could not take
// as the art it masks did, so no mask is left apart from its art.
let before = masked.then(|| (art.clone(), used));
let ran = step.run(art, max, &mut used, copy_whole, Some(&mut warped), false, 0);
if (ran.masks || ran.short)
&& let Some((was, cost)) = before
{
*art = was;
used = cost;
continue;
}
// A Transform effect that moves the art once and that nothing in it took (past the budget)
// is left out ([`crate::Carry::hide_skipped`]), listed by its index in `owner`.
if ran.kept
&& !ran.took
&& e.id == "distort.transform"
&& !crate::raster::holds_back(e)
&& let Some(i) = owner.iter().position(|x| std::ptr::eq(x, *e))
{
whole.skip(i);
}
if let Some(carry) = ran.whole {
whole.push(carry);
}
}
cut_warped_images(art, &warped);
whole
}
/// One geometry effect of an object run over its art ([`reshape_leaves_apart`]): the effect, its
/// merged params, the reference box and seed of the object's art, where a deferred live-effect
/// plug-in is listed, whether each Transform copy becomes an object, the document that the
/// swatches and patterns of painted pieces are looked up in, [`copies_whole`] of the art of
/// each linked mask before the first effect, by the id of the object it masks, and whether one of
/// the object's effects holds the raster effects in its art back from its Transform effects
/// ([`follow_art`]).
struct Step<'a> {
e: &'a Effect,
params: &'a serde_json::Value,
bounds: Rect,
seed: u64,
effect: Option<usize>,
apart: bool,
doc: Option<&'a Document>,
wholes: &'a HashMap<NodeId, bool>,
held: bool,
}
/// What [`Step::run`] did to a piece of art.
struct Ran {
/// A path in the art kept its shape (past the budget).
kept: bool,
/// A path, image or symbol instance in the art took the effect.
took: bool,
/// The linked opacity mask of an object in the art stayed as it was while the paths it masks
/// took the effect, or could have ([`Step::reshape_masks`]).
masks: bool,
/// A Transform effect made fewer copies of a path, image or symbol instance than it asks for
/// (past the budget or out of reach), where a linked mask could take a different number.
short: bool,
/// How what paints the art as a whole follows the effect, when it does.
whole: Option<crate::Carry>,
}
impl Ran {
/// Nothing in the art kept its shape or made fewer copies, and every linked mask in it moved.
fn complete(&self) -> bool {
!self.kept && !self.masks && !self.short
}
}
impl Step<'_> {
/// Run the effect over `art`, whose paths and those of the linked opacity masks in it keep to
/// `max` together with what they cost so far (`used`). `copy_whole` is [`copies_whole`] of the
/// art, or of a mask's art, before the first effect. `warped` lists the images Warp effects
/// bend in the art ([`WarpedImages`]); a mask's art has none. With `own`, `art` is the art of a
/// mask, and its own linked mask takes the effect too. `depth` counts the masks `art` is in. A
/// live-effect plug-in that paints its pieces leaves a mask's art, and art that holds objects
/// with linked masks, as they are and reports the masks as stayed: a mask can't take the
/// pieces as the art it masks does. So does a Warp effect that bends an image with a linked
/// mask.
#[allow(clippy::too_many_arguments)]
fn run(
&self,
art: &mut Node,
max: Budget,
used: &mut Budget,
copy_whole: bool,
mut warped: Option<&mut WarpedImages>,
own: bool,
depth: usize,
) -> Ran {
let (e, params, bounds, seed) = (self.e, self.params, self.bounds, self.seed);
let stayed = Ran { kept: false, took: false, masks: true, short: false, whole: None };
if let Some(plugin) = painted_plugin(e) {
if own || has_linked_mask(art, false) {
return stayed;
}
let at = self.effect.map(|effect| vectorcraft_plugins::deferred::Stage { object: seed, effect, item: None });
for_each_leaf(art, &mut |leaf| {
let Some((path, _)) = leaf_geometry(leaf) else { return };
let before = Budget::of(&path);
let room = max.minus(used.minus(before));
if let Some(made) = paint_leaf(leaf, plugin, params, bounds, room, self.doc, at) {
*used = used.minus(before).plus(made);
}
});
return Ran { kept: false, took: true, masks: false, short: false, whole: None };
}
// A Warp effect that bends images ([`mark_warped_images`]) bends an image's pixels, not the
// art of its linked mask.
let warp = e.id.strip_prefix("warp.").and_then(crate::warp::WarpStyle::from_id).filter(|_| crate::warp::bends(&e.id, params));
if warp.is_some() && has_masked_image(art) {
return stayed;
}
// Placed gradients follow a Transform effect, Free Distort and the Warp effects
// ([`crate::Carry`]): under Free Distort and Warp (`boxes`) each path's move and scale from
// its bounds before the effect to its bounds after it. Under those and the other effects,
// the placed gradients of type's outlines in `art` (`outline_text`) move and scale with
// the outlines' bounds.
let follows = crate::Carry::follows(&e.id, params, bounds, seed);
let boxes = follows && crate::Carry::by_bounds(&e.id);
let mut outlines = vec![];
if !follows || boxes {
type_outline_bounds(art, &mut outlines);
}
let mut leaves = vec![];
if boxes {
leaf_bounds(art, &mut leaves);
}
let mut groups = vec![];
if follows {
group_marks(art, &mut vec![], &HashSet::new(), None, &mut groups);
// The images earlier Warps bend are measured by their pieces only when a group's
// gradient needs the bounds.
if !groups.is_empty()
&& let Some(images) = warped.as_deref().and_then(|w| w.when_cut(art))
{
groups.clear();
group_marks(art, &mut vec![], &HashSet::new(), Some(images), &mut groups);
}
}
// The ids of the paths that kept their shape (past the budget), and their places in the
// order [`for_each_leaf`] visits the paths; and the ids of the paths whose linked masks
// moved with them as they were transformed as objects.
let mut kept_ids = HashSet::new();
let mut kept_leaves = HashSet::new();
let mut leaf_at = 0usize;
let mut moved_ids = HashSet::new();
let transform = (e.id == "distort.transform").then(|| {
let (m, copies) = crate::distort::transform_map(bounds, params, seed);
let options = crate::distort::TransformOptions::of(params);
(m, copies, options, crate::distort::transform_affine(bounds, params, seed))
});
// What the effect does to the raster effects in the art, when they follow it.
let raster_map = if self.held { None } else { crate::raster::contents_step(std::slice::from_ref(e)) };
// In a mask's art, images and symbol instances take only a Transform effect
// ([`transform_placed`]): a mask's art that holds one can't take another effect as the art
// it masks does.
if own && transform.is_none() && (is_placed(art) || placed_count(art) > 0) {
return Ran { kept: false, took: false, masks: true, short: false, whole: None };
}
// Art of several objects is copied whole, so the copies don't interleave member by member.
// The copies of its members carry their linked masks, moved with them.
if let Some((m, copies, options, _)) = transform
&& copies > 0
&& copy_whole
{
if !own {
*used = copy_art(art, m, copies, copy_scaling(options), max);
return Ran { kept: false, took: true, masks: false, short: false, whole: None };
}
// A mask's art: its copies keep to what the rest of the art leaves of `max`, and the
// linked mask of the art itself takes them too. That mask's art stays in `used`, as
// [`copy_art`] counts only the members.
let before = copy_cost(art);
let rest = used.minus(before).plus(art.extras().mask.as_deref().map_or(Budget { anchors: 0, lines: 0 }, |k| copy_cost(&k.art)));
let made = copy_art(art, m, copies, copy_scaling(options), max.minus(rest));
*used = rest.plus(made);
let short = art.children().is_some_and(|c| c.iter().filter(|c| !c.is_guide()).count() <= copies);
let none = HashSet::new();
let masks = self.reshape_own_mask(art, &Skip { moved: &none, kept: &none }, max, used, depth);
return Ran { kept: false, took: true, masks, short, whole: None };
}
// A shortfall in the copies matters for the objects a linked mask above them covers, which
// the mask would take a different number of copies than, and anywhere in a mask's art.
let watch = transform.is_some_and(|(_, copies, options, _)| copies > 0 && options.objects) && (own || has_linked_mask(art, false));
let mut covered = HashSet::new();
if watch && !own {
masked_ids(art, false, &mut covered);
}
let counts = |id: NodeId| watch && (own || covered.contains(&id));
let mut short = false;
let mut took = false;
for_each_leaf(art, &mut |leaf| {
let at = leaf_at;
leaf_at += 1;
let Some((path, rule)) = leaf_geometry(leaf) else { return };
// What the other paths leave of the budget.
let before = Budget::of(&path);
let room = max.minus(used.minus(before));
if let Some((m, copies, options, map)) = transform {
// Transform Patterns and Scale Strokes & Effects change each copy's fills and
// strokes; a gradient paints each copy by itself, not the one piece the copies
// make together; and Expand Appearance (`apart`) makes each copy an object.
// Fills and strokes that stack paint each copy below the one before (#5920, #6316).
// Each copy of a path with a linked mask carries the mask, moved with it.
let separate = copies > 0
&& (self.apart
|| leaf.appearance.has_unplaced_gradient()
|| leaf.appearance.has_placed_gradient()
|| paints_copies_apart(&leaf.appearance, rule)
|| leaf.extras().mask.as_ref().is_some_and(|m| m.linked));
// The copies of a path replace it by a group of them, the path included.
let copied = |leaf: &Node| leaf.children().map_or(0, Vec::len);
if options.reach_appearance() {
let (made, moved) = transform_leaf(leaf, &path, map, copies, options, room, raster_map.is_some(), None);
*used = used.minus(before).plus(made);
took |= moved;
if moved {
// With Transform Objects off the path stays, and its mask takes the
// effect's stroke and pattern changes as its own art.
if options.objects {
moved_ids.insert(leaf.id);
}
short |= counts(leaf.id) && copied(leaf) <= copies;
} else {
kept_ids.insert(leaf.id);
}
return;
}
if separate {
let scaling = vectorcraft_doc::Scaling { keep_fit_when_flat: true, ..Default::default() };
let made = copy_leaf(leaf, &path, m, copies, scaling, room, None);
*used = used.minus(before).plus(made);
took = true;
moved_ids.insert(leaf.id);
short |= counts(leaf.id) && copied(leaf) <= copies;
return;
}
}
let stage = self.effect.map(|effect| vectorcraft_plugins::deferred::Stage { object: seed, effect, item: None });
let Some(out) = apply_one(&e.id, params, &path, bounds, &GeomContext { seed, stage, ..GeomContext::of(leaf) }, room) else {
kept_ids.insert(leaf.id);
kept_leaves.insert(at);
return;
};
took = true;
// A Transform effect's copies of a path are subpaths of it, each with the path's anchors.
if counts(leaf.id)
&& let Some((_, copies, _, _)) = transform
{
short |= out.anchor_count() < path.anchor_count().saturating_mul(copies.saturating_add(1));
}
*used = used.minus(before).plus(Budget::of(&out));
if follows && !boxes {
// A gradient fitted to the path fits the path's own bounds.
let carry = crate::Carry::of(&e.id, params, bounds, path.bounds(), None, seed);
if carry.moves(&leaf.appearance) {
carry.apply(&mut leaf.appearance);
}
}
set_geometry(leaf, out, rule);
});
if boxes {
rebase_leaves(art, &mut leaves.into_iter());
}
// Warp bends images too: each one's pixels follow the warp's map over the same box. The
// images are cut into pieces once, after the last effect ([`cut_warped_images`]). An image
// whose clipping frame kept its shape (past the budget) keeps its pixels as they are.
if let Some(style) = warp
&& let Some(warped) = warped.as_deref_mut()
{
let f: WarpMap = Rc::from(crate::warp::warp_map(bounds, style, params));
warped.reserve(art);
mark_warped_images(art, &f, &kept_leaves, false, &mut 0, warped);
}
if let Some((m, copies, options, _)) = transform
&& options.objects
{
// A mask's art that is one image or symbol instance is placed as the only member of a
// group of it.
let root = is_placed(art);
if root {
let id = art.id;
let one = std::mem::replace(art, Node::group(id, vec![]));
*art = Node::group(id, vec![Arc::new(one)]);
}
let placed = if watch { masked_placed(art, own) } else { 0 };
// Images and symbol instances take the effect whatever the budget.
took = took || placed_count(art) > 0;
// Scale Strokes & Effects leaves the raster effects that follow the effect to
// [`follow_art`].
let mut scaling = copy_scaling(options);
if raster_map.is_some() {
scaling.effects = options.strokes.then_some(scale_unfollowed as fn(&mut Effect, f64));
}
transform_placed(art, m, copies, scaling);
short |= watch && masked_placed(art, own) < placed.saturating_mul(copies.saturating_add(1));
if root && let Some([one]) = art.children().map(Vec::as_slice) {
*art = (**one).clone();
}
}
// A mask's art takes them as a group inside the art: its own raster effects follow too.
if let Some(a) = raster_map {
follow_art(art, a, &kept_ids, Some(&moved_ids), own.then_some(Affine::IDENTITY), depth);
}
if outlines.iter().any(Option::is_some) {
rebase_type_outlines(art, &mut outlines.into_iter());
}
// Under Transform copies, a mask takes the copies as the paths it masks do; the copies of a
// path with a mask of its own each carry it ([`copy_leaf`]).
let masks = self.reshape_masks(art, own, &Skip { moved: &moved_ids, kept: &kept_ids }, max, used, depth);
// What paints the art as a whole, and the own fills and strokes of the groups inside it,
// follow their bounds under Free Distort and Warp (they stay when no path changed them).
// Each stays when a path under it that its bounds measure kept its shape (past the budget).
let mut after = vec![];
let images = if groups.is_empty() { None } else { warped.as_deref().and_then(|w| w.when_cut(art)) };
let held = (!groups.is_empty() || !kept_ids.is_empty()) && group_marks(art, &mut vec![], &kept_ids, images, &mut after).1;
if !groups.is_empty() {
carry_groups(art, groups, after, &|from, to| crate::Carry::of(&e.id, params, bounds, from, to, seed));
}
let drawn = warped.as_deref().map_or_else(|| art.geometric_bounds(), |w| w.bounds(art));
let whole = (follows && !held).then(|| crate::Carry::of(&e.id, params, bounds, Some(bounds), drawn, seed));
Ran { kept: !kept_ids.is_empty(), took, masks, short, whole }
}
/// Run the effect over the art of the linked opacity mask of `n` (with `own`) and of each path,
/// compound path, group and layer under it, with the art's reference box, as the paths the mask
/// covers take it: the mask moves and bends with what it masks. The mask of an object
/// transformed as an object moved with it already and is left as it is. A mask stays as it was
/// past [`vectorcraft_doc::depth::MAX_DEPTH`] masks inside masks, and for the reasons
/// [`Step::reshape_own_mask`] gives. Returns whether a mask stayed for one of those reasons.
fn reshape_masks(&self, n: &mut Node, own: bool, skip: &Skip, max: Budget, used: &mut Budget, depth: usize) -> bool {
if depth >= vectorcraft_doc::depth::MAX_DEPTH || !has_linked_mask(n, own) {
return false;
}
let mut stayed = own && self.reshape_own_mask(n, skip, max, used, depth);
if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } = &mut n.kind {
for c in children.iter_mut() {
if has_linked_mask(c, true) {
stayed |= self.reshape_masks(Arc::make_mut(c), true, skip, max, used, depth);
}
}
}
stayed
}
/// Run the effect over the art of the linked opacity mask of `n` itself, when `n` is a path,
/// compound path, group or layer that `skip` does not list as moved. The mask stays as it was
/// when a path it masks kept its shape, and when its own art could not take the effect whole: a
/// path in it would keep its shape, its copies would stop short, or a mask inside it would stay
/// (no mask is left half moved). Returns whether the mask stayed for one of those reasons.
fn reshape_own_mask(&self, n: &mut Node, skip: &Skip, max: Budget, used: &mut Budget, depth: usize) -> bool {
if depth >= vectorcraft_doc::depth::MAX_DEPTH || !reshapes_mask_of(n) || skip.moved.contains(&n.id) {
return false;
}
let stays = holds_any(n, skip.kept);
let id = n.id;
let Some(m) = n.extras_if_any().and_then(|x| x.mask.as_mut()).filter(|m| m.linked) else { return false };
let before = *used;
let mut art = (*m.art).clone();
let whole = self.wholes.get(&id).copied().unwrap_or_else(|| copies_whole(&art));
let ran = (!stays).then(|| self.run(&mut art, max, used, whole, None, true, depth + 1));
match ran {
Some(ran) if ran.complete() => {
// What paints the mask's art as a whole follows the effect as the art does.
if let Some(carry) = ran.whole
&& carry.moves(&art.appearance)
{
carry.apply(&mut art.appearance);
}
m.art = Arc::new(art);
false
}
_ => {
*used = before;
true
}
}
}
}
/// The objects whose linked masks [`Step::reshape_masks`] leaves as they are: those transformed
/// as objects (`moved`), and those holding a path that kept its shape (`kept`).
struct Skip<'a> {
moved: &'a HashSet<NodeId>,
kept: &'a HashSet<NodeId>,
}
/// Is `n` an object whose linked opacity mask a geometry effect on the art it is in reshapes with
/// it: a path, compound path, group or layer? An image or symbol instance moves its mask only
/// with the map a Transform effect places it by ([`transform_placed`]).
fn reshapes_mask_of(n: &Node) -> bool {
matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } | NodeKind::Group { .. } | NodeKind::Layer { .. })
}
/// Does `n` (with `own`) or a path, compound path, group or layer under it carry a linked opacity
/// mask that [`Step::reshape_masks`] reshapes?
fn has_linked_mask(n: &Node, own: bool) -> bool {
let mine = own && reshapes_mask_of(n) && n.extras().mask.as_ref().is_some_and(|m| m.linked);
mine || match &n.kind {
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => children.iter().any(|c| has_linked_mask(c, true)),
_ => false,
}
}
/// Run `f` on the id of `n` (with `own`) and of each path, compound path, group and layer under it
/// that carries a linked opacity mask, with the art of that mask, and on the masks inside that
/// art, as [`Step::reshape_masks`] reaches them: `depth` masks deep already, up to
/// [`vectorcraft_doc::depth::MAX_DEPTH`].
fn each_linked_mask(n: &Node, own: bool, depth: usize, f: &mut dyn FnMut(NodeId, &Node)) {
if depth >= vectorcraft_doc::depth::MAX_DEPTH {
return;
}
if own
&& reshapes_mask_of(n)
&& let Some(m) = n.extras().mask.as_deref().filter(|m| m.linked)
{
f(n.id, &m.art);
each_linked_mask(&m.art, true, depth + 1, f);
}
if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } = &n.kind {
for c in children {
each_linked_mask(c, true, depth, f);
}
}
}
/// [`each_linked_mask`] that changes the art of each mask: `f` runs on a mask's art before the
/// masks inside it are reached.
fn each_linked_mask_mut(n: &mut Node, own: bool, depth: usize, f: &mut dyn FnMut(&mut Node)) {
if depth >= vectorcraft_doc::depth::MAX_DEPTH || !has_linked_mask(n, own) {
return;
}
if own
&& reshapes_mask_of(n)
&& let Some(m) = n.extras_if_any().and_then(|x| x.mask.as_mut()).filter(|m| m.linked)
{
let art = Arc::make_mut(&mut m.art);
f(art);
each_linked_mask_mut(art, true, depth + 1, f);
}
if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } = &mut n.kind {
for c in children.iter_mut() {
if has_linked_mask(c, true) {
each_linked_mask_mut(Arc::make_mut(c), true, depth, f);
}
}
}
}
/// Does [`Step::reshape_masks`] reshape the linked opacity mask of `n` itself?
fn has_own_linked_mask(n: &Node) -> bool {
reshapes_mask_of(n) && n.extras().mask.as_ref().is_some_and(|m| m.linked)
}
/// Add to `out` the ids of the objects under `n` that the linked opacity mask of an object above
/// them covers (every one of them with `under`).
fn masked_ids(n: &Node, under: bool, out: &mut HashSet<NodeId>) {
for c in n.children().into_iter().flatten() {
if under {
out.insert(c.id);
}
masked_ids(c, under || has_own_linked_mask(c), out);
}
}
/// How many images and symbol instances under `n` the linked opacity mask of an object above them
/// covers (every one of them with `under`).
fn masked_placed(n: &Node, under: bool) -> usize {
n.children().into_iter().flatten().map(|c| usize::from(under && is_placed(c)) + masked_placed(c, under || has_own_linked_mask(c))).sum()
}
/// Is `n`, or anything under it, one of `ids`?
fn holds_any(n: &Node, ids: &HashSet<NodeId>) -> bool {
if ids.is_empty() {
return false;
}
let mut any = false;
n.walk(&mut |c| any |= ids.contains(&c.id));
any
}
/// Do the paths under `n` that [`for_each_leaf`] visits all fill by the non-zero rule without a
/// stroke that paints? Effects then reshape their outlines alike in any order that the
/// optimization of [`crate::geometry::optimize_geometry_effects`] takes.
fn leaves_safe(n: &Node) -> bool {
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => {
let has_stroke = n.appearance.stroke().is_some_and(|s| !s.paint.is_none() && s.width > 0.0);
leaf_geometry(n).is_none_or(|(_, rule)| rule == FillRule::NonZero && !has_stroke)
}
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => children.iter().all(|c| leaves_safe(c)),
_ => true,
}
}
/// Is there an image with a linked opacity mask among the members of `n`, or among its groups'
/// and layers' members, which [`mark_warped_images`] walks?
fn has_masked_image(n: &Node) -> bool {
let (NodeKind::Group { children, .. } | NodeKind::Layer { children, .. }) = &n.kind else { return false };
children.iter().any(|c| match c.kind {
NodeKind::Image(_) => c.extras().mask.as_ref().is_some_and(|m| m.linked),
_ => has_masked_image(c),
})
}
/// Largest factor a Transform effect's Scale Strokes & Effects scales one object's strokes by
/// (its copies stop before one would take more), so stroke weights stay finite where the geometry
/// doesn't grow with them (a horizontal line stretched vertically).
const MAX_STROKE_SCALE: f64 = 1.0e6;
/// How a Transform effect with `options` scales what it maps: strokes, effects and patterns as
/// its options say.
fn copy_scaling(options: crate::distort::TransformOptions) -> vectorcraft_doc::Scaling {
vectorcraft_doc::Scaling {
strokes: options.strokes,
effects: options.strokes.then_some(crate::scale_effect as fn(&mut Effect, f64)),
patterns: options.patterns,
// A map that flattens the path leaves its fitted gradients fitted, as `fixes_fit` does.
keep_fit_when_flat: true,
..Default::default()
}
}
/// [`crate::scale_effect`] for the effects that do not follow a Transform effect as raster effects
/// do ([`crate::raster::follows`]): Scale Strokes & Effects scales them, and [`follow_art`] moves
/// the others.
fn scale_unfollowed(e: &mut Effect, s: f64) {
if !crate::raster::follows(&e.id) {
crate::scale_effect(e, s);
}
}
/// Make the raster effects under `art` follow `a`, the linear part of a Transform effect that moved
/// it ([`crate::raster::follow_contents`]): those of every object in it (its own and its fills' and
/// strokes'), the art's own effects excepted (`up` is `None` for the art itself). A path that kept
/// its shape (in `kept`), and a group with such a path under it, keep their own and their fills'
/// and strokes'. `up` is what the Transform effects of the groups between an object and the art do
/// to the object's effects. A Transform effect of an object's own that makes copies or moves at
/// random leaves the effects it holds back as they are, so those follow `a` and the groups' maps
/// alone. The linked opacity mask of an object that took the effect as an object (in `moved`, and
/// every image and symbol instance) moved with it, so the raster effects in the mask's art follow
/// `a` as those of a mask that takes the effect as art do ([`Step::reshape_masks`]); `moved` is
/// `None` inside such a mask's art, where every linked mask moved. The walk reaches the groups and
/// layers that [`for_each_leaf`] made the art's own, so it copies nothing. Returns whether a path in
/// `kept` is under `n`.
fn follow_art(n: &mut Node, a: Affine, kept: &HashSet<NodeId>, moved: Option<&HashSet<NodeId>>, up: Option<Affine>, depth: usize) -> bool {
if n.is_guide() {
return false;
}
if moved.is_none_or(|moved| moved.contains(&n.id)) || is_placed(n) {
follow_moved_mask(n, a, depth);
}
// What the object's own Transform effects then do to its fills' and strokes' effects and its
// members' (nothing when one of them holds those back); the art's own effects are the ones
// being applied.
let below = up.map_or(Affine::IDENTITY, |up| crate::raster::contents_step(&n.appearance.effects).map_or(up, |s| up * s));
let keeps = match &mut n.kind {
NodeKind::Path { .. } | NodeKind::Compound { .. } => kept.contains(&n.id),
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
let mut keeps = false;
for c in children.iter_mut() {
keeps |= follow_art(Arc::make_mut(c), a, kept, moved, Some(below), depth);
}
keeps
}
_ => false,
};
if !keeps {
if let Some(up) = up
&& contents_raster(&n.appearance.effects)
{
crate::raster::follow_contents(&mut n.appearance.effects, a, up);
}
if n.appearance.items.iter().any(|i| contents_raster(i.effects())) {
for item in &mut n.appearance.items {
crate::raster::follow_contents(item.effects_mut(), a, below);
}
}
}
keeps
}
/// Make the raster effects in the art of the linked opacity mask of `n`, which moved with `n`,
/// follow `a` ([`follow_art`]), with those of the linked masks inside that art, which moved too. A
/// mask past [`vectorcraft_doc::depth::MAX_DEPTH`] masks inside masks stays as it is.
fn follow_moved_mask(n: &mut Node, a: Affine, depth: usize) {
if depth >= vectorcraft_doc::depth::MAX_DEPTH {
return;
}
let Some(m) = n.extras_if_any().and_then(|x| x.mask.as_mut()).filter(|m| m.linked) else { return };
follow_art(Arc::make_mut(&mut m.art), a, &HashSet::new(), None, Some(Affine::IDENTITY), depth + 1);
}
/// Does any of `effects` follow a Transform effect as a raster effect ([`crate::raster::follows`])?
fn contents_raster(effects: &[Effect]) -> bool {
effects.iter().any(|e| crate::raster::follows(&e.id))
}
/// A Transform effect whose Transform Patterns or Scale Strokes & Effects is on (`options`), with
/// map `m` and `copies`, applied to `leaf` (a path or compound path with geometry `path`) as Object
/// › Transform with those options transforms an object (#885): the strokes' weights and dashes
/// and the distances of the effects on its fills and strokes scale by `m`'s mean scale, and its
/// pattern paints transform by `m`. With copies, the original keeps them and copy `k` takes them
/// `k` times, each copy an object of its own ([`copy_leaf`], within `budget`). With Transform
/// Objects off the geometry stays and the options apply once. With `follows`, Scale Strokes &
/// Effects leaves the raster effects that follow the Transform effect ([`follow_art`]) to the
/// caller. The effects `kept` names keep their values on the leaf and on each copy
/// ([`keep_listed`]), whether Scale Strokes & Effects scaled them or left them to the caller.
/// Returns what the leaf costs after, and `false` when the path kept its shape because the moved
/// path would pass `budget`.
#[allow(clippy::too_many_arguments)]
fn transform_leaf(
leaf: &mut Node,
path: &PathData,
m: Affine,
copies: usize,
options: crate::distort::TransformOptions,
budget: Budget,
follows: bool,
kept: Kept,
) -> (Budget, bool) {
let mut scaling = copy_scaling(options);
if follows && copies == 0 {
scaling.effects = options.strokes.then_some(scale_unfollowed as fn(&mut Effect, f64));
}
if !options.objects {
if options.strokes
&& let Some(k) = vectorcraft_doc::Scaling::factor(m).filter(|k| k.is_finite() && *k > 0.0)
{
leaf.appearance.scale_strokes(k);
leaf.appearance.scale_effects(k, crate::scale_effect);
}
if options.patterns {
vectorcraft_doc::pattern::transform_pattern_paints(leaf, m);
}
keep_listed(leaf, kept);
return (Budget::of(path), true);
}
if copies == 0 {
// A path scaled up is drawn with more lines; past the budget it keeps its shape.
let moved = path.transformed(m);
if !budget.fits(Budget::of(&moved)) {
return (Budget::of(path), false);
}
leaf.transform(m, scaling);
keep_listed(leaf, kept);
return (Budget::of(&moved), true);
}
(copy_leaf(leaf, path, m, copies, scaling, budget, kept), true)
}
/// Does a Transform effect's copies of `art` copy it whole ([`copy_art`])? They do when `art` is a
/// group or layer of several objects, at least one of which paints, with no type left in it and no
/// fill or stroke of its own that paints. Type that stays type is copied in its place by the baking
/// that keeps it, a clipping group's first member is its clip, and the art's own fills and strokes
/// paint the shape of everything under it, copies included. A member that [`makes_art`] is copied
/// in its place as well: each copy would make that art again, past what [`copy_art`] counts.
fn copies_whole(art: &Node) -> bool {
fn parts(n: &Node, count: &mut usize, painted: &mut bool, text: &mut bool) {
match &n.kind {
NodeKind::Text(_) => *text = true,
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => {
*count += 1;
*painted |= n.appearance.items.iter().any(crate::paints);
}
NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => {
*count += 1;
*painted = true;
}
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
*painted |= n.appearance.items.iter().any(crate::paints);
children.iter().for_each(|c| parts(c, count, painted, text));
}
_ => {}
}
}
if art.clips() || !matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || art.appearance.items.iter().any(crate::paints) {
return false;
}
let (mut count, mut painted, mut text) = (0, false, false);
parts(art, &mut count, &mut painted, &mut text);
count > 1 && painted && !text && !art.children().into_iter().flatten().any(|c| makes_art(c))
}
/// Does `n` or anything visible under it, the art of its opacity masks included, carry a visible
/// effect that makes art when it is drawn (its own or that of a fill or stroke that paints), or is
/// any of them a compound shape? Raster effects, color adjustments and unknown effects make none.
fn makes_art(n: &Node) -> bool {
let makes = |e: &Effect| {
e.visible
&& (is_geometry(&e.id)
|| crate::is_pathfinder(&e.id)
|| crate::is_three_d(&e.id)
|| e.id == crate::CROP_MARKS
|| e.id == crate::addons::halftone::HALFTONE)
};
n.visible
&& (n.is_compound_shape()
|| n.appearance.effects.iter().any(makes)
|| n.appearance.items.iter().any(|i| crate::paints(i) && i.effects().iter().any(makes))
|| n.extras().mask.as_deref().is_some_and(|m| makes_art(&m.art))
|| n.children().into_iter().flatten().any(|c| makes_art(c)))
}
/// What the paths under `n` cost together, those of its opacity masks included.
fn copy_cost(n: &Node) -> Budget {
let mut cost = Budget { anchors: 0, lines: 0 };
vectorcraft_doc::swatches::walk_with_masks(n, &mut |c| {
if let NodeKind::Path { guide: false, path, .. } = &c.kind {
cost = cost.plus(Budget::of(path));
}
});
cost
}
/// What the geometry effects of `leaf` (with geometry `path`) and of its fills and strokes add to
/// what `path` costs when it is drawn.
fn effects_cost(leaf: &Node, path: &PathData) -> Budget {
let none = Budget { anchors: 0, lines: 0 };
let items: Vec<&AppearanceItem> = leaf.appearance.items.iter().filter(|i| crate::paints(i) && crate::has_geometry(i.effects())).collect();
if items.is_empty() && !crate::has_geometry(&leaf.appearance.effects) {
return none;
}
let Some(bounds) = path.bounds() else { return none };
let ctx = GeomContext { seed: leaf.id.0, ..GeomContext::of(leaf) };
let own = crate::apply_geometry_with(&leaf.appearance.effects, path, bounds, &ctx);
let item_cost = |i: &&AppearanceItem| {
let b = own.bounds()?;
Some(Budget::of(&crate::apply_geometry_with(i.effects(), &own, b, &ctx.item(i))))
};
// The object's geometry is drawn as it is only by what has no geometry effect of its own.
let plain = items.is_empty() || leaf.appearance.items.iter().any(|i| crate::paints(i) && !crate::has_geometry(i.effects()));
let base = if plain { Budget::of(&own) } else { none };
base.plus(items.iter().filter_map(item_cost).fold(none, Budget::plus)).minus(Budget::of(path))
}
/// Remove the guides anywhere under `n`.
fn without_guides(n: &mut Node) {
let Some(children) = n.children_mut() else { return };
children.retain(|c| !c.is_guide());
children.iter_mut().for_each(|c| without_guides(Arc::make_mut(c)));
}
/// Replace the members of `art` by groups of them: the original on top and `copies` more below it,
/// each mapped once more by `m` than the last (with `scaling`), as a Transform effect copies a
/// group, without its guides. The copies stop where [`copy_leaf`]'s would, before they pass `budget` or
/// [`crate::distort::MAX_COPY_OBJECTS`] nodes in all, the art of opacity masks counted. Returns what
/// the art costs after.
fn copy_art(art: &mut Node, m: Affine, copies: usize, scaling: vectorcraft_doc::Scaling, budget: Budget) -> Budget {
let Some(mut members) = art.children().cloned() else { return art_cost(art) };
// A layer's ruler guides are not copied: they stay above the copies, once.
let guides = members.split_off(Node::guide_run_start(&members));
let original = Node::group(art.id, members);
let mut made = copy_cost(&original);
let mut drawn = original.clone();
without_guides(&mut drawn);
let mut nodes = original.count_with_masks();
let one = drawn.count_with_masks();
let mut placements: Vec<Arc<Node>> = vec![];
let mut xf = Affine::IDENTITY;
for _ in 0..copies {
xf *= m;
if scaling.strokes && vectorcraft_doc::Scaling::factor(xf).is_some_and(|k| !(k.is_finite() && k <= MAX_STROKE_SCALE)) {
break;
}
nodes = nodes.saturating_add(one);
if nodes > crate::distort::MAX_COPY_OBJECTS {
break;
}
let mut copy = drawn.clone();
copy.transform(xf, scaling);
let cost = copy_cost(&copy);
if !budget.fits(made.plus(cost)) || !copy.geometric_bounds().is_some_and(crate::distort::within_reach) {
break;
}
placements.push(Arc::new(copy));
made = made.plus(cost);
}
// The last copy is the lowest.
placements.reverse();
placements.push(Arc::new(original));
placements.extend(guides);
if let Some(children) = art.children_mut() {
*children = placements;
}
made
}
/// Replace `leaf` (a path or compound path with geometry `path`) by a group of it and `copies`
/// more below it, each mapped once more by `m` than the last (with `scaling`), as a Transform effect
/// copies an object. Each copy is an object of its own, so its gradients fit it (a placed gradient
/// moves with it) rather than spanning every copy, and its strokes and patterns can differ from the
/// others'. The copies stop where [`crate::distort::transform_within`]'s would, before they pass
/// `budget` with what each copy's own geometry effects make ([`effects_cost`]), and before one's
/// strokes would scale past [`MAX_STROKE_SCALE`]. The effects `kept` names keep their values on
/// each copy, before its cost is counted ([`keep_listed`]). Returns what the copies cost, the
/// original included.
fn copy_leaf(leaf: &mut Node, path: &PathData, m: Affine, copies: usize, scaling: vectorcraft_doc::Scaling, budget: Budget, kept: Kept) -> Budget {
let mut made = Budget::of(path).plus(effects_cost(leaf, path));
let mut placements: Vec<Arc<Node>> = vec![Arc::new(leaf.clone())];
let mut xf = Affine::IDENTITY;
for _ in 0..copies {
xf *= m;
// A copy scaled up is drawn with more lines.
let moved = path.transformed(xf);
if scaling.strokes && vectorcraft_doc::Scaling::factor(xf).is_some_and(|k| !(k.is_finite() && k <= MAX_STROKE_SCALE)) {
break;
}
let mut copy = leaf.clone();
copy.transform(xf, scaling);
keep_listed(&mut copy, kept);
// Each copy's own geometry effects make a different amount, as the copy and its effects scale.
let mut cost = Budget::of(&moved).plus(effects_cost(&copy, &moved));
// Each copy carries the art of its linked masks, moved with it.
each_linked_mask(&copy, true, 0, &mut |_, m| cost = cost.plus(art_cost(m)));
if !budget.fits(made.plus(cost)) || !copy.geometric_bounds().is_some_and(crate::distort::within_reach) {
break;
}
placements.push(Arc::new(copy));
made = made.plus(cost);
}
// The copies become nodes of their own under a group with the object's id and name, the object
// on top.
placements.reverse();
let mut g = Node::group(leaf.id, placements);
g.name = leaf.name.clone();
*leaf = g;
made
}
/// Live-effect plug-in `plugin` (one that paints what it returns) with `params` run on `leaf`, a
/// path or compound path, as one of its object's effects with reference box `bounds`: `leaf`
/// becomes a group with its id and name of one object for each piece returned, in order, each with
/// the piece's geometry and the leaf's appearance with the piece's fills and strokes in place of
/// its own ([`vectorcraft_plugins::painted::appearance`]). The swatches and patterns the pieces
/// name must be in `doc`, and without one the pieces may name none. Returns what the pieces cost
/// ([`piece_cost`]); `None` (the leaf stays as it is) when the plug-in fails, the pieces are
/// refused, or they would pass `budget`. A deferred run is listed by `at`.
fn paint_leaf(
leaf: &mut Node,
plugin: &str,
params: &serde_json::Value,
bounds: Rect,
budget: Budget,
doc: Option<&Document>,
at: Option<vectorcraft_plugins::deferred::Stage>,
) -> Option<Budget> {
let (path, rule) = leaf_geometry(leaf)?;
let paints = vectorcraft_plugins::effect::Paints { appearance: Some(&leaf.appearance), item: None, at };
let pieces = vectorcraft_plugins::effect::apply_painted(plugin, params, &path, bounds, rule, paints, doc)?;
let made = pieces.iter().map(|p| piece_cost(&p.path)).fold(Budget { anchors: 0, lines: 0 }, Budget::plus);
if !budget.fits(made) {
return None;
}
let children = pieces
.iter()
.map(|p| {
let mut one = leaf.clone();
one.name = None;
one.appearance = vectorcraft_plugins::painted::appearance(&leaf.appearance, p).into();
set_geometry(&mut one, p.path.clone(), p.rule);
Arc::new(one)
})
.collect();
let mut g = Node::group(leaf.id, children);
g.name = leaf.name.clone();
*leaf = g;
Some(made)
}
/// What a painted piece with geometry `path` costs: its anchors and lines, plus a share of the
/// effects' budget for the object it becomes, so that a budget of [`Budget::EFFECTS`] holds at most
/// [`MAX_OBJECTS`](vectorcraft_plugins::objects::MAX_OBJECTS) pieces however small their paths.
fn piece_cost(path: &PathData) -> Budget {
Budget::of(path).plus(piece_share())
}
/// The share of the effects' budget each painted piece costs besides its path ([`piece_cost`]).
fn piece_share() -> Budget {
let max = vectorcraft_plugins::objects::MAX_OBJECTS;
Budget { anchors: Budget::EFFECTS.anchors / max, lines: Budget::EFFECTS.lines / max }
}
/// Live-effect plug-in `plugin` (one that paints what it returns) with `params`, a later effect of
/// `item`, run on `leaf`, a path of [`transform_piece`] painted by `item` (its first fill or stroke),
/// with reference box `bounds`: `leaf` becomes a group of one path for each piece returned, in
/// order. The plug-in is given `appearance`'s fills and strokes and told that it is on item `index`
/// of them. A piece that gives fills (on a fill) or strokes (on a stroke) is painted by copies of
/// `item` with those paints ([`vectorcraft_plugins::painted::items`]), under the leaf's other fills
/// and strokes; one that gives none keeps the leaf's. Returns
/// what the paths cost ([`piece_cost`]); `None` (the leaf stays as it is) when the plug-in fails,
/// the pieces are refused (a swatch or pattern not in `doc`, or any without one), or they would pass
/// `budget`. A deferred run is listed by `at`.
#[allow(clippy::too_many_arguments)]
fn paint_piece(
leaf: &mut Node,
item: &AppearanceItem,
(appearance, index): (&Appearance, usize),
(plugin, params): (&str, &serde_json::Value),
bounds: Rect,
budget: Budget,
doc: Option<&Document>,
at: Option<vectorcraft_plugins::deferred::Stage>,
) -> Option<Budget> {
let (path, rule) = leaf_geometry(leaf)?;
let paints = vectorcraft_plugins::effect::Paints { appearance: Some(appearance), item: Some(index), at };
let pieces = vectorcraft_plugins::effect::apply_painted(plugin, params, &path, bounds, rule, paints, doc)?;
let made = pieces.iter().map(|p| piece_cost(&p.path)).fold(Budget { anchors: 0, lines: 0 }, Budget::plus);
if !budget.fits(made) {
return None;
}
let mut children = vec![];
for p in pieces.iter() {
let gives = match item {
AppearanceItem::Fill(_) => p.fills.is_some(),
AppearanceItem::Stroke(_) => p.strokes.is_some(),
};
let items = if gives {
let mut items = vectorcraft_plugins::painted::items(item, p);
items.extend_from_slice(leaf.appearance.items.get(1..).unwrap_or_default());
items
} else {
leaf.appearance.items.clone()
};
if items.is_empty() {
continue;
}
let mut one = leaf.clone();
one.appearance.items = items;
set_geometry(&mut one, p.path.clone(), p.rule);
children.push(Arc::new(one));
}
*leaf = Node::group(leaf.id, children);
Some(made)
}
/// Every path and compound path under `art` (other than clipping paths) with a fill or stroke whose
/// own Transform effect has Transform Patterns or Scale Strokes & Effects on
/// ([`item_transform_options`]), or whose own live-effect plug-in paints what it returns
/// ([`painted_plugin`]), made into a group of objects of its own ([`transform_item_leaf`]). The
/// new paths keep to one [`Budget::EFFECTS`] together (or to what `art` costs, when that is more).
/// The swatches and patterns painted pieces name must be in `doc`; without one they may name none.
fn transform_items(art: &mut Node, doc: Option<&Document>) {
let mut used = art_cost(art);
let max = Budget::EFFECTS.at_least(used);
for_each_leaf(art, &mut |leaf| transform_item_leaf(leaf, max, &mut used, doc));
}
/// Is `e` an effect that makes a fill's or stroke's piece of a path an object of its own: a
/// Transform effect that [`reaches_appearance`], or a live-effect plug-in that paints what it
/// returns ([`painted_plugin`])?
fn makes_piece(e: &Effect) -> bool {
reaches_appearance(e) || painted_plugin(e).is_some()
}
/// Does `item` paint and carry an effect that [`makes_piece`]?
fn item_makes_piece(item: &AppearanceItem) -> bool {
crate::paints(item) && item.effects().iter().any(makes_piece)
}
/// `leaf`, a path or compound path with a fill or stroke whose own Transform effect has Transform
/// Patterns or Scale Strokes & Effects on, as a group with its id, name, transparency and object
/// effects. In paint order, each such fill or stroke is a path of its own: the leaf's geometry after
/// the geometry effects of that fill or stroke before its first such Transform effect, transformed
/// as [`transform_leaf`] transforms an object with that effect's map, copies and options, so the
/// stroke's weight and dashes, the distances of the effects before the Transform effect and its
/// pattern follow the map, and the raster effects before it that follow it take their followed
/// values ([`follow_piece`]). The effects after it keep their values and apply to what it made
/// ([`keep_listed`]), and with Transform Objects off so do the effects before it. The reference box
/// is that geometry's bounds and the seed is the leaf's id, as the renderer draws a fill's or
/// stroke's own effects. The geometry effects after the Transform effect then
/// apply to the transformed path and its copies as one piece ([`transform_piece`]), and its other
/// effects stay on each of them. The other fills and strokes stay together on paths of the leaf's
/// geometry between them, with their own effects. `max` is the budget of the whole art and `used`
/// what it has spent.
///
/// A fill or stroke whose first such effect is a live-effect plug-in that paints what it returns
/// ([`painted_plugin`]) becomes, in place of a transformed path, one path for each piece the
/// plug-in returns, painted by copies of the fill or stroke with the piece's paints
/// ([`vectorcraft_plugins::painted::items`]); the plug-in is given the geometry before it and told
/// which fill or stroke it is on. When it fails (or names a swatch or pattern not in `doc`, or any
/// without a document), the
/// fill or stroke paints that geometry as it is. The geometry effects after it then apply to all
/// the pieces as one ([`transform_piece`]).
fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget, doc: Option<&Document>) {
if matches!(leaf.kind, NodeKind::Path { clipping: true, .. }) || !leaf.appearance.items.iter().any(item_makes_piece) {
return;
}
let Some((path, rule)) = leaf_geometry(leaf) else { return };
let original = leaf.clone();
let ctx = GeomContext::of(&original);
let bounds = path.to_bezpath().bounding_box();
// A path of the leaf's geometry painted by `items` alone, without the leaf's transparency and
// object effects (the group keeps them).
let piece = |items: Vec<AppearanceItem>| {
let mut p = original.clone();
p.appearance.items = items;
p.appearance.effects.clear();
p.appearance.contents_index = None;
p.opacity = 1.0;
p.blend = Default::default();
p.isolate = false;
p.knockout = Default::default();
p.extras_mut().mask = None;
p
};
// The pieces replace the leaf's own geometry (counted in `used`): each plain piece costs that
// geometry again, and each transformed fill or stroke what its paths cost.
let cost = Budget::of(&path);
*used = used.minus(cost);
let mut pieces: Vec<Arc<Node>> = vec![];
let mut plain: Vec<AppearanceItem> = vec![];
// Which items become a transformed piece of their own; every other run of items is one plain piece.
let transformed: Vec<bool> = original.appearance.items.iter().map(item_makes_piece).collect();
// The number of pieces after item `i` in paint order: each costs at least the leaf's geometry.
let later = |i: usize| (i + 1..transformed.len()).filter(|&j| transformed[j] || j == i + 1 || transformed[j - 1]).count();
for (i, item) in original.appearance.items.iter().enumerate() {
let fx = item.effects();
let Some(at) = fx.iter().position(makes_piece).filter(|_| crate::paints(item)) else {
plain.push(item.clone());
continue;
};
if !plain.is_empty() {
pieces.push(Arc::new(piece(std::mem::take(&mut plain))));
*used = used.plus(cost);
}
let before = &fx[..at];
// What the art's other paths leave of the budget, less the leaf's geometry for each piece
// still to come: the geometry before the Transform effect and its copies keep to it.
let reserved = (0..later(i)).fold(Budget { anchors: 0, lines: 0 }, |r, _| r.plus(cost));
let limit = max.minus(reserved);
let room = limit.minus(*used);
// The fill's or stroke's gradient follows the geometry effects before the Transform effect.
let mut carry = crate::Carry::default();
let g = if crate::has_geometry(before) && !path.is_empty() {
crate::geometry::apply_geometry_carrying_within(before, &path, bounds, &ctx.item(item), room, Some(&mut carry))
} else {
path.clone()
};
let Some(tb) = g.bounds() else { continue };
let params = merged_params(&fx[at].id, &fx[at].params);
// Where each effect the piece keeps sits among the item's effects.
let kept: Vec<usize> = (0..at).filter(|&k| !is_geometry(&fx[k].id)).chain(at + 1..fx.len()).collect();
// A Transform effect after this one that makes copies or moves at random holds the raster
// effects before this one back from every Transform effect ([`crate::raster::followed`]).
let held_later = fx.get(at + 1..).unwrap_or_default().iter().any(crate::raster::holds_back);
// The effects before the Transform effect that it keeps, each raster effect following the
// geometry effects before it that moved the geometry ([`crate::raster::without_geometry`],
// [`crate::Carry::hide_skipped`]) unless held back.
let mut ran = std::borrow::Cow::Borrowed(before);
if carry.skipped() {
carry.hide_skipped(ran.to_mut());
}
let kept_before: Vec<Effect> =
if held_later { before.iter().filter(|e| !is_geometry(&e.id)).cloned().collect() } else { crate::without_geometry(&ran) };
if let Some(plugin) = painted_plugin(&fx[at]) {
let mut it = item.clone();
let rest: Vec<Effect> = kept_before.iter().chain(&fx[at + 1..]).cloned().collect();
*it.effects_mut() = rest;
let mut one = Appearance { items: vec![it.clone()], ..Default::default() };
if carry.moves(&one) {
carry.apply(&mut one);
}
let it = one.items.pop().unwrap_or(it);
let stage = vectorcraft_plugins::deferred::Stage { object: ctx.seed, effect: at, item: Some(i) };
let paints = vectorcraft_plugins::effect::Paints { appearance: Some(&original.appearance), item: Some(i), at: Some(stage) };
let painted = vectorcraft_plugins::effect::apply_painted(plugin, &params, &g, tb, rule, paints, doc)
.map(|pieces| pieces.iter().map(|p| (p.path.clone(), p.rule, vectorcraft_plugins::painted::items(&it, p))).collect::<Vec<_>>())
.filter(|pieces| room.fits(pieces.iter().map(|(p, ..)| piece_cost(p)).fold(Budget { anchors: 0, lines: 0 }, Budget::plus)));
// A failed run draws the fill or stroke on the geometry it was given.
let returned = painted.unwrap_or_else(|| vec![(g.clone(), rule, vec![it])]);
let mut group: Vec<Arc<Node>> = vec![];
for (path, rule, items) in returned.into_iter().filter(|(_, _, items)| !items.is_empty()) {
*used = used.plus(piece_cost(&path));
let mut p = piece(items);
set_geometry(&mut p, path, rule);
group.push(Arc::new(p));
}
let mut p = Node::group(leaf.id, group);
transform_piece(&mut p, (ctx.seed, i, &kept), crate::raster::held_back(before), true, limit, used, doc);
pieces.push(Arc::new(p));
continue;
}
let (_, copies) = crate::distort::transform_map(tb, &params, ctx.seed);
let options = crate::distort::TransformOptions::of(&params);
let map = crate::distort::transform_affine(tb, &params, ctx.seed);
let mut it = item.clone();
let rest: Vec<Effect> = kept_before.iter().chain(&fx[at + 1..]).cloned().collect();
*it.effects_mut() = rest.clone();
let mut p = piece(vec![it]);
if carry.moves(&p.appearance) {
carry.apply(&mut p.appearance);
}
set_geometry(&mut p, g.clone(), rule);
let after = kept_before.len();
let keep = |k: usize| k >= after || !options.objects;
let (made, moved) = transform_leaf(&mut p, &g, map, copies, options, room, false, Some((&rest, &keep)));
*used = used.plus(made);
let from = crate::raster::held_back(before);
if moved && !held_later {
follow_piece(&mut p, kept_before.get(from..).unwrap_or_default(), from, &fx[at]);
}
transform_piece(&mut p, (ctx.seed, i, &kept), crate::raster::held_back(&fx[..=at]), false, limit, used, doc);
pieces.push(Arc::new(p));
}
if !plain.is_empty() {
pieces.push(Arc::new(piece(plain)));
*used = used.plus(cost);
}
leaf.appearance.items.clear();
leaf.appearance.contents_index = None;
leaf.knockout = vectorcraft_doc::Knockout::Off;
leaf.kind = NodeKind::Group { children: pieces, clip: false };
}
/// Apply the visible geometry effects left on the fills or strokes of each path under `p` (a
/// transformed piece of [`transform_item_leaf`] and its copies) to all of `p`'s paths as one piece:
/// each effect takes the bounds of all the paths as its reference box and `seed`, as an object's
/// own effects reshape all its paths ([`reshape_leaves_carrying`]). Each path applies its own copy of
/// the effect, at the effect's own distances. A Transform effect with Transform Patterns or Scale
/// Strokes & Effects on transforms each path as [`transform_leaf`] does, and leaves the values of
/// the effects after it, and with Transform Objects off those before it, as they are
/// ([`keep_listed`]).
/// A live-effect plug-in that paints what it returns ([`painted_plugin`]) makes each path one path
/// for each piece it returns ([`paint_piece`]). A path with several fills or strokes (those a
/// painted plug-in returned) takes such an effect, a live-effect plug-in or a geometry effect on
/// strokes once for each of them, in paint order, given its own place among them
/// ([`shares_stage`]): each run's pieces replace only that fill or stroke, and a run that fails or
/// would pass the budget leaves that fill or stroke on the path as it was. The raster effects
/// before a Transform effect follow it ([`follow_piece`]), except the first `held` effects, which
/// a Transform effect before the piece's effects that makes copies or moves at random holds back,
/// and the effects before such a Transform effect among the piece's effects. The geometry effects then
/// leave the fills and strokes; their other effects stay. The paths keep to `max` together with
/// the rest of the art (`used`), and the swatches and patterns painted pieces name must be in
/// `doc`. `(seed, source, kept)`: the object's seed, the index in the object's appearance of the
/// fill or stroke the piece was made from, and the index among that item's effects of each effect
/// the piece kept, which a deferred live-effect plug-in is listed by. `painted`: `p`'s paths are
/// the pieces of a painted run, each of which `used` counts at [`piece_cost`].
fn transform_piece(
p: &mut Node,
(seed, source, kept): (u64, usize, &[usize]),
held: usize,
painted: bool,
max: Budget,
used: &mut Budget,
doc: Option<&Document>,
) {
let mut count = 0;
for_each_leaf(p, &mut |l| {
for item in &l.appearance.items {
count = count.max(item.effects().len());
}
});
// The effects before a Transform effect among the piece's effects that makes copies or moves at
// random are held back from the Transform effects before it too.
let mut held = held;
for_each_leaf(p, &mut |l| {
for item in &l.appearance.items {
if let Some(last) = item.effects().iter().rposition(crate::raster::holds_back) {
held = held.max(last);
}
}
});
// Whether `used` counts each path under `p`, in the order `for_each_leaf` visits them, at
// [`piece_cost`] (its geometry and a painted piece's share) rather than at its geometry alone.
// The pieces of a painted run are counted so, and so is the first path an effect makes of one
// that was: the paths of a Transform effect's copies share their original's share.
let mut shares = vec![painted; leaf_count(p)];
for j in 0..count {
let Some(bounds) = p.geometric_bounds() else { break };
let from = held.min(j);
let mut holds = false;
let stage = vectorcraft_plugins::deferred::Stage { object: seed, effect: kept.get(j).copied().unwrap_or(j), item: Some(source) };
let at = PieceStage { j, from, seed, bounds, doc, stage };
// Stage `j` on `leaf`, a path `used` counts at `piece_cost` when `share`: whether `used`
// counts each path it became so, or `None` when it stays as it was.
let mut on_leaf = |leaf: &mut Node, share: bool| -> Option<Vec<bool>> {
let effect = |item: &AppearanceItem| item.effects().get(j).filter(|e| e.visible && is_geometry(&e.id)).cloned();
let items = &leaf.appearance.items;
let first = items.first()?;
let (path, _) = leaf_geometry(leaf)?;
// What `used` counts the path at; what the other paths leave of the budget is the rest.
let geometry = Budget::of(&path);
let counted = if share { piece_cost(&path) } else { geometry };
if shares_stage(items, j) {
let e = effect(first)?;
holds |= crate::raster::holds_back(&e);
let appearance = leaf.appearance.clone();
// A painted run's pieces each cost a share of their own ([`paint_piece`]); the
// paths of any other effect keep the leaf's.
let painted = painted_plugin(&e).is_some();
let before = if painted { counted } else { geometry };
let room = max.minus(used.minus(before));
let made = piece_stage(leaf, &e, &at, (&appearance, 0), room)?;
*used = used.minus(before).plus(made);
return Some((0..leaf_count(leaf)).map(|k| painted || (share && k == 0)).collect());
}
// Each fill or stroke takes the effect on its own path, given the leaf's fills and
// strokes and its own place among them; those left with the same path as the one
// before them stay on it. Each path costs its geometry and a painted piece's share.
// Each run is first given all the room the art leaves. When a fill or stroke that keeps
// the leaf's path then finds no room for its copy of it, the results are placed again,
// each but the last leaving room for one more copy of the leaf's own path; a result
// that does not fit that room is left out. Only a fill or stroke that keeps the leaf's
// path right after one that changed costs such a copy (the ones after it stay on that
// copy), so every copy fits but the first fill's or stroke's, when the art leaves no
// room for it: the leaf then stays as it was. A live-effect plug-in is given all the
// room the art leaves, and each placing checks that its result fits; a built-in effect
// builds within the room of each placing.
let appearance = leaf.appearance.clone();
let one = piece_cost(&path);
let n = appearance.items.len();
let base = used.minus(counted);
let alone = |item: &AppearanceItem| {
let mut sub = leaf.clone();
sub.appearance.items = vec![item.clone()];
sub
};
// `item`, the `k`th fill or stroke, alone on the leaf's path with effect `e` applied
// within `room`, and what its paths cost; `None` when it failed or found no room.
let apply = |item: &AppearanceItem, k: usize, e: &Effect, room: Budget| {
let painted = painted_plugin(e).is_some();
let room = if painted { room } else { room.minus(piece_share()) };
let mut sub = alone(item);
let made = piece_stage(&mut sub, e, &at, (&appearance, k), room)?;
Some((sub, if painted { made } else { made.plus(piece_share()) }))
};
// Each fill's or stroke's live-effect plug-in run. A result is kept only while a
// placing places it on a path of its own; one that does not fit is dropped, with what
// it cost, and the plug-in runs again in the second placing only when that much room
// is left there (a result small enough to cache comes back from the cache). A result
// with the same path as the one before it costs nothing: it is placed on that path
// whatever room is left, and dropped once its fill or stroke is moved there. The
// results kept at once cost at most what the two placings place.
let mut runs: Vec<PluginRun> = vec![PluginRun::Not; n];
let mut kept_cost = Budget { anchors: 0, lines: 0 };
// Each placing gives the paths, whether `used` counts each of the paths they hold at
// `piece_cost` (all of a painted run's, and the first of any other result's), and what
// the art then spends.
let mut split = |reserve: bool| -> Option<(Vec<Node>, Vec<bool>, Budget)> {
let mut spent = base;
let mut out: Vec<Node> = vec![];
let mut counts: Vec<bool> = vec![];
for (k, (item, ran)) in appearance.items.iter().zip(runs.iter_mut()).enumerate() {
let room = if reserve && k + 1 < n { max.minus(spent).minus(one) } else { max.minus(spent) };
let e = effect(item);
let paints = e.as_ref().is_some_and(|e| painted_plugin(e).is_some());
// Does `sub` stay on the path of the result before it, at no cost?
let merges = |sub: &Node| out.last().is_some_and(|last| same_path(last, sub));
// What the kept plug-in result this fill or stroke takes cost.
let mut taken = None;
let result = match e {
None => None,
Some(e) => {
holds |= crate::raster::holds_back(&e);
if vectorcraft_plugins::effect::plugin_id(&e.id).is_some() {
let again = match ran {
PluginRun::Not | PluginRun::Merged => true,
PluginRun::Left(cost) => room.fits(*cost),
PluginRun::Kept(..) | PluginRun::Failed => false,
};
if again {
#[cfg(test)]
PLUGIN_RUNS.with(|n| n.set(n.get() + 1));
*ran = match apply(item, k, &e, max.minus(base)) {
None => PluginRun::Failed,
Some((sub, cost)) if !room.fits(cost) && !merges(&sub) => PluginRun::Left(cost),
Some((sub, cost)) => {
kept_cost = kept_cost.plus(cost);
PluginRun::Kept(sub, cost)
}
};
}
match ran {
PluginRun::Kept(sub, cost) if room.fits(*cost) || merges(sub) => {
taken = Some(*cost);
Some((sub.clone(), *cost))
}
PluginRun::Kept(_, cost) => {
let cost = *cost;
kept_cost = kept_cost.minus(cost);
*ran = PluginRun::Left(cost);
None
}
PluginRun::Not | PluginRun::Merged | PluginRun::Left(_) | PluginRun::Failed => None,
}
} else {
// A built-in effect that finds no room within `room` leaves the
// leaf's path. When its result is not on the path before it, it
// is built again with all the room the art leaves and taken only
// when that result is on the path before it, which costs nothing.
let made = apply(item, k, &e, room);
let short = match &made {
None => true,
Some((sub, cost)) => !merges(sub) && (!room.fits(*cost) || same_path(sub, leaf)),
};
match out.last() {
Some(_) if short => match apply(item, k, &e, max.minus(base)) {
Some((sub, cost)) if merges(&sub) => Some((sub, cost)),
_ => made,
},
_ => made,
}
}
}
};
let placed = match result {
Some((sub, cost)) if room.fits(cost) || merges(&sub) => Some((sub, cost, paints)),
_ => None,
};
// A fill or stroke whose result is not placed stays on the leaf's path, which
// is copied only when the path before it is another.
let (mut sub, cost, all) = match placed {
Some(placed) => placed,
None => match out.last_mut() {
Some(last) if same_path(last, leaf) => {
last.appearance.items.push(item.clone());
continue;
}
_ => (alone(item), one, true),
},
};
match out.last_mut() {
Some(last) if same_path(last, &sub) => {
last.appearance.items.append(&mut sub.appearance.items);
if let Some(cost) = taken {
kept_cost = kept_cost.minus(cost);
*ran = PluginRun::Merged;
}
}
_ if !max.minus(spent).fits(cost) => return None,
_ => {
spent = spent.plus(cost);
counts.extend((0..leaf_count(&sub)).map(|k| all || k == 0));
out.push(sub);
}
}
#[cfg(test)]
HELD_PEAK.with(|p| p.set(p.get().at_least(kept_cost)));
}
Some((out, counts, spent))
};
let (mut out, counts, spent) = split(false).or_else(|| split(true))?;
*used = spent;
*leaf = match out.len() {
1 => out.pop().unwrap_or_else(|| leaf.clone()),
_ => Node::group(leaf.id, out.into_iter().map(Arc::new).collect()),
};
Some(counts)
};
let mut next: Vec<bool> = vec![];
let mut visited = 0;
for_each_leaf(p, &mut |leaf| {
let share = shares.get(visited).copied().unwrap_or(false);
visited += 1;
next.extend(on_leaf(leaf, share).unwrap_or_else(|| vec![share]));
});
shares = next;
if holds {
held = held.max(j);
}
}
for_each_leaf(p, &mut |leaf| {
for item in &mut leaf.appearance.items {
item.effects_mut().retain(|e| !(e.visible && is_geometry(&e.id)));
}
});
}
/// A fill's or stroke's live-effect plug-in run in a placing of [`transform_piece`]: not run yet,
/// its result and what that costs while a placing places it on a path of its own, what a result
/// that did not fit cost (the result itself is dropped), a result dropped once its fill or stroke
/// was placed on the path before it, or failed.
#[derive(Clone)]
enum PluginRun {
Not,
Kept(Node, Budget),
Left(Budget),
Merged,
Failed,
}
#[cfg(test)]
thread_local! {
/// The most the live-effect plug-in results [`transform_piece`] keeps from one fill or stroke
/// to the next have cost at once on this thread.
pub(crate) static HELD_PEAK: std::cell::Cell<Budget> = const { std::cell::Cell::new(Budget { anchors: 0, lines: 0 }) };
/// How many times [`transform_piece`] has run a fill's or stroke's live-effect plug-in on this
/// thread when they did not share the stage.
pub(crate) static PLUGIN_RUNS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Where a stage of [`transform_piece`] runs: `j`, the index of its effect among the piece's
/// effects; `from`, the first of the effects before it whose raster effects follow a Transform
/// effect; the object's `seed`; the reference box `bounds` of all the piece's paths; the document
/// that painted pieces' swatches and patterns must be in; and the `stage` a deferred live-effect
/// plug-in is listed by.
struct PieceStage<'a> {
j: usize,
from: usize,
seed: u64,
bounds: Rect,
doc: Option<&'a Document>,
stage: vectorcraft_plugins::deferred::Stage,
}
/// Can stage `j` of [`transform_piece`] run once for all of a path's `items`: there is at most one,
/// or they hold the same effects and the effect at `j` is not a visible geometry effect, is a
/// Transform effect, or is a built-in geometry effect and they are all fills (an effect that
/// changes a fill's path does not depend on which fill it is on)?
fn shares_stage(items: &[AppearanceItem], j: usize) -> bool {
let Some(first) = items.first() else { return true };
if items.len() == 1 {
return true;
}
if !items.iter().all(|i| i.effects() == first.effects()) {
return false;
}
match first.effects().get(j) {
Some(e) if e.visible && is_geometry(&e.id) => {
vectorcraft_plugins::effect::plugin_id(&e.id).is_none() && (reaches_appearance(e) || items.iter().all(AppearanceItem::is_fill))
}
_ => true,
}
}
/// Effect `e`, a visible geometry effect at stage `at` of [`transform_piece`], applied to `leaf`, a
/// path painted by its first fill or stroke (and, when they share the stage, [`shares_stage`], the
/// others): `appearance` is what the effect is given of the path's fills and strokes and `index`
/// which of them it is on. A live-effect plug-in that paints what it returns makes `leaf` a group
/// of its pieces ([`paint_piece`]), and a Transform effect that reaches its appearance transforms
/// it ([`transform_leaf`]). Returns what the new paths cost, within `room`; `None` when `leaf`
/// stays as it is.
fn piece_stage(leaf: &mut Node, e: &Effect, at: &PieceStage, (appearance, index): (&Appearance, usize), room: Budget) -> Option<Budget> {
let item = leaf.appearance.items.first()?.clone();
// The effects before this one as they were before it moved the path.
let was = item.effects().get(at.from..at.j).unwrap_or_default();
let (path, rule) = leaf_geometry(leaf)?;
if let Some(plugin) = painted_plugin(e) {
let params = merged_params(&e.id, &e.params);
return paint_piece(leaf, &item, (appearance, index), (plugin, &params), at.bounds, room, at.doc, Some(at.stage));
}
if reaches_appearance(e) {
let params = merged_params(&e.id, &e.params);
let (_, copies) = crate::distort::transform_map(at.bounds, &params, at.seed);
let options = crate::distort::TransformOptions::of(&params);
let map = crate::distort::transform_affine(at.bounds, &params, at.seed);
let j = at.j;
let keep = |k: usize| k > j || (k < j && !options.objects);
let (made, moved) = transform_leaf(leaf, &path, map, copies, options, room, false, Some((item.effects(), &keep)));
if moved {
follow_piece(leaf, was, at.from, e);
}
return Some(made);
}
let stroke = match &item {
AppearanceItem::Stroke(s) => Some(s),
AppearanceItem::Fill(_) => None,
};
let ctx = GeomContext { stroke, rule, seed: at.seed, appearance: Some(appearance), item: Some(index), doc: at.doc, stage: Some(at.stage) };
let mut carry = crate::Carry::default();
let out = crate::geometry::apply_geometry_carrying_within(std::slice::from_ref(e), &path, at.bounds, &ctx, room, Some(&mut carry));
let made = Budget::of(&out);
// The fill's or stroke's gradient follows the effect as the object's does.
if carry.moves(&leaf.appearance) {
carry.apply(&mut leaf.appearance);
}
set_geometry(leaf, out, rule);
// A Transform effect left out (past the budget) leaves the path and its raster effects as they were.
if !carry.skipped() {
follow_piece(leaf, was, at.from, e);
}
Some(made)
}
/// Are `a` and `b` paths or compound paths that differ only in their fills and strokes?
fn same_path(a: &Node, b: &Node) -> bool {
let leaf = |n: &Node| matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. });
if !leaf(a) || !leaf(b) {
return false;
}
// Every field but the fills and strokes, compared in place; the geometry last.
let Node { id, name, visible, locked, opacity, blend, isolate, knockout, knockout_shape, appearance, kind, bbox_angle, extras } = a;
let Appearance { items: _, effects, contents_index } = &**appearance;
*id == b.id
&& *name == b.name
&& *visible == b.visible
&& *locked == b.locked
&& *opacity == b.opacity
&& *blend == b.blend
&& *isolate == b.isolate
&& *knockout == b.knockout
&& *knockout_shape == b.knockout_shape
&& *bbox_angle == b.bbox_angle
&& *extras == b.extras
&& *effects == b.appearance.effects
&& *contents_index == b.appearance.contents_index
&& *kind == b.kind
}
/// The effects a Transform effect in a fill's or stroke's own list leaves as they were
/// ([`keep_listed`]): the list as it was before the Transform effect was applied, and which of its
/// indices keep their values. `None` for an object's own Transform effect.
type Kept<'a> = Option<(&'a [Effect], &'a dyn Fn(usize) -> bool)>;
/// Put back on the fills and strokes of each path under `p` the effect `kept`'s list holds at each
/// index it takes, in place of what a Transform effect in their own list scaled or moved with its
/// options ([`transform_leaf`]). The effects after the Transform effect apply to the art it made,
/// and with Transform Objects off the effects before it keep their values too. A piece's fills and
/// strokes share one list: a live-effect plug-in that paints what it returns gives each paint a copy
/// of the fill or stroke ([`vectorcraft_plugins::painted::items`]).
fn keep_listed(p: &mut Node, kept: Kept) {
let Some((was, keep)) = kept else { return };
for_each_leaf(p, &mut |leaf| {
for item in &mut leaf.appearance.items {
for (k, (e, old)) in item.effects_mut().iter_mut().zip(was).enumerate() {
if keep(k) {
e.clone_from(old);
}
}
}
});
}
/// Make the raster effects of each of `leaf`'s fills and strokes from index `from` on follow
/// Transform effect `t`, listed after them, once `t` has moved the leaf
/// ([`crate::raster::follow_one`]). The fills and strokes are copies of one fill or stroke (those a
/// painted plug-in returns, [`vectorcraft_plugins::painted::items`]), with the same effects. `was`
/// holds those effects of the first one as they were before `t` was applied: each one that
/// follows takes the value [`crate::raster::followed`] gives it, in place of what Scale Strokes &
/// Effects made of it, and the others keep theirs.
fn follow_piece(leaf: &mut Node, was: &[Effect], from: usize, t: &Effect) {
let mut follow = was.to_vec();
if !crate::raster::follow_one(&mut follow, t) {
return;
}
for item in &mut leaf.appearance.items {
for (k, (new, old)) in follow.iter().zip(was).enumerate() {
if new != old
&& let Some(e) = item.effects_mut().get_mut(from + k)
{
*e = new.clone();
}
}
}
}
/// The bounds of each group of type's outlines under `n` ([`outline_text`]), `n` included, in
/// order. A group inside one of them is part of it.
fn type_outline_bounds(n: &Node, out: &mut Vec<Option<Rect>>) {
if n.extras().type_outlines {
out.push(n.geometric_bounds());
} else if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } = &n.kind {
for c in children {
type_outline_bounds(c, out);
}
}
}
/// Move and scale the placed gradients of each group of type's outlines under `n` from its bounds
/// in `before` ([`type_outline_bounds`] before its paths changed) to its bounds now.
fn rebase_type_outlines(n: &mut Node, before: &mut std::vec::IntoIter<Option<Rect>>) {
if !n.extras().type_outlines {
if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } = &mut n.kind {
for c in children.iter_mut() {
rebase_type_outlines(Arc::make_mut(c), before);
}
}
} else if let Some(Some(from)) = before.next()
&& let Some(to) = n.geometric_bounds()
&& from != to
{
for_each_leaf(n, &mut |leaf| {
if leaf.appearance.has_positioned_gradient() {
follow_boxes(&mut leaf.appearance, from, to);
}
});
}
}
/// The geometric bounds of each path under `n` that [`for_each_leaf`] visits, outside groups of
/// type's outlines (those follow the outlines' bounds, [`rebase_type_outlines`]).
fn leaf_bounds(n: &Node, out: &mut Vec<Option<Rect>>) {
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => out.push(n.geometric_bounds()),
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } if !n.extras().type_outlines => {
for c in children {
leaf_bounds(c, out);
}
}
_ => {}
}
}
/// A group inside the art whose own fills and strokes [`carry_groups`] moves (groups of type's
/// outlines follow the outlines' bounds, [`rebase_type_outlines`]).
fn is_member_group(n: &Node) -> bool {
matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) && !n.extras().type_outlines
}
/// A group below the art that [`is_member_group`] takes and whose own fills or strokes hold a
/// gradient: the child indices that lead to it from the art, its geometric bounds, and
/// whether a path below it that its bounds measure kept its shape.
type GroupMark = (Vec<usize>, Option<Rect>, bool);
/// The geometric bounds of `n` (as [`Node::geometric_bounds`] gives them) and whether a path or
/// compound path below it that those bounds measure is in `kept`, measured in one walk. Each group below `n` that
/// [`GroupMark`] describes goes into `out`, reached from `n` through `at`. An image that `images`
/// lists is measured by the pieces it will be cut into ([`WarpedImages::image_bounds`]).
fn group_marks(
n: &Node,
at: &mut Vec<usize>,
kept: &HashSet<NodeId>,
images: Option<&WarpedImages>,
out: &mut Vec<GroupMark>,
) -> (Option<Rect>, bool) {
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => (n.geometric_bounds(), kept.contains(&n.id)),
NodeKind::Image(_) => (images.map_or_else(|| n.geometric_bounds(), |w| w.image_bounds(n)), false),
NodeKind::Group { children, clip } | NodeKind::Layer { children, clip, .. } if !n.extras().type_outlines => {
let layer = matches!(n.kind, NodeKind::Layer { .. });
let skip = children.len().saturating_sub(n.editing_children().len());
let (mut bounds, mut held) = (None, false);
for (i, c) in children.iter().enumerate() {
at.push(i);
let (b, h) = group_marks(c, at, kept, images, out);
if is_member_group(c) && (c.appearance.has_positioned_gradient() || c.appearance.has_unplaced_gradient()) {
out.push((at.clone(), b, h));
}
at.pop();
// Only members measured in the bounds hold the group's gradients in place.
let counts = if *clip { i == 0 } else { i >= skip && (c.visible || !layer) };
if counts {
bounds = vectorcraft_geom::union_opt(bounds, b);
held |= h;
}
}
(bounds, held)
}
// Type's outlines: measured as a whole, with each outline that kept its shape counted.
NodeKind::Group { .. } | NodeKind::Layer { .. } => (n.geometric_bounds(), !kept.is_empty() && holds_kept(n, kept)),
_ => (n.geometric_bounds(), false),
}
}
/// Is a path or compound path below `n` (`n` included) in `kept`?
fn holds_kept(n: &Node, kept: &HashSet<NodeId>) -> bool {
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => kept.contains(&n.id),
_ => n.children().into_iter().flatten().any(|c| holds_kept(c, kept)),
}
}
/// Move the placed gradients of the own fills and strokes of each group in `after` (the groups of
/// `before`, measured again once the paths changed) by what `carry` gives for its bounds before
/// and after. A group with a path below it that kept its shape keeps them in place.
fn carry_groups(art: &mut Node, before: Vec<GroupMark>, after: Vec<GroupMark>, carry: &dyn Fn(Option<Rect>, Option<Rect>) -> crate::Carry) {
for ((at, from, _), (at_after, to, held)) in before.into_iter().zip(after) {
if held || at != at_after {
continue;
}
let k = carry(from, to);
if k.is_empty() {
continue;
}
if let Some(n) = node_at(art, &at)
&& is_member_group(n)
&& k.moves(&n.appearance)
{
k.apply(&mut n.appearance);
}
}
}
/// The node below `n` that the child indices `at` lead to.
fn node_at<'a>(n: &'a mut Node, at: &[usize]) -> Option<&'a mut Node> {
let Some((first, rest)) = at.split_first() else { return Some(n) };
let c = n.children_mut()?.get_mut(*first)?;
node_at(Arc::make_mut(c), rest)
}
/// Move and scale the placed gradients of each path under `n` that [`leaf_bounds`] lists from its
/// bounds in `before` (before its geometry changed) to its bounds now.
fn rebase_leaves(n: &mut Node, before: &mut std::vec::IntoIter<Option<Rect>>) {
if matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. }) {
if let Some(Some(from)) = before.next()
&& let Some(to) = n.geometric_bounds()
&& from != to
&& n.appearance.has_positioned_gradient()
{
follow_boxes(&mut n.appearance, from, to);
}
return;
}
if n.extras().type_outlines {
return;
}
if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } = &mut n.kind {
for c in children.iter_mut() {
if matches!(c.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } | NodeKind::Group { .. } | NodeKind::Layer { .. }) {
rebase_leaves(Arc::make_mut(c), before);
}
}
}
}
/// Map the placed gradients of `ap` from box `from` to box `to` through the map that takes one box
/// onto the other (strokes: their stroke-inflated boxes). The start and end go where
/// [`Appearance::rebase_gradients`] puts them, and a radial's ellipse and a linear's isolines are
/// mapped with them.
pub(crate) fn follow_boxes(ap: &mut Appearance, from: Rect, to: Rect) {
for i in &mut ap.items {
match i {
AppearanceItem::Fill(f) => {
if let vectorcraft_doc::color::Paint::Gradient(g) = &mut f.paint {
g.transform(box_map(from, to));
}
}
AppearanceItem::Stroke(s) => {
let m = box_map(s.paint_bounds(from), s.paint_bounds(to));
if let vectorcraft_doc::color::Paint::Gradient(g) = &mut s.paint {
g.transform(m);
}
}
}
}
}
/// The map that takes box `from` onto box `to` along each axis (along an axis `from` has no extent
/// on, a move from its center to `to`'s).
fn box_map(from: Rect, to: Rect) -> vectorcraft_geom::Affine {
let axis = |a0: f64, a1: f64, b0: f64, b1: f64| {
if (a1 - a0).abs() > 1e-12 {
let s = (b1 - b0) / (a1 - a0);
(s, b0 - a0 * s)
} else {
(1.0, (b0 + b1 - a0 - a1) / 2.0)
}
};
let (sx, tx) = axis(from.x0, from.x1, to.x0, to.x1);
let (sy, ty) = axis(from.y0, from.y1, to.y0, to.y1);
vectorcraft_geom::Affine::new([sx, 0.0, 0.0, sy, tx, ty])
}
/// Apply the Transform effect's map `m` to every image and symbol instance under `n`, as
/// [`for_each_leaf`] does to paths: the map goes into the object's placement, and `copies` more
/// follow, each mapped once more than the last. Without copies the object itself takes the map,
/// as a path does. The art of each one's linked opacity mask takes the map with `scaling`, as a
/// path's mask does.
fn transform_placed(n: &mut Node, m: Affine, copies: usize, scaling: vectorcraft_doc::Scaling) {
let mut room = crate::distort::MAX_COPY_OBJECTS.saturating_sub(placed_count(n));
copy_placed(n, m, copies, scaling, &mut room);
}
/// [`transform_placed`] whose copies make at most `max` image and symbol instance nodes of the art,
/// the originals included: each copy adds the object once more, and a Transform after this one
/// copies the lot again, so copies stop where the result would pass `max` (#2042).
#[cfg(test)]
pub(crate) fn transform_placed_within(n: &mut Node, m: Affine, copies: usize, max: usize) {
let mut room = max.saturating_sub(placed_count(n));
copy_placed(n, m, copies, vectorcraft_doc::Scaling::default(), &mut room);
}
/// Rows and columns of the pieces a Warp effect cuts an image into (VectorCraft's own number).
pub(crate) const WARP_IMAGE_CELLS: usize = 10;
/// A Warp effect's point map, shared by the images it bends.
pub(crate) type WarpMap = Rc<dyn Fn(Point) -> Point>;
/// One image a Warp effect bent: the image's own key, and each warp it took with the placement
/// the image had when it took it, in order.
type WarpedImage = (String, Vec<(WarpMap, Affine)>);
/// The images Warp effects bend while [`reshape_leaves_apart`] runs the effects. Each such image's
/// key is replaced by a key of this table ([`WarpedImages::add`]), which a copy a later Transform
/// effect makes keeps. The images are cut into pieces once, after the last effect
/// ([`cut_warped_images`]): the effects after a Warp reshape the image's frame, not each piece,
/// and a later Warp or Transform moves the one set of pieces.
pub(crate) struct WarpedImages {
/// What this table's keys start with: no image key in the art starts with it
/// ([`WarpedImages::reserve`]).
prefix: String,
/// The prefix was checked against the art's image keys.
reserved: bool,
images: Vec<WarpedImage>,
}
impl WarpedImages {
pub(crate) fn new() -> Self {
static TABLES: AtomicUsize = AtomicUsize::new(0);
Self { prefix: format!("\0warp{}:", TABLES.fetch_add(1, Ordering::Relaxed)), reserved: false, images: vec![] }
}
/// Before the first image of `art` is marked: lengthen the prefix until no image key in `art`
/// starts with it, so no key from the document is read as one of this table's.
pub(crate) fn reserve(&mut self, art: &Node) {
if self.reserved {
return;
}
self.reserved = true;
let mut keys = vec![];
art.walk(&mut |c| {
if let NodeKind::Image(im) = &c.kind {
keys.push(im.key.clone());
}
});
while keys.iter().any(|k| k.starts_with(self.prefix.as_str())) {
self.prefix.push(':');
}
}
/// What this table's keys start with.
#[cfg(test)]
pub(crate) fn prefix(&self) -> &str {
&self.prefix
}
/// The image that `key`, a key of this table, stands for.
fn get(&self, key: &str) -> Option<&WarpedImage> {
self.images.get(key.strip_prefix(self.prefix.as_str())?.parse::<usize>().ok()?)
}
/// Record that the image with key `key`, placed by `xf`, takes warp `f`; returns the image's
/// new key.
fn add(&mut self, key: &str, xf: Affine, f: &WarpMap) -> String {
let (own, mut warps) = self.get(key).cloned().unwrap_or_else(|| (key.to_string(), vec![]));
warps.push((f.clone(), xf));
self.images.push((own, warps));
format!("{}{}", self.prefix, self.images.len() - 1)
}
/// Does the art `n` have room, within `max` nodes, for the pieces of every image under it that
/// this table lists?
fn fits(&self, n: &Node, max: usize) -> bool {
let mut count = 0usize;
n.walk(&mut |c| {
if let NodeKind::Image(im) = &c.kind
&& self.get(&im.key).is_some()
{
count += 1;
}
});
vectorcraft_doc::live::mesh_warp_cost(WARP_IMAGE_CELLS).saturating_mul(count) <= max.saturating_sub(n.count())
}
/// The geometric bounds of `art` as it draws after the effects so far: each image this table
/// lists measured by the pieces the warps and moves it took would cut it into, when the art
/// has room for them ([`cut_warped_images`]).
pub(crate) fn bounds(&self, art: &Node) -> Option<Rect> {
if self.images.is_empty() || !self.fits(art, crate::distort::MAX_COPY_OBJECTS) {
return art.geometric_bounds();
}
// The pieces the images would be cut into now: each group of pieces is shown or hidden as
// its image is, and hidden objects are left out of a layer's bounds.
let mut probe = art.clone();
restore_warped_images(&mut probe, self, true);
probe.geometric_bounds()
}
/// The geometric bounds of image `n`: when this table lists it, measured by the pieces the
/// warps and moves it took would cut it into.
fn image_bounds(&self, n: &Node) -> Option<Rect> {
let NodeKind::Image(im) = &n.kind else { return n.geometric_bounds() };
if self.get(&im.key).is_none() {
return n.geometric_bounds();
}
let mut probe = Node::group(n.id, vec![Arc::new(n.clone())]);
restore_warped_images(&mut probe, self, true);
probe.geometric_bounds()
}
/// This table when the art `art` has room for the pieces of the images it lists, so that a
/// measure of the art takes them bent ([`WarpedImages::bounds`]); `None` otherwise.
fn when_cut(&self, art: &Node) -> Option<&Self> {
(!self.images.is_empty() && self.fits(art, crate::distort::MAX_COPY_OBJECTS)).then_some(self)
}
}
/// Record in `warped` that each image under `n` takes warp `f`, walking the groups and layers
/// [`for_each_leaf`] walks and counting the paths it passes in `at`. `kept` holds the places of
/// the paths that kept their shape (past the budget), in the order [`for_each_leaf`] visits the
/// paths of the art: an image in a clip group whose clipping art holds one (`held`) does not take
/// the warp.
pub(crate) fn mark_warped_images(n: &mut Node, f: &WarpMap, kept: &HashSet<usize>, held: bool, at: &mut usize, warped: &mut WarpedImages) {
let (NodeKind::Group { children, clip } | NodeKind::Layer { children, clip, .. }) = &mut n.kind else { return };
let mut held = held;
// A clip group's first member is its clipping art: when a path in it kept its shape, it and
// the members it clips keep theirs.
if *clip && let Some(first) = children.first() {
let start = *at;
held |= (start..start + leaf_count(first)).any(|p| kept.contains(&p));
}
for c in children.iter_mut() {
match &c.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => *at += 1,
NodeKind::Group { .. } | NodeKind::Layer { .. } => mark_warped_images(Arc::make_mut(c), f, kept, held, at, warped),
NodeKind::Image(_) if !held => {
if let NodeKind::Image(im) = &mut Arc::make_mut(c).kind {
im.key = warped.add(&im.key, im.xf, f);
}
}
_ => {}
}
}
}
/// Give each image under `n` that `warped` lists its own key back, and cut it into the pieces of
/// a raster mesh warp ([`vectorcraft_doc::live::mesh_warp_image`], each piece drawn as the image
/// is, smoothed or not) through the warps it took and the moves between and after them
/// ([`warped_map`]). The art keeps to [`crate::distort::MAX_COPY_OBJECTS`] nodes: when the pieces
/// would pass it, no image is cut. An image whose placement has no inverse is not cut either.
fn cut_warped_images(n: &mut Node, warped: &WarpedImages) {
cut_warped_images_within(n, warped, crate::distort::MAX_COPY_OBJECTS);
}
/// [`cut_warped_images`] with the art keeping to `max` nodes.
pub(crate) fn cut_warped_images_within(n: &mut Node, warped: &WarpedImages, max: usize) {
if warped.images.is_empty() {
return;
}
let cut = warped.fits(n, max);
restore_warped_images(n, warped, cut);
}
/// The walk of [`cut_warped_images`]: each listed image gets its key back, and with `cut` is cut
/// into pieces.
fn restore_warped_images(n: &mut Node, warped: &WarpedImages, cut: bool) {
let Some(children) = n.children_mut() else { return };
for c in children.iter_mut() {
let NodeKind::Image(im) = &c.kind else {
if c.children().is_some() {
restore_warped_images(Arc::make_mut(c), warped, cut);
}
continue;
};
let Some((key, warps)) = warped.get(&im.key) else { continue };
let mut image = (**c).clone();
let NodeKind::Image(own) = &mut image.kind else { continue };
own.key = key.clone();
// The pieces start from the image as it was placed when it took its first warp.
let first = warps.first().map(|(_, at)| ImageObject { xf: *at, ..(**own).clone() });
let map = warped_map(warps, own.xf);
*c = Arc::new(match (first, map) {
(Some(first), Some(map)) if cut => {
let pixelated = image.extras().pixelated;
vectorcraft_doc::live::mesh_warp_image(&image, &first, &*map, WARP_IMAGE_CELLS, pixelated, false)
}
_ => image,
});
}
}
/// The map that takes a point of an image placed by the first of `warps`' placements to where the
/// warps and the moves between them take it, for an image now placed by `placed`: each warp in
/// turn, then the move from the placement the image had when it took that warp to the one it had
/// at the next, or to `placed` after the last. `None` when a placement has no inverse.
fn warped_map(warps: &[(WarpMap, Affine)], placed: Affine) -> Option<Box<dyn Fn(Point) -> Point>> {
let mut steps: Vec<(WarpMap, Affine)> = Vec::with_capacity(warps.len());
for (i, (f, at)) in warps.iter().enumerate() {
let det = at.determinant();
if !det.is_finite() || det == 0.0 {
return None;
}
let next = warps.get(i + 1).map_or(placed, |(_, at)| *at);
let m = next * at.inverse();
if !m.is_finite() {
return None;
}
steps.push((f.clone(), m));
}
Some(Box::new(move |p| steps.iter().fold(p, |q, (f, m)| *m * f(q))))
}
/// How many paths [`for_each_leaf`] visits in `n`.
fn leaf_count(n: &Node) -> usize {
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => 1,
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => children.iter().map(|c| leaf_count(c)).sum(),
_ => 0,
}
}
/// An object that a Transform effect places through its transform rather than reshapes: an image
/// (pixel space to document) or a symbol instance (the symbol's art to document).
fn is_placed(n: &Node) -> bool {
matches!(n.kind, NodeKind::Image(_) | NodeKind::SymbolInstance { .. })
}
/// The walk of [`transform_placed`]: the map and its copies go to each image and symbol instance in
/// document order, each copy taking one from `room` (what the art can still grow by), until the
/// budget is full. As with a path, the copies stop before one reaches past
/// [`crate::distort::within_reach`].
fn copy_placed(n: &mut Node, m: Affine, copies: usize, scaling: vectorcraft_doc::Scaling, room: &mut usize) {
let Some(children) = n.children_mut() else { return };
for c in children.iter_mut() {
if !is_placed(c) {
copy_placed(Arc::make_mut(c), m, copies, scaling, room);
continue;
}
let src = (**c).clone();
let placed = |xf: Affine| {
let mut one = src.clone();
// Gradients fitted to it are fixed to their fit first when a refit would not match.
if crate::geometry::fixes_fit(xf) {
one.pin_gradients();
}
match &mut one.kind {
NodeKind::Image(im) => im.xf = xf * im.xf,
NodeKind::SymbolInstance { xf: at, .. } => *at = xf * *at,
_ => {}
}
// The placed gradients of its own fills and strokes move with it, as a path's do.
if xf != Affine::IDENTITY && one.appearance.has_positioned_gradient() {
one.appearance.transform_gradients(xf);
}
// So does the art of its linked opacity mask.
if xf != Affine::IDENTITY
&& let Some(m) = one.extras_if_any().and_then(|x| x.mask.as_mut()).filter(|m| m.linked)
{
Arc::make_mut(&mut m.art).transform(xf, scaling);
}
one
};
if copies == 0 {
*c = Arc::new(placed(m));
continue;
}
// The copies become nodes of their own, under a group with the object's id, name and place
// in the art (as the runs an outlined text gets).
let mut placements: Vec<Arc<Node>> = vec![Arc::new(placed(Affine::IDENTITY))];
let mut xf = Affine::IDENTITY;
for _ in 0..copies.min(*room) {
xf *= m;
let copy = placed(xf);
if !copy.geometric_bounds().is_some_and(crate::distort::within_reach) {
break;
}
placements.push(Arc::new(copy));
}
*room -= placements.len() - 1;
// The object stays on top of its copies, as a path does.
placements.reverse();
let mut g = Node::group(src.id, placements);
g.name = src.name.clone();
*c = Arc::new(g);
}
}
/// How many images and symbol instances `n` holds below itself.
fn placed_count(n: &Node) -> usize {
n.children().into_iter().flatten().map(|c| usize::from(is_placed(c)) + placed_count(c)).sum()
}
/// `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, doc, n, &outline_text, &SymbolNodes::default())
}
/// [`outlined_members`] with live objects and symbol instances given as art through `outline`
/// ([`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
}
/// Run `f` on every path and compound path under `n` (guides skipped).
fn for_each_leaf(n: &mut Node, f: &mut dyn FnMut(&mut Node)) {
match &mut n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => f(n),
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
for c in children.iter_mut() {
for_each_leaf(Arc::make_mut(c), f);
}
}
_ => {}
}
}
/// What the paths under `n` that [`for_each_leaf`] visits cost together, as [`leaf_geometry`]
/// gives them.
fn art_cost(n: &Node) -> Budget {
let none = Budget { anchors: 0, lines: 0 };
match &n.kind {
NodeKind::Path { guide: false, path, .. } => Budget::of(path),
NodeKind::Compound { children, .. } => children.iter().filter_map(|c| c.path_data()).map(Budget::of).fold(none, Budget::plus),
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => children.iter().map(|c| art_cost(c)).fold(none, Budget::plus),
_ => none,
}
}
pub(crate) fn leaf_geometry(n: &Node) -> Option<(PathData, FillRule)> {
match &n.kind {
NodeKind::Path { path, rule, .. } => Some((path.clone(), *rule)),
NodeKind::Compound { children, rule } => {
Some((PathData::new(children.iter().filter_map(|c| c.path_data()).flat_map(|p| p.subpaths.iter().cloned()).collect()), *rule))
}
_ => None,
}
}
/// Replace the geometry of path or compound `n` (a compound when `path` has several subpaths).
fn set_geometry(n: &mut Node, path: PathData, rule: FillRule) {
let clipping = matches!(n.kind, NodeKind::Path { clipping: true, .. });
n.kind = if path.subpaths.len() > 1 {
let id = n.id;
let children = path.subpaths.into_iter().map(|sp| Arc::new(Node::path(id, PathData::single(sp), Appearance::default()))).collect();
NodeKind::Compound { children, rule }
} else {
NodeKind::Path { path, rule, live: None, clipping, guide: false }
};
}