mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 04:54:45 +00:00
Plug-ins: Expand works on each painted piece and stacked painted fills keep the others
Object > Expand of gradient and pattern fills now expands the pieces a painted live effect draws one by one, so a piece keeps its own paints. A later painted effect on a fill replaces only the first fill of a piece and leaves the piece's other fills in place. Document Info passes the document when it reads an image's painted fills.
This commit is contained in:
@@ -79,8 +79,8 @@ pub use marks::{CROP_MARKS, crop_marks_art, has_crop_marks};
|
||||
pub use pixel::{PIXEL_EFFECTS, PixelFx, PixelSpace};
|
||||
pub use raster::{RASTERIZE, RasterFx, Rasterize, color_param, outset, raster_effects};
|
||||
pub use reshape::{
|
||||
SYMBOL_ART_MAX_NODES, expand_outlined, has_gradient_copies, has_item_geometry, has_transform_art, has_transform_copies, has_transform_options,
|
||||
image_paint, needs_outline, outline_art, outline_text, reshape,
|
||||
SYMBOL_ART_MAX_NODES, expand_outlined, has_gradient_copies, has_item_geometry, has_painted_effects, has_transform_art, has_transform_copies,
|
||||
has_transform_options, image_paint, needs_outline, outline_art, outline_text, reshape,
|
||||
};
|
||||
pub use revolve::{REVOLVE, has_revolve, revolve_art, revolve_options, validate_revolve};
|
||||
pub use solids::{
|
||||
|
||||
@@ -869,8 +869,9 @@ fn piece_cost(path: &PathData) -> Budget {
|
||||
/// 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 copies of `item`, with reference
|
||||
/// box `bounds`: `leaf` becomes a group of one path for each piece returned, in order. 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`]); one that gives none keeps the leaf's. Returns
|
||||
/// gives fills (on a fill) or strokes (on a stroke) is painted by copies of `item`, the leaf's first
|
||||
/// fill or stroke, 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`.
|
||||
@@ -896,7 +897,13 @@ fn paint_piece(
|
||||
AppearanceItem::Fill(_) => p.fills.is_some(),
|
||||
AppearanceItem::Stroke(_) => p.strokes.is_some(),
|
||||
};
|
||||
let items = if gives { vectorcraft_plugins::painted::items(item, p) } else { leaf.appearance.items.clone() };
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@ fn painted_runs<'a>(d: &Document, n: &'a Node) -> Option<(&'a vectorcraft_doc::T
|
||||
/// it) or of type whose layout draws nothing (`painted_runs`). An image's fills and strokes paint
|
||||
/// only where an effect gives them a shape.
|
||||
fn own_patterns<'a>(
|
||||
d: &Document,
|
||||
n: &'a Node,
|
||||
members: &HashSet<*const Node>,
|
||||
painted_runs: Option<&(&vectorcraft_doc::TextObject, BTreeSet<usize>)>,
|
||||
@@ -296,7 +297,7 @@ fn own_patterns<'a>(
|
||||
// The patterns an image's shaped fills and strokes paint (their effects can transform the
|
||||
// pattern, so they are matched by name).
|
||||
let shaped: Option<Vec<String>> = matches!(n.kind, NodeKind::Image(_)).then(|| {
|
||||
vectorcraft_render::effects::image_paint(n)
|
||||
vectorcraft_render::effects::image_paint(Some(d), n)
|
||||
.into_iter()
|
||||
.filter_map(|(_, _, item)| match item.paint() {
|
||||
Paint::Pattern { pattern, .. } => Some(pattern.clone()),
|
||||
@@ -415,7 +416,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The patterns this object's fills and strokes use, each once.
|
||||
let mut used_patterns: Vec<&str> = vec![];
|
||||
let painted_runs = painted_runs(d, n);
|
||||
for pattern in own_patterns(n, &members, painted_runs.as_ref()) {
|
||||
for pattern in own_patterns(d, n, &members, painted_runs.as_ref()) {
|
||||
patterns.insert(pattern.to_string());
|
||||
used_patterns.push(pattern);
|
||||
}
|
||||
|
||||
@@ -393,8 +393,17 @@ fn expand_gradient_fills(s: &mut Session, to: GradientTo) -> Result<()> {
|
||||
/// Path `l` with each gradient fill turned into art, in appearance order: the other items stay
|
||||
/// on plain copies of the path. The path's geometry effects ([`reshaped`]) and each fill's own
|
||||
/// ([`fill_shape`]) shape the outline the art fills first, as on the canvas. `None` when no
|
||||
/// gradient fill can be expanded.
|
||||
/// gradient fill can be expanded. A path that live-effect plug-ins paint is expanded piece by piece
|
||||
/// ([`expand_pieces`]).
|
||||
fn expand_fills(d: &mut Document, l: &Node, to: GradientTo) -> Option<Node> {
|
||||
if vectorcraft_render::effects::has_painted_effects(l) {
|
||||
return expand_pieces(d, l, usize::MAX, |d, piece, _| expand_piece_fills(d, piece, to));
|
||||
}
|
||||
expand_piece_fills(d, l, to)
|
||||
}
|
||||
|
||||
/// [`expand_fills`] of a path with no live-effect plug-in that paints what it returns.
|
||||
fn expand_piece_fills(d: &mut Document, l: &Node, to: GradientTo) -> Option<Node> {
|
||||
if !matches!(l.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) || !l.appearance.items.iter().any(|i| gradient_fill(i).is_some()) {
|
||||
return None;
|
||||
}
|
||||
@@ -466,10 +475,50 @@ fn gradient_art(d: &mut Document, l: &Node, path: &PathData, bounds: Rect, f: &F
|
||||
Some(art)
|
||||
}
|
||||
|
||||
/// Path `l`, which live-effect plug-ins paint, as the art the canvas draws
|
||||
/// ([`reshape`](vectorcraft_render::effects::reshape)) with ids of its own, each piece of the art
|
||||
/// replaced by what `expand` makes of it (`expand` gets the piece and how many objects it may become;
|
||||
/// `None` keeps the piece with its own paints). `None` when the art would hold more than `budget`
|
||||
/// objects or no piece expands.
|
||||
fn expand_pieces(d: &mut Document, l: &Node, budget: usize, mut expand: impl FnMut(&mut Document, &Node, usize) -> Option<Node>) -> Option<Node> {
|
||||
let art = vectorcraft_render::effects::reshape(Some(d), l, None)?;
|
||||
let mut art = d.reid(&art);
|
||||
art.locked = l.locked;
|
||||
let mut left = budget.checked_sub(art.count_with_masks())?;
|
||||
let mut expanded = false;
|
||||
expand_piece_leaves(d, &mut art, &mut left, &mut expanded, &mut expand);
|
||||
expanded.then_some(art)
|
||||
}
|
||||
|
||||
/// Replace each path and compound path under `n` (`n` included) by what `expand` makes of it, in
|
||||
/// paint order. `left` is how many more objects the art may hold, and `expanded` is set when a path
|
||||
/// is replaced.
|
||||
fn expand_piece_leaves(
|
||||
d: &mut Document,
|
||||
n: &mut Node,
|
||||
left: &mut usize,
|
||||
expanded: &mut bool,
|
||||
expand: &mut impl FnMut(&mut Document, &Node, usize) -> Option<Node>,
|
||||
) {
|
||||
if matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) {
|
||||
let room = *left + n.count_with_masks();
|
||||
if let Some(new) = expand(d, n, room) {
|
||||
*left = room.saturating_sub(new.count_with_masks());
|
||||
*n = new;
|
||||
*expanded = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
expand_piece_leaves(d, Arc::make_mut(c), left, expanded, expand);
|
||||
}
|
||||
}
|
||||
|
||||
/// Path `l` as the canvas paints its fills and strokes: its visible geometry effects applied to its
|
||||
/// path (a plain path then), and every geometry effect of its own removed. `None` when it has no
|
||||
/// visible geometry effect. The swatches and patterns that live-effect plug-ins painting their
|
||||
/// pieces name are looked up in `doc`.
|
||||
/// pieces name are looked up in `doc`. This merges the pieces of such a plug-in into one path, so a
|
||||
/// path that one paints goes through [`expand_pieces`] instead.
|
||||
fn reshaped(doc: &Document, l: &Node) -> Option<Node> {
|
||||
use vectorcraft_render::effects::{GeomContext, apply_geometry_with, has_geometry, is_geometry};
|
||||
if !has_geometry(&l.appearance.effects) {
|
||||
@@ -771,8 +820,17 @@ fn expand_patterns(d: &mut Document, l: &Node) -> Option<Node> {
|
||||
}
|
||||
|
||||
/// [`expand_patterns`] for only the pattern fills `which` picks: the others stay like the other
|
||||
/// items, as does a pattern fill whose tiles would take the tiles of `l` past `budget` objects.
|
||||
/// items, as does a pattern fill whose tiles would take the tiles of `l` past `budget` objects. A
|
||||
/// path that live-effect plug-ins paint is expanded piece by piece ([`expand_pieces`]).
|
||||
fn expand_patterns_where(d: &mut Document, l: &Node, which: impl Fn(&Document, &AppearanceItem) -> bool, budget: usize) -> Option<Node> {
|
||||
if vectorcraft_render::effects::has_painted_effects(l) {
|
||||
return expand_pieces(d, l, budget, |d, piece, room| expand_piece_patterns(d, piece, &which, room));
|
||||
}
|
||||
expand_piece_patterns(d, l, which, budget)
|
||||
}
|
||||
|
||||
/// [`expand_patterns_where`] of a path with no live-effect plug-in that paints what it returns.
|
||||
fn expand_piece_patterns(d: &mut Document, l: &Node, which: impl Fn(&Document, &AppearanceItem) -> bool, budget: usize) -> Option<Node> {
|
||||
if !matches!(l.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })
|
||||
|| !l.appearance.items.iter().any(|i| pattern_fill(d, i).is_some() && which(d, i))
|
||||
{
|
||||
|
||||
@@ -482,20 +482,96 @@ fn a_retained_renderer_shows_a_piece_once_its_swatch_exists() {
|
||||
assert_eq!(at_piece(&mut r, &s), GREEN, "the piece is drawn with its swatch");
|
||||
}
|
||||
|
||||
/// Object › Expand of a gradient fill takes the geometry of a piece linked to the document's swatch.
|
||||
/// Object › Expand of a gradient fill works piece by piece, for a painted effect on the object and
|
||||
/// on the fill: a piece that keeps the gradient has it expanded, and a piece with a paint of its own,
|
||||
/// here linked to the document's swatch, keeps that paint on its own path.
|
||||
#[test]
|
||||
fn expanding_a_gradient_fill_takes_a_swatch_linked_piece() {
|
||||
fn expanding_a_gradient_fill_keeps_each_piece_s_own_paint() {
|
||||
for on_fill in [false, true] {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 10.0, "#ff0000");
|
||||
run(&mut s, "paint.setFill", json!({"ids": [id.0], "gradient": {"kind": "linear"}}));
|
||||
run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"}));
|
||||
let mut leaf = solid("#00ff00");
|
||||
leaf["swatch"] = json!("Leaf");
|
||||
let out = json!({"objects": [{"path": square_at(40.0), "fills": [leaf]}, {"path": square_at(100.0)}]}).to_string();
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.expandref", true), &out));
|
||||
let item = if on_fill { json!(node(&s, id).appearance.items.iter().position(|i| i.is_fill()).unwrap()) } else { Value::Null };
|
||||
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.expandref", "item": item}));
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, "object.expand", json!({"object": false, "fill": true, "stroke": false}));
|
||||
let img = drawn(&s);
|
||||
assert_eq!((img.pixel(15, 15), img.pixel(50, 50)), (WHITE, GREEN), "on_fill {on_fill}: the input is gone and the piece keeps its paint");
|
||||
assert!(![WHITE, GREEN].contains(&img.pixel(110, 50)), "on_fill {on_fill}: the other piece shows the gradient");
|
||||
let mut own = Paint::solid(Color::from_hex("#00ff00").unwrap());
|
||||
if let Paint::Solid { swatch, .. } = &mut own {
|
||||
*swatch = Some("Leaf".into());
|
||||
}
|
||||
let mut kept = vec![];
|
||||
let mut plugins = 0;
|
||||
s.doc().unwrap().doc.walk(|n| {
|
||||
plugins += n
|
||||
.appearance
|
||||
.effects
|
||||
.iter()
|
||||
.chain(n.appearance.items.iter().flat_map(|i| i.effects()))
|
||||
.filter(|e| e.id.starts_with("plugin."))
|
||||
.count();
|
||||
if n.path_data().is_some_and(|p| p.bounds() == square_at(40.0).bounds()) {
|
||||
kept.push(n.appearance.fill().map(|f| f.paint.clone()));
|
||||
}
|
||||
});
|
||||
assert_eq!(kept, [Some(own)], "on_fill {on_fill}");
|
||||
assert_eq!(plugins, 0, "on_fill {on_fill}: no live effect is left");
|
||||
}
|
||||
}
|
||||
|
||||
/// Object › Expand of a pattern fill works piece by piece: a piece that keeps the pattern has its
|
||||
/// tiles expanded, and a piece with a paint of its own keeps that paint.
|
||||
#[test]
|
||||
fn expanding_a_pattern_fill_keeps_each_piece_s_own_paint() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 10.0, "#ff0000");
|
||||
run(&mut s, "paint.setFill", json!({"ids": [id.0], "gradient": {"kind": "linear"}}));
|
||||
run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"}));
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.expandref", true), &leaf_square()));
|
||||
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.expandref"}));
|
||||
let tile = rect(&mut s, 300.0, "#00ff00");
|
||||
run(&mut s, "object.pattern.make", json!({"ids": [tile.0], "name": "Tiles", "edit": false}));
|
||||
run(&mut s, "paint.setFill", json!({"ids": [id.0], "swatch": "Tiles"}));
|
||||
let out = json!({"objects": [{"path": square_at(40.0), "fills": [solid("#0000ff")]}, {"path": square_at(100.0)}]}).to_string();
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.expandtiles", true), &out));
|
||||
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.expandtiles"}));
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, "object.expand", json!({"object": false, "fill": true, "stroke": false}));
|
||||
let img = drawn(&s);
|
||||
assert_eq!(img.pixel(15, 15), WHITE, "the input path is gone");
|
||||
assert_ne!(img.pixel(50, 50), WHITE, "the piece's geometry is filled");
|
||||
assert_eq!((img.pixel(15, 15), img.pixel(50, 50)), (WHITE, BLUE), "the input is gone and the piece keeps its paint");
|
||||
assert!(near(img.pixel(110, 50), GREEN), "the other piece shows the tiles");
|
||||
let mut patterns = 0;
|
||||
s.doc().unwrap().doc.walk(|n| patterns += n.appearance.items.iter().filter(|i| matches!(i.paint(), Paint::Pattern { .. })).count());
|
||||
assert_eq!(patterns, 0, "the tiles are objects");
|
||||
}
|
||||
|
||||
/// Two painted effects on one fill, the first returning a piece with two fills: the second replaces
|
||||
/// the first of them and the other fill stays over it.
|
||||
#[test]
|
||||
fn a_second_painted_effect_on_a_fill_keeps_the_piece_s_other_fills() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 10.0, "#ff0000");
|
||||
let two = json!({"objects": [{"path": square_at(40.0), "fills": [solid("#ff0000"), solid("#00ff00")]}]}).to_string();
|
||||
let one = json!({"objects": [{"path": square_at(40.0), "fills": [solid("#0000ff")]}]}).to_string();
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.twofills", true), &two));
|
||||
install_wat(&mut s, &tpl::returning(&painting("test.engine.onefill", true), &one));
|
||||
let fill = node(&s, id).appearance.items.iter().position(|i| i.is_fill()).unwrap();
|
||||
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.twofills", "item": fill}));
|
||||
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.onefill", "item": fill}));
|
||||
assert_eq!(drawn(&s).pixel(50, 50), GREEN, "the other fill paints over the replaced one");
|
||||
let st = s.doc().unwrap();
|
||||
let art = vectorcraft_render::effects::reshape(Some(&st.doc), st.doc.node(id).unwrap(), None).unwrap();
|
||||
let mut paints = vec![];
|
||||
art.walk(&mut |n| {
|
||||
if n.path_data().is_some() {
|
||||
paints.push(n.appearance.items.iter().filter(|i| i.is_fill()).map(|i| i.paint().clone()).collect::<Vec<_>>());
|
||||
}
|
||||
});
|
||||
let fill = |hex: &str| Paint::solid(Color::from_hex(hex).unwrap());
|
||||
assert!(paints.contains(&vec![fill("#0000ff"), fill("#00ff00")]), "{paints:?}");
|
||||
}
|
||||
|
||||
/// The geometry of a painted result that names a swatch counts only when the document has that
|
||||
|
||||
+2
-1
@@ -437,7 +437,8 @@ is a piece drawn by itself, in the order returned, with paints of its own:
|
||||
`Test Tone: plug-in output: objects[1].strokes[0].width: …`.
|
||||
- The pieces are never written into the document while it is drawn, measured, previewed, saved or exported. PDF and SVG
|
||||
export draw them as the canvas does, from a copy of the document. Object › Expand Appearance writes each piece as an
|
||||
ordinary path or compound path with its fills and strokes, as one undo step.
|
||||
ordinary path or compound path with its fills and strokes, as one undo step. Object › Expand of gradient and pattern
|
||||
fills expands the fills of each piece, and leaves a piece with other paints as it is drawn.
|
||||
|
||||
An **exporter** returns `{"dataBase64": "…", "warnings"?: ["…"]}`: the file, in base64, which the host writes as it
|
||||
is, and what the export left out or changed, for the user (at most 100 warnings of at most 500 characters each, shown
|
||||
|
||||
Reference in New Issue
Block a user