Files
vectorcraft/crates/effects/src/bake.rs
T
dotCooCoo 732434b4e5 Effects: placed gradients move with Transform and Free Distort effects
A gradient placed with the Gradient tool stayed where it was when the
object's Transform or Free Distort effect moved, turned or scaled the
outline, so the shape slid across its own gradient.

The Transform effect now maps placed gradients by its own transform,
and each of its copies takes them as many times as it takes the
transform. Under Free Distort, each path's placed gradients move and
scale from the path's bounds before the effect to its bounds after it.
This change does not alter gradients fitted to the object.

The canvas, Expand Appearance and export, Pathfinder and compound
shapes, groups, a fill's or stroke's own effects, and Object › Expand
of a gradient fill apply the same rule. In a group, the own fills and
strokes of the group, of the groups inside it and of a symbol's art
follow it too, and images and symbol instances that a group's
Transform effect moves take the placed gradients of their own fills
and strokes along. A group whose path keeps its shape because the
effect would pass the geometry budget keeps its own gradients in
place.

This changes how existing documents draw: a gradient placed on a path,
or on an image's or symbol instance's own fill, inside a group with a
Transform effect now moves with it. Before, only the gradients of type
moved (#2214).

How gradients follow each effect is VectorCraft's own rule. The
Distort & Transform help page describes what the effects do to the
object:
https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html
2026-11-09 00:34:12 +07:00

651 lines
29 KiB
Rust

//! Baking live geometry effects for export.
//!
//! SVG, PDF and the clipboard have no notion of Illustrator's live effects, so exporters receive a
//! copy of the document in which every geometry effect — object level, per fill/stroke and
//! Effect → Pathfinder, and those on type, images, symbol instances and live objects (through their
//! outlines, [`reshape`]) — is evaluated exactly like the renderer does, and so are the own fills,
//! strokes and geometry effects of groups and layers ([`evaluate_container`]). Raster effects
//! (shadows, glows, blur) stay on the objects for the exporter to translate (e.g. SVG filters).
//! Colour adjustments recolour the objects ([`adjust_in_document`]; recoloured embedded images
//! become images of their own).
//!
//! Type that Transform effects on it, or on its group or layer, only move and copy stays type in
//! each place when its characters are in solid colors and the copies do not overlap ([`moved_type`]
//! and [`container_keeping_type`] list the other conditions). The renderer still draws its outlines.
//!
//! Object → Expand Appearance goes further ([`expand_leaf`]): every fill and stroke becomes an
//! object of its own, a stroke a path stroked by it alone (brushed strokes their brush art, made by
//! the caller).
use std::collections::HashMap;
use std::sync::Arc;
use vectorcraft_doc::color::Paint;
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, FillLayer, Knockout, Node, NodeKind, StrokeLayer};
use vectorcraft_geom::{Affine, FillRule, PathData, Rect, Vec2};
use crate::distort::{transform_move, transform_offsets};
use crate::group::{has_own_paint, paints};
use crate::reshape::{as_art, geometry_effects, glyph_outlines, outline_nested_text, reshape_leaves};
use crate::{
GeomContext, adjust_in_document, apply_geometry_carrying, crop_marks_art, evaluate_container, has_adjustment, has_crop_marks, has_geometry,
has_item_geometry, has_pathfinder, is_geometry, is_pathfinder, merged_params, needs_outline, outline_text, reshape,
};
fn item_effects(item: &AppearanceItem) -> &[vectorcraft_doc::Effect] {
match item {
AppearanceItem::Fill(f) => &f.effects,
AppearanceItem::Stroke(s) => &s.effects,
}
}
fn clear_item_effects(item: &mut AppearanceItem) {
match item {
AppearanceItem::Fill(f) => f.effects.retain(|e| !is_geometry(&e.id)),
AppearanceItem::Stroke(s) => s.effects.retain(|e| !is_geometry(&e.id)),
}
}
/// Does anything in `n`'s subtree need baking?
pub fn needs_bake(n: &Node) -> bool {
crate::has_revolve(n)
|| crate::has_solid(n)
|| has_adjustment(n)
|| has_crop_marks(n)
|| crate::addons::halftone::has_halftone(n)
|| has_pathfinder(n)
|| has_own_paint(n)
|| has_geometry(&n.appearance.effects)
|| n.projection().is_some()
|| n.appearance.items.iter().any(|i| has_geometry(item_effects(i)))
|| n.children().is_some_and(|ch| ch.iter().any(|c| needs_bake(c)))
}
fn geometry(n: &Node) -> Option<(PathData, FillRule)> {
match &n.kind {
NodeKind::Path { path, rule, guide: false, .. } => 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,
}
}
/// `path` with the geometry effects of `effects` applied, and how gradients follow them
/// ([`crate::Carry`]).
fn apply(effects: &[vectorcraft_doc::Effect], path: &PathData, ctx: &GeomContext) -> (PathData, crate::Carry) {
if !has_geometry(effects) || path.is_empty() {
return (path.clone(), crate::Carry::default());
}
let b = vectorcraft_geom::Shape::bounding_box(&path.to_bezpath());
apply_geometry_carrying(effects, path, b, ctx)
}
/// The geometry `g` with the own effects of `item` (one of the fills and strokes `ctx` is the
/// context of) applied, and `carried` (`item` as the object's effects left it) with its gradient
/// moved along with them.
fn apply_item(item: &AppearanceItem, carried: &AppearanceItem, g: &PathData, ctx: &GeomContext) -> (PathData, AppearanceItem) {
let (ig, carry) = apply(item_effects(item), g, &ctx.item(item));
let mut ap = Appearance { items: vec![carried.clone()], ..Default::default() };
if !carry.moves(&ap) {
return (ig, carried.clone());
}
carry.apply(&mut ap);
(ig, ap.items.pop().unwrap_or_else(|| carried.clone()))
}
/// A path node of `kind`'s flavour (compound when the result has several subpaths).
fn path_kind(d: &mut Document, path: PathData, rule: FillRule) -> NodeKind {
if path.subpaths.len() > 1 {
let children = path.subpaths.into_iter().map(|sp| Arc::new(Node::path(d.alloc_id(), PathData::single(sp), Appearance::default()))).collect();
NodeKind::Compound { children, rule }
} else {
NodeKind::Path { path, rule, live: None, clipping: false, guide: false }
}
}
/// `n` with its geometry effects evaluated (`None` = unchanged).
fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
// A cancelled export leaves the rest of the document as it is: the caller drops the result.
if !needs_bake(n) || vectorcraft_doc::cancel::is_cancelled() {
return None;
}
// Colour adjustments: the object recoloured (inside it too), then baked as it is.
if has_adjustment(n)
&& let Some(m) = adjust_in_document(d, n)
{
return Some(bake_node(d, &m).unwrap_or(m));
}
// Crop marks: the object and its marks.
if let Some(m) = crop_marks_art(n) {
return Some(bake_pieces(d, m));
}
// Vector Halftone: the object as its dots.
if let Some(m) = crate::addons::halftone::halftone_art(d, n) {
return Some(bake_pieces(d, m));
}
if let Some(m) = crate::revolve_art(n).or_else(|| crate::solid_art(n)) {
let m = evaluate_container(Some(d), &m).unwrap_or(m);
return Some(bake_pieces(d, m));
}
// Type, images, symbol instances and live objects: reshaped through their outlines, and so are
// type whose own fills or strokes have geometry effects and images whose own fills or strokes
// paint a shape ([`has_item_geometry`]). Type that Transform effects only move and copy stays
// type, moved to each place.
if needs_outline(n) && (has_geometry(&n.appearance.effects) || n.projection().is_some() || has_item_geometry(n)) {
if let Some(m) = moved_type_group(n) {
return Some(bake_pieces(d, m));
}
let symbol = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => {
d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n))
}
_ => None,
};
let m = reshape(Some(d), n, symbol.as_ref())?;
return Some(bake_node(d, &m).unwrap_or(m));
}
// A path whose Transform effect copies it with a gradient, or scales its strokes or moves its
// patterns: each copy an object of its own, so its gradient fits it rather than spanning
// every copy and its strokes and patterns can differ from the others'.
if crate::has_transform_art(n)
&& let Some(m) = reshape(Some(d), n, None)
{
return Some(bake_pieces(d, m));
}
// Groups and layers: Pathfinder, geometry effects and their own fills and strokes (type that
// Transform effects only move stays type). The new pieces (results, outlines, copies, the
// fills' and strokes' art) get ids of their own.
if let Some(m) = container_keeping_type(d, n).or_else(|| evaluate_container(Some(d), n)) {
return Some(bake_pieces(d, m));
}
if let Some(ch) = n.children() {
if matches!(n.kind, NodeKind::Compound { .. })
&& (has_geometry(&n.appearance.effects) || n.appearance.items.iter().any(|i| has_geometry(item_effects(i))))
{
return bake_leaf(d, n);
}
let mut m = n.clone();
let baked: Vec<Arc<Node>> = ch.iter().map(|c| bake_node(d, c).map(Arc::new).unwrap_or_else(|| c.clone())).collect();
if let Some(ch) = m.children_mut() {
*ch = baked;
}
return Some(m);
}
bake_leaf(d, n)
}
/// `m`, art evaluated from an object, with its children baked and then given ids of their own (the
/// pieces copied from the source keep the source's ids until then). The pieces are baked with the
/// ids the renderer draws them with: Random in a Transform effect takes its seed from the id.
fn bake_pieces(d: &mut Document, mut m: Node) -> Node {
let mut seen = std::collections::HashSet::from([m.id]);
let children = m.children().cloned().unwrap_or_default();
let children = children
.into_iter()
.map(|c| {
let c = Arc::unwrap_or_clone(c);
let mut c = bake_node(d, &c).unwrap_or(c);
fresh_ids(d, &mut c, &mut seen);
Arc::new(c)
})
.collect();
if let Some(ch) = m.children_mut() {
*ch = children;
}
m
}
/// Give `n` and its descendants a new id wherever one was already `seen` (the pieces evaluated art
/// copies from its source keep ids of their own).
pub fn fresh_ids(d: &mut Document, n: &mut Node, seen: &mut std::collections::HashSet<vectorcraft_doc::NodeId>) {
if !seen.insert(n.id) {
n.id = d.alloc_id();
seen.insert(n.id);
}
for c in n.children_mut().into_iter().flatten() {
fresh_ids(d, Arc::make_mut(c), seen);
}
}
/// Most copies of one type object that stay type, the original included ([`moved_type`]).
const MAX_TYPE_COPIES: usize = 10_000;
/// Most bytes that the copies of one type object that stay type take together in the document's
/// JSON form, the original included ([`moved_type`]): its text, frame or path, wrap shapes and the
/// rest of the object, once per copy.
const MAX_TYPE_COPY_BYTES: usize = 20_000_000;
/// The params of geometry effects `fx` when there are some and they are all Transform effects that
/// only move ([`transform_move`]), in order; `None` otherwise.
fn transform_moves(fx: &[&Effect]) -> Option<Vec<serde_json::Value>> {
if fx.is_empty() {
return None;
}
fx.iter().map(|e| (e.id == "distort.transform").then(|| merged_params(&e.id, &e.params)).filter(|p| transform_move(p).is_some())).collect()
}
/// Type `n` in each place that Transform effects that only move ([`transform_moves`]) copy its
/// glyph outlines to: `n` moved there, the original's place first. `None` for other objects; for
/// type that is hidden, in perspective, has effects, transparency, an opacity mask or fills and
/// strokes of its own (beyond its characters'), has characters painted with a gradient or pattern,
/// underlined or struck through, has character strokes and is scaled or sheared, holds inline
/// graphics or has no glyphs; when the effects would stop copies of its outlines early
/// ([`transform_offsets`]); when the copies would pass [`MAX_TYPE_COPIES`] or
/// [`MAX_TYPE_COPY_BYTES`]; and when the copies overlap: what one copy paints (its
/// character strokes included) reaches into another's box.
fn moved_type(n: &Node, moves: &[serde_json::Value]) -> Option<Vec<Node>> {
let NodeKind::Text(t) = &n.kind else { return None };
let flat = |p: &Paint| matches!(p, Paint::None | Paint::Solid { .. });
let [a, b, c, d, _, _] = t.xf.as_coeffs();
let rigid = (a * a + b * b - 1.0).abs() < 1e-9 && (c * c + d * d - 1.0).abs() < 1e-9 && (a * c + b * d).abs() < 1e-9;
let stroked = t.runs.iter().any(|r| r.style.has_stroke());
if !n.visible
|| (stroked && !rigid)
|| n.projection().is_some()
|| !n.has_default_transparency()
|| !n.appearance.effects.is_empty()
|| n.appearance.items.iter().any(|i| paints(i) || !item_effects(i).is_empty())
|| t.runs.iter().any(|r| r.inline.is_some() || r.style.underline || r.style.strikethrough || !flat(&r.style.fill) || !flat(&r.style.stroke))
{
return None;
}
let (_, all) = glyph_outlines(t);
let outline = PathData::from_bezpath(&all).transformed(t.xf);
let ink = outline.bounds()?;
let offsets = transform_offsets(moves, &outline)?;
if offsets.len() > MAX_TYPE_COPIES {
return None;
}
let size = serde_json::to_vec(n).ok()?.len();
if size.saturating_mul(offsets.len()) > MAX_TYPE_COPY_BYTES {
return None;
}
let strokes = t.runs.iter().map(|r| r.style.appearance().outset()).fold(0.0, f64::max);
if !apart(&offsets, ink.inflate(strokes, strokes)) {
return None;
}
Some(
offsets
.into_iter()
.map(|o| {
let mut c = n.clone();
c.transform(Affine::translate(o), false);
c
})
.collect(),
)
}
/// Do the copies of box `b` moved by each of `offsets` stay clear of each other? Copies may touch.
/// False as well when an offset is 10^15 or more times the box's width or height.
fn apart(offsets: &[Vec2], b: Rect) -> bool {
let (w, h) = (b.width(), b.height());
if offsets.len() < 2 {
return true;
}
if !(w > 0.0 && h > 0.0 && w.is_finite() && h.is_finite()) {
return false;
}
// Copies closer than `w` across and `h` down lie in the same or a neighboring cell.
let cell = |o: Vec2| {
let (x, y) = ((o.x / w).floor(), (o.y / h).floor());
(x.abs() < 1e15 && y.abs() < 1e15).then_some((x as i64, y as i64))
};
let mut cells: HashMap<(i64, i64), Vec2> = HashMap::new();
for &o in offsets {
let Some((x, y)) = cell(o) else { return false };
for dx in -1..=1 {
for dy in -1..=1 {
if let Some(p) = cells.get(&(x + dx, y + dy))
&& (p.x - o.x).abs() < w
&& (p.y - o.y).abs() < h
{
return false;
}
}
}
cells.insert((x, y), o);
}
true
}
/// Type `n` whose own geometry effects only move it ([`transform_moves`]) as a group of the moved
/// type ([`moved_type`]). The group keeps `n`'s id, name, visibility, transparency, opacity mask and
/// remaining (raster) effects, and the type in it has none of them. `None` otherwise.
fn moved_type_group(n: &Node) -> Option<Node> {
if !matches!(n.kind, NodeKind::Text(_)) {
return None;
}
let moves = transform_moves(&geometry_effects(n))?;
let mut plain = n.clone();
plain.visible = true;
plain.opacity = 1.0;
plain.blend = Default::default();
plain.isolate = false;
plain.knockout_shape = false;
plain.extras_mut().mask = None;
plain.appearance.effects.clear();
let copies = moved_type(&plain, &moves)?;
let mut g = Node::group(n.id, copies.into_iter().map(Arc::new).collect());
g.name = n.name.clone();
g.visible = n.visible;
g.opacity = n.opacity;
g.blend = n.blend;
g.isolate = n.isolate;
// One element: its pieces never knock each other out.
g.knockout = Knockout::Off;
g.knockout_shape = n.knockout_shape;
g.extras_mut().mask = n.extras().mask.clone();
g.appearance.effects = n.appearance.effects.iter().filter(|e| !is_geometry(&e.id)).cloned().collect();
Some(g)
}
/// `n`, a group or layer without Pathfinder effects or fills and strokes that paint, whose geometry
/// effects only move its members ([`transform_moves`]), as plain art in which the type it holds
/// stays type: each type object moved, or replaced by a group of its copies ([`moved_type`]).
/// Type in a clipping path, other type that [`moved_type`] doesn't copy, and type inside live
/// objects or inside groups with Pathfinder or geometry effects or fills and strokes that paint, is
/// outlined as [`evaluate_container`] outlines it, and the other members become what it makes of
/// them. `None` when no type stays type.
fn container_keeping_type(d: &Document, n: &Node) -> Option<Node> {
fn keep(n: &mut Node, moves: &[serde_json::Value], clipping: bool, kept: &mut 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);
if !matches!(c.kind, NodeKind::Text(_)) {
if has_pathfinder(c) || has_own_paint(c) || !geometry_effects(c).is_empty() {
outline_nested_text(Arc::make_mut(c), clipping);
} else if c.children().is_some() {
keep(Arc::make_mut(c), moves, clipping, kept);
}
continue;
}
// Type that isn't copied is outlined as `outline_nested_text` outlines it.
let copies = if clipping { None } else { moved_type(c, moves) };
let art = match copies {
Some(mut copies) => {
*kept = true;
match copies.pop() {
Some(only) if copies.is_empty() => Some(only),
last => Some(Node::group(c.id, copies.into_iter().chain(last).map(Arc::new).collect())),
}
}
None if clipping => outline_text(c).map(|o| Node { visible: c.visible, ..o }),
None => as_art(None, c, None),
};
if let Some(o) = art {
*c = Arc::new(o);
}
}
}
if !matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || has_pathfinder(n) || has_own_paint(n) {
return None;
}
let fx = geometry_effects(n);
let moves = transform_moves(&fx)?;
let mut text = false;
n.walk(&mut |c| text |= matches!(c.kind, NodeKind::Text(_)));
if !text {
return None;
}
let mut m = crate::reshape::evaluated(Some(d), n);
let mut kept = false;
keep(&mut m, &moves, false, &mut kept);
if !kept {
return None;
}
reshape_leaves(&mut m, &fx);
m.appearance.effects.retain(|e| !is_pathfinder(&e.id) && !is_geometry(&e.id));
m.appearance.items.clear();
m.appearance.contents_index = None;
Some(m)
}
/// A new path of `n`'s (its geometry `path`, filled under `rule`) painted by `item` alone, with
/// none of `n`'s transparency.
fn piece(d: &mut Document, n: &Node, path: PathData, rule: FillRule, item: AppearanceItem) -> Node {
let id = d.alloc_id();
let kind = path_kind(d, path, rule);
let mut out = n.clone();
out.id = id;
out.name = None;
out.appearance = Appearance { items: vec![item], ..Default::default() }.into();
out.opacity = 1.0;
out.blend = Default::default();
out.isolate = false;
out.knockout = Default::default();
out.knockout_shape = false;
out.kind = kind;
let mut extras = out.extras_mut();
extras.mask = None;
extras.trace = None;
extras.wrap = None;
extras.graph = None;
// The group keeps the URL (one link around the pieces).
extras.attrs = None;
drop(extras);
out
}
/// `n`'s geometry with its object-level geometry effects applied, its fill rule, the context of
/// its effects, and `n` without those effects, its gradients moved along with them.
fn leaf_base(n: &Node) -> Option<(PathData, FillRule, GeomContext<'_>, Node)> {
let (base, rule) = geometry(n)?;
let ctx = GeomContext::of(n);
let mut m = n.clone();
let (g, carry) = apply(&n.appearance.effects, &base, &ctx);
if carry.moves(&m.appearance) {
carry.apply(&mut m.appearance);
}
m.appearance.effects.retain(|e| !is_geometry(&e.id));
Some((g, rule, ctx, m))
}
fn bake_leaf(d: &mut Document, n: &Node) -> Option<Node> {
let (g, rule, ctx, mut m) = leaf_base(n)?;
if !n.appearance.items.iter().any(|i| has_geometry(item_effects(i))) {
m.kind = path_kind(d, g, rule);
return Some(m);
}
// Per-item geometry: one path per fill/stroke, grouped under the object's transparency and
// raster effects.
let children = n
.appearance
.items
.iter()
.zip(&m.appearance.items)
.map(|(item, carried)| {
let (ig, mut it) = apply_item(item, carried, &g, &ctx);
clear_item_effects(&mut it);
Arc::new(piece(d, n, ig, rule, it))
})
.collect();
m.appearance.items.clear();
m.kind = NodeKind::Group { children, clip: false };
Some(m)
}
/// Turns one brushed stroke of a path (the path after its geometry effects, its fill rule, the
/// stroke without geometry effects) into its brush art for [`expand_leaf`]. `None` when it paints
/// nothing.
pub type StrokeArt<'a> = &'a mut dyn FnMut(&mut Document, &PathData, FillRule, &StrokeLayer) -> Option<Node>;
/// Object → Expand Appearance of a path or compound path `n`: its geometry effects applied, every
/// visible fill becomes a copy of the path painted by that fill alone and every visible stroke a
/// copy stroked by that stroke alone (its weight, caps, joins, dashes, arrowheads, alignment and
/// width profile stay stroke attributes, which Object → Expand or Outline Stroke outline later,
/// #3089), or for a brushed stroke the brush art `stroke_art` makes of it; each with its fill's or
/// stroke's opacity and blend mode as its own, in paint order, as the members of a group that keeps
/// `n`'s id, name, transparency, opacity mask and remaining (raster) effects. A single piece
/// without transparency or effects of its own takes `n`'s place itself. Hidden fills, strokes and
/// effects are dropped. `None` for other kinds of objects.
pub fn expand_leaf(d: &mut Document, n: &Node, stroke_art: StrokeArt) -> Option<Node> {
if let Some(m) = crate::revolve::expand_revolve_art(n).or_else(|| crate::solid_art(n)) {
let m = evaluate_container(Some(d), &m).unwrap_or(m);
return Some(bake_pieces(d, m));
}
let (g, rule, ctx, mut m) = leaf_base(n)?;
m.appearance.effects.retain(|e| e.visible);
let mut pieces = vec![];
for (item, carried) in n.appearance.items.iter().zip(&m.appearance.items).filter(|(i, _)| paints(i)) {
let (ig, mut it) = apply_item(item, carried, &g, &ctx);
clear_item_effects(&mut it);
it.effects_mut().retain(|e| e.visible);
let p = match &it {
AppearanceItem::Stroke(st) if st.brush.is_some() => stroke_art(d, &ig, rule, st),
_ => Some(piece(d, n, ig, rule, it)),
};
pieces.extend(p.map(hoist_transparency));
}
m.appearance.items.clear();
m.appearance.contents_index = None;
match pieces.as_slice() {
[one] if m.appearance.effects.is_empty() && one.has_default_transparency() && one.appearance.effects.is_empty() => {
m.kind = one.kind.clone();
m.appearance = one.appearance.clone();
}
_ => m.kind = NodeKind::Group { children: pieces.into_iter().map(Arc::new).collect(), clip: false },
}
Some(m)
}
/// `p` with the opacity and blend mode of its only fill or stroke as its own (the same look, and a
/// basic appearance).
fn hoist_transparency(mut p: Node) -> Node {
if p.has_default_transparency()
&& let [item] = p.appearance.items.as_mut_slice()
{
let (opacity, blend) = match item {
AppearanceItem::Fill(f) => (&mut f.opacity, &mut f.blend),
AppearanceItem::Stroke(s) => (&mut s.opacity, &mut s.blend),
};
p.opacity = std::mem::replace(opacity, 1.0);
p.blend = std::mem::take(blend);
}
p
}
/// [`expand_leaf`] every path and compound path in `n` (art made for Expand Appearance, such as a
/// group's own fills and strokes or outlined type), in place.
pub fn expand_art(d: &mut Document, n: &mut Node, stroke_art: StrokeArt) {
if matches!(n.kind, NodeKind::Path { clipping: false, guide: false, .. } | NodeKind::Compound { .. }) {
if let Some(m) = expand_leaf(d, n, stroke_art) {
*n = m;
}
return;
}
for c in n.children_mut().into_iter().flatten() {
expand_art(d, Arc::make_mut(c), stroke_art);
}
}
/// Envelope Options → Distort Appearance: path or compound path `n` as plain filled art that an
/// envelope bends as a whole: its geometry effects applied, every visible fill a copy of the path
/// painted by that fill and every visible stroke without a brush its filled outline in the
/// stroke's paint, opacity, blend mode and raster effects (a gradient along or across the stroke:
/// its gradient meshes clipped to the outline; a brushed stroke stays a stroke), in paint order, as
/// the members of a group keeping `n`'s id, transparency, opacity mask and raster effects. A path
/// with [`has_transform_art`](crate::has_transform_art) is first made into the art [`reshape`]
/// makes of it, and each copy is baked this way. `None` when it has neither a stroke to outline nor
/// a geometry effect, so bending first changes nothing.
pub fn bake_appearance(n: &Node) -> Option<Node> {
if let Some(m) = crate::revolve_art(n).or_else(|| crate::solid_art(n)) {
return Some(m);
}
if !matches!(n.kind, NodeKind::Path { clipping: false, guide: false, .. } | NodeKind::Compound { .. }) {
return None;
}
// A path whose Transform effect copies it with a gradient, or scales its strokes or moves its
// patterns: the art Expand Appearance makes of it (each copy an object with its own strokes,
// patterns and gradients), each piece baked in turn.
if crate::has_transform_art(n)
&& let Some(mut m) = reshape(None, n, None)
{
bake_appearance_pieces(&mut m);
return Some(m);
}
let outlined = |i: &AppearanceItem| paints(i) && matches!(i, AppearanceItem::Stroke(s) if s.brush.is_none());
let geometric = has_geometry(&n.appearance.effects) || n.appearance.items.iter().any(|i| has_geometry(item_effects(i)));
if !geometric && !n.appearance.items.iter().any(outlined) {
return None;
}
let (g, rule, ctx, mut m) = leaf_base(n)?;
let piece = |path: PathData, rule: FillRule, item: AppearanceItem| {
let mut p = Node::path(n.id, path, Appearance { items: vec![item], ..Default::default() });
if let NodeKind::Path { rule: r, .. } = &mut p.kind {
*r = rule;
}
Arc::new(p)
};
let mut pieces = vec![];
for (item, carried) in n.appearance.items.iter().zip(&m.appearance.items).filter(|(i, _)| paints(i)) {
let (ig, mut it) = apply_item(item, carried, &g, &ctx);
clear_item_effects(&mut it);
pieces.push(match &it {
AppearanceItem::Stroke(st) if st.brush.is_none() => {
let outline = crate::stroke::outline_region(&ig, rule, st);
match st.path_gradient() {
// A gradient along or across the stroke: its gradient meshes clipped to the outline.
Some(g) => {
let mut clip = Node::path(n.id, outline, Appearance::default());
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
*clipping = true;
}
let meshes = crate::stroke::gradient_meshes(&ig.to_bezpath(), rule, st, &g.gradient);
let meshes = meshes.into_iter().map(|m| Node::new(n.id, NodeKind::Mesh(m)));
let children = std::iter::once(clip).chain(meshes).map(Arc::new).collect();
let mut group = Node::new(n.id, NodeKind::Group { children, clip: true });
(group.opacity, group.blend) = (st.opacity, st.blend);
group.appearance.effects = st.effects.clone();
Arc::new(group)
}
None => {
let effects = st.effects.clone();
let fill = FillLayer { opacity: st.opacity, blend: st.blend, effects, ..FillLayer::new(st.paint.stroke_as_fill()) };
piece(outline, FillRule::NonZero, AppearanceItem::Fill(fill))
}
}
}
_ => piece(ig, rule, it),
});
}
m.appearance.items.clear();
m.appearance.contents_index = None;
m.kind = NodeKind::Group { children: pieces, clip: false };
Some(m)
}
/// [`bake_appearance`] every path and compound path in `n` that it changes, in place.
fn bake_appearance_pieces(n: &mut Node) {
if matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) {
if let Some(m) = bake_appearance(n) {
*n = m;
}
return;
}
for c in n.children_mut().into_iter().flatten() {
bake_appearance_pieces(Arc::make_mut(c));
}
}
/// A copy of `doc` ready to export: every live geometry effect baked into plain paths, and the
/// resources of its placed documents' art added ([`Document::with_placed_art`]); `None` when
/// there is nothing to do (export it as is).
pub fn bake_document(doc: &Document) -> Option<Document> {
let placed = doc.with_placed_art();
if !placed.layers.iter().any(|l| needs_bake(l)) {
return match placed {
std::borrow::Cow::Owned(d) => Some(d),
std::borrow::Cow::Borrowed(_) => None,
};
}
let mut d = placed.into_owned();
let layers = d.layers.clone();
d.layers = layers.iter().map(|l| bake_node(&mut d, l).map(Arc::new).unwrap_or_else(|| l.clone())).collect();
Some(d)
}
#[cfg(test)]
mod tests;