mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
A path with a Transform effect that makes copies and a gradient was still merged into one path by bake_leaf and expand_leaf, so the gradient spanned every copy. Route it through reshape / expand_outlined like the renderer does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
533 lines
24 KiB
Rust
533 lines
24 KiB
Rust
//! 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. 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.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, Node, NodeKind};
|
|
use vectorcraft_geom::{Affine, FillRule, PathData, Rect, shapes};
|
|
|
|
use crate::{GeomContext, 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_with_gradients`]), as a group's copies do.
|
|
pub fn has_gradient_copies(n: &Node) -> bool {
|
|
matches!(n.kind, NodeKind::Path { guide: false, clipping: false, .. } | NodeKind::Compound { .. })
|
|
&& (n.appearance.has_unplaced_gradient() || n.appearance.has_placed_gradient())
|
|
&& 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` type without inline graphics that has a fill or stroke of its own that paints and has a
|
|
/// visible geometry effect? Such an effect reshapes the whole outline that its fill or stroke
|
|
/// paints ([`outline_text`], which leaves inline graphics out).
|
|
pub fn has_item_geometry(n: &Node) -> bool {
|
|
let NodeKind::Text(t) = &n.kind else { return false };
|
|
t.runs.iter().all(|r| r.inline.is_none()) && n.appearance.items.iter().any(|i| crate::paints(i) && crate::has_geometry(i.effects()))
|
|
}
|
|
|
|
/// 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`] and the bounds of `t`'s layout in text space.
|
|
fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath, Rect) {
|
|
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();
|
|
for g in &layout.glyphs {
|
|
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());
|
|
}
|
|
(runs, all, layout.bounds)
|
|
}
|
|
|
|
/// Type as glyph outlines in document space: one path per run with the run's fill and stroke,
|
|
/// 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) = 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>> = runs
|
|
.iter()
|
|
.zip(&t.runs)
|
|
.filter(|(bp, _)| !bp.elements().is_empty())
|
|
.map(|(bp, run)| {
|
|
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);
|
|
Arc::new(piece)
|
|
})
|
|
.collect();
|
|
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)
|
|
}
|
|
|
|
/// `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. `None` for other kinds (images, paths, containers).
|
|
pub fn outline_art(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|
let hook: &dyn Fn(&Node) -> Option<Node> = &outline_text;
|
|
let mut art = match &n.kind {
|
|
NodeKind::Text(_) => outline_text(n)?,
|
|
NodeKind::SymbolInstance { xf, .. } => {
|
|
let mut a = symbol_art?.clone();
|
|
a.transform(*xf, false);
|
|
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_gradient_copies(n) => {
|
|
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)])
|
|
}
|
|
_ if vectorcraft_doc::live::is_live(n) => vectorcraft_doc::live::expand_deep(n, Some(hook)),
|
|
_ => return None,
|
|
};
|
|
outline_nested_text(&mut art, false);
|
|
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)
|
|
}
|
|
|
|
/// 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 plain outlines ([`outline_text`]) with its visibility, which is what
|
|
/// [`crate::clip_outline`] clips by.
|
|
pub(crate) fn outline_nested_text(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 => outline_text(c).map(|o| Node { visible: c.visible, ..o }),
|
|
NodeKind::Text(_) => as_art(c, None),
|
|
_ => None,
|
|
};
|
|
if let Some(o) = art {
|
|
*c = Arc::new(o);
|
|
} else if c.children().is_some() {
|
|
outline_nested_text(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. 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 with
|
|
/// a geometry effect on a fill or stroke of its own ([`has_item_geometry`]), or has no art (a
|
|
/// symbol instance without `symbol_art`).
|
|
pub fn reshape(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|
if (geometry_effects(n).is_empty() && n.projection().is_none() && !has_item_geometry(n)) || !(needs_outline(n) || has_gradient_copies(n)) {
|
|
return None;
|
|
}
|
|
as_art(n, symbol_art)
|
|
}
|
|
|
|
/// `n` (see [`needs_outline`], or a path with [`has_gradient_copies`]) as plain art for Expand
|
|
/// Appearance: [`reshape`]d when it has
|
|
/// geometry effects, else type with fills or strokes of its own outlined, in the same form. `None`
|
|
/// when it has neither.
|
|
pub fn expand_outlined(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|
// A path whose Transform effect copies it with a gradient: each copy an object of its own.
|
|
if has_gradient_copies(n) {
|
|
return as_art(n, symbol_art);
|
|
}
|
|
let own_paint = matches!(n.kind, NodeKind::Text(_)) && !n.appearance.items.is_empty();
|
|
if !needs_outline(n) || (geometry_effects(n).is_empty() && !own_paint) {
|
|
return None;
|
|
}
|
|
as_art(n, symbol_art)
|
|
}
|
|
|
|
/// [`reshape`] without its checks (no geometry effects: just the art).
|
|
pub(crate) fn as_art(n: &Node, symbol_art: Option<&Node>) -> 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 clip = Node::path(n.id, frame, Appearance::default());
|
|
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
|
*clipping = true;
|
|
}
|
|
let image = Node::new(n.id, n.kind.clone());
|
|
Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image)], clip: true })
|
|
}
|
|
_ => outline_art(n, symbol_art)?,
|
|
};
|
|
reshape_leaves(&mut art, &fx);
|
|
// Type and symbols in perspective: the art projected onto their plane.
|
|
if let Some(h) = n.projection() {
|
|
project_leaves(&mut art, &h);
|
|
}
|
|
// The art is a group with `n`'s id (a clip group for an image).
|
|
let mut out = art;
|
|
out.name = n.name.clone();
|
|
out.visible = n.visible;
|
|
out.opacity = n.opacity;
|
|
out.blend = n.blend;
|
|
out.isolate = n.isolate;
|
|
// One element: its pieces never knock each other out.
|
|
out.knockout = vectorcraft_doc::Knockout::Off;
|
|
out.knockout_shape = n.knockout_shape;
|
|
out.extras_mut().mask = n.extras().mask.clone();
|
|
out.appearance.effects = n.appearance.effects.iter().filter(|e| !is_geometry(&e.id)).cloned().collect();
|
|
Some(out)
|
|
}
|
|
|
|
/// Map every path under `art` (and its gradients) through the projective map `h`, curves split
|
|
/// finely enough to follow it.
|
|
fn project_leaves(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`]); other geometry
|
|
/// effects leave them as they are (a raster follows a non-affine map only as a mesh warp).
|
|
pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect]) {
|
|
let seed = art.id.0;
|
|
// The placed gradients of type's outlines in `art` (`outline_text`) move and scale with the
|
|
// pieces, from the outlines' bounds before the effects to their bounds after them.
|
|
let mut outlines = vec![];
|
|
type_outline_bounds(art, &mut outlines);
|
|
for e in fx {
|
|
let Some(bounds) = art.geometric_bounds() else { break };
|
|
let params = merged_params(&e.id, &e.params);
|
|
let transform = (e.id == "distort.transform").then(|| crate::distort::transform_map(bounds, ¶ms, seed));
|
|
for_each_leaf(art, &mut |leaf| {
|
|
let Some((path, rule)) = leaf_geometry(leaf) else { return };
|
|
// A gradient paints each copy by itself, not the one piece the copies make together.
|
|
if let Some((m, copies)) = transform
|
|
&& copies > 0
|
|
&& (leaf.appearance.has_unplaced_gradient() || leaf.appearance.has_placed_gradient())
|
|
{
|
|
copy_with_gradients(leaf, &path, m, copies);
|
|
return;
|
|
}
|
|
let out = apply_one(&e.id, ¶ms, &path, bounds, &GeomContext { seed, ..GeomContext::of(leaf) });
|
|
set_geometry(leaf, out, rule);
|
|
});
|
|
if let Some((m, copies)) = transform {
|
|
transform_placed(art, m, copies);
|
|
}
|
|
}
|
|
if !fx.is_empty() && outlines.iter().any(Option::is_some) {
|
|
rebase_type_outlines(art, &mut outlines.into_iter());
|
|
}
|
|
}
|
|
|
|
/// Replace `leaf` (a path or compound path with geometry `path`) by a group of it and `copies`
|
|
/// more, each mapped once more by `m` than the last, 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. The copies stop where [`crate::distort::transform_within`]'s would.
|
|
fn copy_with_gradients(leaf: &mut Node, path: &PathData, m: Affine, copies: usize) {
|
|
let each = path.anchor_count();
|
|
let mut anchors = each;
|
|
let mut placements: Vec<Arc<Node>> = vec![Arc::new(leaf.clone())];
|
|
let mut xf = Affine::IDENTITY;
|
|
for _ in 0..copies {
|
|
anchors = anchors.saturating_add(each);
|
|
if anchors > crate::distort::MAX_COPY_ANCHORS {
|
|
break;
|
|
}
|
|
xf *= m;
|
|
let mut copy = leaf.clone();
|
|
copy.transform(xf, vectorcraft_doc::Scaling::default());
|
|
if !copy.geometric_bounds().is_some_and(crate::distort::within_reach) {
|
|
break;
|
|
}
|
|
placements.push(Arc::new(copy));
|
|
}
|
|
// The copies become nodes of their own under a group with the object's id and name.
|
|
let mut g = Node::group(leaf.id, placements);
|
|
g.name = leaf.name.clone();
|
|
*leaf = g;
|
|
}
|
|
|
|
/// 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_placed_gradient() {
|
|
follow_boxes(&mut leaf.appearance, from, to);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
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.
|
|
fn transform_placed(n: &mut Node, m: Affine, copies: usize) {
|
|
transform_placed_within(n, m, copies, crate::distort::MAX_COPY_OBJECTS)
|
|
}
|
|
|
|
/// [`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).
|
|
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, &mut room);
|
|
}
|
|
|
|
/// 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, 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, room);
|
|
continue;
|
|
}
|
|
let src = (**c).clone();
|
|
let placed = |xf: Affine| {
|
|
let mut one = src.clone();
|
|
match &mut one.kind {
|
|
NodeKind::Image(im) => im.xf = xf * im.xf,
|
|
NodeKind::SymbolInstance { xf: at, .. } => *at = xf * *at,
|
|
_ => {}
|
|
}
|
|
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;
|
|
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(n: &Node) -> Node {
|
|
let hook: &dyn Fn(&Node) -> Option<Node> = &outline_text;
|
|
let mut art = vectorcraft_doc::live::expand_deep(n, Some(hook));
|
|
outline_nested_text(&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);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
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 }
|
|
};
|
|
}
|