Plug-ins: painted pieces keep their swatches in Pathfinder and baked art

Pathfinder effects, compound shapes, Expand Appearance and pattern
expansion now pass the document to the effects they evaluate, so a
painted piece that names a swatch or pattern takes part as drawn instead
of being refused. A path with a painted effect takes part in Pathfinder
and compound shapes as its pieces, each with its own paint, inside the
path's opacity and blend mode.

The renderer's kept effect art and live evaluations now count the
document's swatch and pattern names for objects with a plug-in effect,
so deleting or restoring a swatch a piece names redraws the object.
This commit is contained in:
dotCooCoo
2026-11-08 05:52:39 -08:00
parent c2f0a5667f
commit d8afed1562
22 changed files with 318 additions and 165 deletions
+15 -14
View File
@@ -333,16 +333,16 @@ fn moved_type_group(n: &Node) -> Option<Node> {
/// 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) {
fn keep(d: &Document, 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);
outline_nested_text(Some(d), Arc::make_mut(c), clipping);
} else if c.children().is_some() {
keep(Arc::make_mut(c), moves, clipping, kept);
keep(d, Arc::make_mut(c), moves, clipping, kept);
}
continue;
}
@@ -357,7 +357,7 @@ fn container_keeping_type(d: &Document, n: &Node) -> Option<Node> {
}
}
None if clipping => outline_text(c).map(|o| Node { visible: c.visible, ..o }),
None => as_art(None, c, None),
None => as_art(Some(d), c, None),
};
if let Some(o) = art {
*c = Arc::new(o);
@@ -376,11 +376,11 @@ fn container_keeping_type(d: &Document, n: &Node) -> Option<Node> {
}
let mut m = crate::reshape::evaluated(Some(d), n);
let mut kept = false;
keep(&mut m, &moves, false, &mut kept);
keep(d, &mut m, &moves, false, &mut kept);
if !kept {
return None;
}
reshape_leaves(&mut m, &fx);
reshape_leaves(&mut m, &fx, Some(d));
m.appearance.effects.retain(|e| !is_pathfinder(&e.id) && !is_geometry(&e.id));
m.appearance.items.clear();
m.appearance.contents_index = None;
@@ -530,9 +530,10 @@ pub fn expand_art(d: &mut Document, n: &mut Node, stroke_art: StrokeArt) {
/// 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> {
/// makes of it, and each copy is baked this way; the swatches and patterns that live-effect plug-ins
/// painting their pieces name are looked up in `doc`. `None` when it has neither a stroke to
/// outline nor a geometry effect, so bending first changes nothing.
pub fn bake_appearance(doc: Option<&Document>, n: &Node) -> Option<Node> {
if let Some(m) = crate::revolve_art(n).or_else(|| crate::solid_art(n)) {
return Some(m);
}
@@ -543,9 +544,9 @@ pub fn bake_appearance(n: &Node) -> Option<Node> {
// 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)
&& let Some(mut m) = reshape(doc, n, None)
{
bake_appearance_pieces(&mut m);
bake_appearance_pieces(doc, &mut m);
return Some(m);
}
let outlined = |i: &AppearanceItem| paints(i) && matches!(i, AppearanceItem::Stroke(s) if s.brush.is_none());
@@ -601,15 +602,15 @@ pub fn bake_appearance(n: &Node) -> Option<Node> {
}
/// [`bake_appearance`] every path and compound path in `n` that it changes, in place.
fn bake_appearance_pieces(n: &mut Node) {
fn bake_appearance_pieces(doc: Option<&Document>, n: &mut Node) {
if matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) {
if let Some(m) = bake_appearance(n) {
if let Some(m) = bake_appearance(doc, n) {
*n = m;
}
return;
}
for c in n.children_mut().into_iter().flatten() {
bake_appearance_pieces(Arc::make_mut(c));
bake_appearance_pieces(doc, Arc::make_mut(c));
}
}
+6 -6
View File
@@ -67,7 +67,7 @@ fn a_baked_stroke_keeps_its_raster_effects() {
s.effects.push(fx("path.offsetPath", json!({"offset": 5.0})));
s.effects.push(fx("stylize.dropShadow", json!({})));
}
let m = bake_appearance(&n).unwrap();
let m = bake_appearance(None, &n).unwrap();
let ch = m.children().unwrap();
let Some(AppearanceItem::Fill(f)) = ch[1].appearance.items.first() else { panic!("the outline is filled") };
assert_eq!(f.effects.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), ["stylize.dropShadow"]);
@@ -82,7 +82,7 @@ fn a_baked_stroke_leaves_dither_behind() {
g.gradient.dither = true;
s.paint = Paint::Gradient(Box::new(g));
}
let m = bake_appearance(&n).unwrap();
let m = bake_appearance(None, &n).unwrap();
let ch = m.children().unwrap();
let Some(AppearanceItem::Fill(f)) = ch[1].appearance.items.first() else { panic!("the outline is filled") };
let Paint::Gradient(g) = &f.paint else { panic!("a gradient fill") };
@@ -98,7 +98,7 @@ fn a_baked_stroke_with_a_gradient_along_it_becomes_meshes_clipped_to_its_outline
s.gradient_mode = vectorcraft_doc::StrokeGradientMode::Along;
s.opacity = 0.5;
}
let m = bake_appearance(&n).unwrap();
let m = bake_appearance(None, &n).unwrap();
let ch = m.children().unwrap();
let NodeKind::Group { children, clip: true } = &ch[1].kind else { panic!("a clip group: {:?}", ch[1].kind) };
assert!(matches!(children[0].kind, NodeKind::Path { clipping: true, .. }));
@@ -131,7 +131,7 @@ fn a_baked_transform_copy_keeps_its_scaled_stroke() {
// Distort Appearance: the 200% copy's stroke is 4 pt, as outside the envelope, so its outline
// is 204 pt wide.
let n = transformed(Paint::None, Paint::solid(Color::BLACK), 2.0, json!({"scaleH": 200, "scaleV": 200, "copies": 1, "strokes": true}));
let m = bake_appearance(&n).unwrap();
let m = bake_appearance(None, &n).unwrap();
let w = widths(&m);
assert_eq!(w.len(), 2, "{w:?}");
assert!((w[0] - 102.0).abs() < 0.5 && (w[1] - 204.0).abs() < 0.5, "{w:?}");
@@ -142,7 +142,7 @@ fn a_baked_transform_copy_keeps_its_scaled_stroke() {
fn a_baked_transform_copy_keeps_its_moved_pattern() {
let paint = Paint::Pattern { pattern: "p".into(), xf: vectorcraft_geom::Affine::IDENTITY };
let n = transformed(paint, Paint::None, 0.0, json!({"moveH": 30, "copies": 1, "patterns": true}));
let m = bake_appearance(&n).unwrap();
let m = bake_appearance(None, &n).unwrap();
let xs: Vec<f64> = baked_paths(&m)
.iter()
.map(|p| match p.appearance.fill_paint() {
@@ -157,7 +157,7 @@ fn a_baked_transform_copy_keeps_its_moved_pattern() {
fn a_baked_transform_without_its_options_outlines_the_copies_together() {
// Without Scale Strokes & Effects the copy keeps the 2 pt stroke: one outline of both squares.
let n = transformed(Paint::None, Paint::solid(Color::BLACK), 2.0, json!({"scaleH": 200, "scaleV": 200, "copies": 1}));
let m = bake_appearance(&n).unwrap();
let m = bake_appearance(None, &n).unwrap();
let w = widths(&m);
assert_eq!(w.len(), 1, "{w:?}");
assert!((w[0] - 202.0).abs() < 0.5, "{w:?}");
+52 -40
View File
@@ -67,25 +67,28 @@ pub type OutlineHook<'a> = &'a dyn Fn(&Node) -> Option<Node>;
/// back to front: a compound shape as its area, a blend, an envelope or an object with a warp as
/// the art it draws, and type and symbol instances as their art through `hook`. Past
/// [`SYMBOL_ART_MAX_NODES`](crate::SYMBOL_ART_MAX_NODES) objects of symbol art, further
/// instances add nothing.
pub fn member_leaves(n: &Node, hook: Option<OutlineHook>) -> Vec<Node> {
/// instances add nothing. The swatches and patterns that live-effect plug-ins painting their
/// pieces name are looked up in `doc` ([`shape_art`]).
pub fn member_leaves(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>) -> Vec<Node> {
let mut out = vec![];
leaves(n, hook, &SymbolNodes::default(), &mut out);
leaves(doc, n, hook, &SymbolNodes::default(), &mut out);
out
}
/// [`member_leaves`], with the symbol art given so far counted in `nodes`.
fn leaves(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes, out: &mut Vec<Node>) {
fn leaves(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes, out: &mut Vec<Node>) {
if !n.visible {
return;
}
// A blend, an envelope or an object with a warp takes part as the art it draws.
if let Some(art) = shape_art_in(n, hook, nodes) {
if let Some(art) = shape_art_in(doc, n, hook, nodes) {
let first = out.len();
leaves(&art, hook, nodes, out);
// Every path of a live object's, type's or symbol instance's art is drawn inside the
// object's opacity and blend mode.
if crate::needs_outline(n) {
leaves(doc, &art, hook, nodes, out);
// Every path of a live object's, type's or symbol instance's art, and every piece of a path
// whose live-effect plug-in paints its pieces, is drawn inside the object's opacity and
// blend mode.
let pieces = matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) && matches!(art.kind, NodeKind::Group { .. });
if crate::needs_outline(n) || pieces {
for l in out.iter_mut().skip(first) {
within(l, n);
}
@@ -95,11 +98,11 @@ fn leaves(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes, out: &mut Ve
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => out.push(n.clone()),
// A compound shape takes part as its area, styled like it.
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => match compound_shape_art_in(n, hook, nodes) {
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => match compound_shape_art_in(doc, n, hook, nodes) {
Some(art) => out.push(art),
None => {
for c in children {
leaves(c, hook, nodes, out);
leaves(doc, c, hook, nodes, out);
}
}
},
@@ -114,7 +117,7 @@ fn leaves(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes, out: &mut Ve
let art = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } if nodes.charge(&art) => {
let mut art = crate::reshape::symbol_art_with(&art, h, nodes, symbol);
crate::reshape::outline_nested_text(&mut art, false);
crate::reshape::outline_nested_text(doc, &mut art, false);
transparency_into_members(&mut art);
art
}
@@ -122,7 +125,7 @@ fn leaves(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes, out: &mut Ve
_ => art,
};
let first = out.len();
leaves(&art, None, nodes, out);
leaves(doc, &art, None, nodes, out);
for l in out.iter_mut().skip(first) {
within(l, n);
}
@@ -176,29 +179,37 @@ fn transparency_into_members(n: &mut Node) {
/// [`outline_object`](crate::outline_object) gives a symbol instance its symbol's art. The art
/// keeps the effects of `n` other than its geometry effects, and the groups inside it pass their
/// opacity and blend mode to the objects they hold. `None` when `n` is none of these, for images,
/// for symbol instances without geometry effects, and for one without art from `hook`.
pub fn shape_art(n: &Node, hook: Option<OutlineHook>) -> Option<Node> {
shape_art_in(n, hook, &SymbolNodes::default())
/// for symbol instances without geometry effects, and for one without art from `hook`. A
/// live-effect plug-in that paints its pieces makes a path, and each path of a group's or symbol's
/// art, a group of its pieces, whose swatches and patterns must be in `doc` (without one, pieces
/// that name one are refused and the path stays as it is).
pub fn shape_art(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>) -> Option<Node> {
shape_art_in(doc, n, hook, &SymbolNodes::default())
}
/// [`shape_art`], with the symbol art given so far counted in `nodes`.
fn shape_art_in(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Option<Node> {
let mut art = reshaped_art(n, hook, nodes)?;
fn shape_art_in(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Option<Node> {
let mut art = reshaped_art(doc, n, hook, nodes)?;
transparency_into_members(&mut art);
Some(art)
}
/// [`shape_art_in`] before the groups inside the art pass on their transparency.
fn reshaped_art(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Option<Node> {
fn reshaped_art(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Option<Node> {
let fx = crate::reshape::geometry_effects(n);
let outline = hook.unwrap_or(&crate::outline_text);
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } if !fx.is_empty() => Some(reshaped(n, n)),
NodeKind::Group { .. } | NodeKind::Layer { .. } if !fx.is_empty() => match compound_shape_art_in(n, hook, nodes) {
// A path with a live-effect plug-in that paints its pieces: the group of its pieces, each
// with its own paints, as the renderer draws it.
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } if !fx.is_empty() => {
let painted = crate::reshape::has_painted_effects(n).then(|| crate::reshape::reshape(doc, n, None)).flatten();
Some(painted.unwrap_or_else(|| reshaped(n, n)))
}
NodeKind::Group { .. } | NodeKind::Layer { .. } if !fx.is_empty() => match compound_shape_art_in(doc, n, hook, nodes) {
Some(area) => Some(reshaped(&area, n)),
None => {
let mut m = crate::reshape::outlined_members_with(None, n, outline, nodes);
crate::reshape::reshape_leaves(&mut m, &fx);
crate::reshape::reshape_leaves(&mut m, &fx, doc);
without_geometry_effects(&mut m);
Some(m)
}
@@ -209,8 +220,8 @@ fn reshaped_art(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Opt
let h = hook?;
let art = h(n).filter(|art| nodes.charge(art))?;
let mut art = crate::reshape::symbol_art_with(&art, h, nodes, symbol);
crate::reshape::outline_nested_text(&mut art, false);
crate::reshape::reshape_leaves(&mut art, &fx);
crate::reshape::outline_nested_text(doc, &mut art, false);
crate::reshape::reshape_leaves(&mut art, &fx, doc);
(art.opacity, art.blend, art.isolate) = (n.opacity, n.blend, n.isolate);
art.appearance.effects = n.appearance.effects.clone();
without_geometry_effects(&mut art);
@@ -270,12 +281,12 @@ fn shape_op(mode: ShapeMode) -> po::BoolOp {
/// it by the member's shape mode; the backmost member starts the area. `None` when `n` is not a
/// compound shape, or its members are past a live Pathfinder's budget ([`PATHFINDER_MAX_SEGMENTS`],
/// [`PATHFINDER_MAX_OVERLAPS`], [`SYMBOL_ART_MAX_NODES`](crate::SYMBOL_ART_MAX_NODES)).
pub fn compound_shape_area(n: &Node, hook: Option<OutlineHook>) -> Option<PathData> {
compound_shape_area_in(n, hook, &SymbolNodes::default())
pub fn compound_shape_area(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>) -> Option<PathData> {
compound_shape_area_in(doc, n, hook, &SymbolNodes::default())
}
/// [`compound_shape_area`], with the symbol art given so far counted in `nodes`.
fn compound_shape_area_in(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Option<PathData> {
fn compound_shape_area_in(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Option<PathData> {
if !n.is_compound_shape() {
return None;
}
@@ -284,7 +295,7 @@ fn compound_shape_area_in(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNod
let mut members: Vec<(po::BoolOp, usize)> = vec![];
for c in n.children()? {
let before = all.len();
leaves(c, hook, nodes, &mut all);
leaves(doc, c, hook, nodes, &mut all);
if all.len() > before {
members.push((shape_op(c.extras().shape_mode), all.len() - before));
}
@@ -310,13 +321,13 @@ fn compound_shape_area_in(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNod
/// Compound shape `n` as one path of its area ([`compound_shape_area`]) with its appearance,
/// opacity and blend mode: how it takes part in a Pathfinder operation.
pub fn compound_shape_art(n: &Node, hook: Option<OutlineHook>) -> Option<Node> {
compound_shape_art_in(n, hook, &SymbolNodes::default())
pub fn compound_shape_art(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>) -> Option<Node> {
compound_shape_art_in(doc, n, hook, &SymbolNodes::default())
}
/// [`compound_shape_art`], with the symbol art given so far counted in `nodes`.
fn compound_shape_art_in(n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Option<Node> {
let area = compound_shape_area_in(n, hook, nodes)?;
fn compound_shape_art_in(doc: Option<&Document>, n: &Node, hook: Option<OutlineHook>, nodes: &SymbolNodes) -> Option<Node> {
let area = compound_shape_area_in(doc, n, hook, nodes)?;
let mut art = Node::path(n.id, area, Appearance { items: n.appearance.items.clone(), ..Default::default() });
(art.opacity, art.blend) = (n.opacity, n.blend);
Some(art)
@@ -422,15 +433,16 @@ fn within_budget<'a>(leaves: impl IntoIterator<Item = &'a Node>) -> bool {
/// Evaluate the visible Pathfinder effects of `group` in order: the resulting child objects.
/// `None` when the group has no Pathfinder effect. Past the budget ([`PATHFINDER_MAX_SEGMENTS`],
/// [`PATHFINDER_MAX_OVERLAPS`], [`SYMBOL_ART_MAX_NODES`](crate::SYMBOL_ART_MAX_NODES) for
/// each effect) it returns the children as they are.
pub fn pathfinder_children(group: &Node, hook: Option<OutlineHook>) -> Option<Vec<Arc<Node>>> {
/// each effect) it returns the children as they are. The swatches and patterns that live-effect
/// plug-ins painting their pieces name are looked up in `doc` ([`shape_art`]).
pub fn pathfinder_children(doc: Option<&Document>, group: &Node, hook: Option<OutlineHook>) -> Option<Vec<Arc<Node>>> {
if !has_pathfinder(group) {
return None;
}
let source: &[Arc<Node>] = group.children()?;
let mut stack: Vec<Arc<Node>> = if group.is_compound_shape() {
// The group's own appearance paints the area ([`evaluate_container`]), so the art has none.
match compound_shape_area(group, hook) {
match compound_shape_area(doc, group, hook) {
Some(area) if area.is_empty() => vec![],
Some(area) => vec![Arc::new(Node::path(group.id, area, Appearance::default()))],
None => return Some(source.to_vec()),
@@ -442,7 +454,7 @@ pub fn pathfinder_children(group: &Node, hook: Option<OutlineHook>) -> Option<Ve
if e.id == crate::trap::TRAP {
let (mut lv, nodes) = (vec![], SymbolNodes::default());
for c in &stack {
leaves(c, hook, &nodes, &mut lv);
leaves(doc, c, hook, &nodes, &mut lv);
}
if nodes.spent() || !within_budget(&lv) {
return Some(source.to_vec());
@@ -466,7 +478,7 @@ pub fn pathfinder_children(group: &Node, hook: Option<OutlineHook>) -> Option<Ve
.iter()
.map(|c| {
let mut lv = vec![];
leaves(c, hook, &nodes, &mut lv);
leaves(doc, c, hook, &nodes, &mut lv);
lv
})
.collect();
@@ -620,7 +632,7 @@ pub fn member_shapes(doc: Option<&Document>, n: &Node) -> Vec<(PathData, FillRul
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => out.extend(crate::reshape::leaf_geometry(n)),
NodeKind::Group { .. } if n.is_compound_shape() => {
let hook = |c: &Node| crate::outline_object(doc, c);
match compound_shape_area(n, Some(&hook)) {
match compound_shape_area(doc, n, Some(&hook)) {
Some(area) => out.push((area, FillRule::NonZero)),
None => members(doc, n, out),
}
@@ -702,7 +714,7 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option<Node> {
let mut m = n.clone();
if pathfinder {
let hook = |c: &Node| crate::outline_object(doc, c);
let result = pathfinder_children(n, Some(&hook)).unwrap_or_default();
let result = pathfinder_children(doc, n, Some(&hook)).unwrap_or_default();
if let Some(ch) = m.children_mut() {
*ch = result;
}
@@ -711,7 +723,7 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option<Node> {
}
if !fx.is_empty() {
m = crate::reshape::outlined_members(doc, &m);
crate::reshape::reshape_leaves_in(&mut m, &fx, doc);
crate::reshape::reshape_leaves(&mut m, &fx, doc);
}
m.appearance.effects.retain(|e| !is_pathfinder(&e.id) && !crate::is_geometry(&e.id));
if paint {
+20 -20
View File
@@ -27,7 +27,7 @@ fn area(n: &Node) -> f64 {
}
fn overlap_cmyk(g: &Node) -> [f32; 4] {
let faces = pathfinder_children(g, None).unwrap();
let faces = pathfinder_children(None, g, None).unwrap();
assert_eq!(faces.len(), 3);
let overlap = faces
.iter()
@@ -61,7 +61,7 @@ fn trap_effect_adds_an_overprinting_strip_above_the_members() {
let mut g = Node::group(NodeId(10), vec![sq(1, 0.0, yellow), sq(2, 10.0, blue)]);
let params = serde_json::json!({"thickness": 1.0, "tintReduction": 100});
g.appearance.effects.push(Effect { id: crate::trap::TRAP.into(), params, visible: true });
let out = pathfinder_children(&g, None).unwrap();
let out = pathfinder_children(None, &g, None).unwrap();
assert_eq!(out.len(), 3, "both squares and one trap");
assert_eq!((out[0].id, out[1].id), (NodeId(1), NodeId(2)));
let b = out[2].geometric_bounds().unwrap();
@@ -89,17 +89,17 @@ fn soft_mix_blends_cmyk_bottom_to_top_and_drops_strokes() {
#[test]
fn shape_modes_follow_the_pathfinder_panel() {
let add = pathfinder_children(&group_with("pathfinder.add"), None).unwrap();
let add = pathfinder_children(None, &group_with("pathfinder.add"), None).unwrap();
assert_eq!(add.len(), 1);
assert!((area(&add[0]) - 150.0).abs() < 1e-3);
// Add keeps the front object's paint, Subtract the back one's.
assert_eq!(add[0].id, NodeId(2));
let sub = pathfinder_children(&group_with("pathfinder.subtract"), None).unwrap();
let sub = pathfinder_children(None, &group_with("pathfinder.subtract"), None).unwrap();
assert_eq!(sub[0].id, NodeId(1));
assert!((area(&sub[0]) - 50.0).abs() < 1e-3);
let int = pathfinder_children(&group_with("pathfinder.intersect"), None).unwrap();
let int = pathfinder_children(None, &group_with("pathfinder.intersect"), None).unwrap();
assert!((area(&int[0]) - 50.0).abs() < 1e-3);
let div = pathfinder_children(&group_with("pathfinder.divide"), None).unwrap();
let div = pathfinder_children(None, &group_with("pathfinder.divide"), None).unwrap();
assert_eq!(div.len(), 3);
}
@@ -112,7 +112,7 @@ fn divide_and_outline_draw_nothing_for_unpainted_members() {
for effect in ["pathfinder.divide", "pathfinder.outline"] {
let mut g = Node::group(NodeId(10), vec![sq(1, 0.0, Color::rgb(1.0, 0.0, 0.0)), unpainted.clone()]);
g.appearance.effects.push(Effect { id: effect.into(), params: serde_json::json!({}), visible: true });
let out = pathfinder_children(&g, None).unwrap();
let out = pathfinder_children(None, &g, None).unwrap();
assert!(out.iter().any(|n| n.id == NodeId(1)) && out.iter().any(|n| n.id == NodeId(2)), "{effect}: {out:#?}");
for n in out.iter().filter(|n| n.id == NodeId(2)) {
assert!(n.appearance.items.iter().all(|i| i.paint().is_none()), "{effect}: {n:#?}");
@@ -138,7 +138,7 @@ fn square_and(front: Option<Node>, effect: &str) -> Node {
/// Returns the Pathfinder results of `g`, with type outlined as the renderer outlines it.
fn results(g: &Node) -> Vec<Arc<Node>> {
pathfinder_children(g, crate::text_outliner()).unwrap()
pathfinder_children(None, g, crate::text_outliner()).unwrap()
}
#[test]
@@ -167,7 +167,7 @@ fn type_changes_the_result_of_every_pathfinder_effect() {
assert!((with_type("pathfinder.intersect") + with_type("pathfinder.minusBack") - glyph_area).abs() < 0.5, "{glyph_area}");
// Outline art nested one group deeper takes part as well.
let nested: OutlineHook = &|n: &Node| crate::outline_text(n).map(|g| Node::group(n.id, vec![Arc::new(g)]));
let sub = pathfinder_children(&square_and(Some(type_h()), "pathfinder.subtract"), Some(nested)).unwrap();
let sub = pathfinder_children(None, &square_and(Some(type_h()), "pathfinder.subtract"), Some(nested)).unwrap();
assert!((sub.iter().map(|n| area(n)).sum::<f64>() - with_type("pathfinder.subtract")).abs() < 1e-6);
}
@@ -243,7 +243,7 @@ fn type_in_a_group_keeps_its_look_when_the_groups_geometry_effects_outline_it()
fn hidden_effect_or_plain_group_is_untouched() {
let mut g = group_with("pathfinder.add");
g.appearance.effects[0].visible = false;
assert!(pathfinder_children(&g, None).is_none());
assert!(pathfinder_children(None, &g, None).is_none());
assert!(!has_pathfinder(&sq(1, 0.0, Color::BLACK)));
}
@@ -328,7 +328,7 @@ fn pathfinder_past_its_budget_draws_the_source_art() {
// n² crossings past the cap: no evaluation (#3591), the source art as it is.
for n in [130, 800] {
let g = crossing_bars(n, "pathfinder.divide");
let out = pathfinder_children(&g, None).unwrap();
let out = pathfinder_children(None, &g, None).unwrap();
assert_eq!(out.len(), 2 * n, "{n} x {n} bars");
let ids: Vec<NodeId> = out.iter().map(|c| c.id).collect();
let src: Vec<NodeId> = g.children().unwrap().iter().map(|c| c.id).collect();
@@ -343,7 +343,7 @@ fn pathfinder_within_its_budget_still_divides() {
// n x n bars divide into 3·n² pieces, and 128 x 128 bars, which overlap in exactly
// `PATHFINDER_MAX_OVERLAPS` pairs, must still divide.
for n in [8, 128] {
let out = pathfinder_children(&crossing_bars(n, "pathfinder.divide"), None).unwrap();
let out = pathfinder_children(None, &crossing_bars(n, "pathfinder.divide"), None).unwrap();
assert_eq!(out.len(), 3 * n * n, "{n} x {n} bars");
}
}
@@ -361,7 +361,7 @@ fn pathfinder_on_many_stacked_bars_still_runs() {
.collect();
let mut g = Node::group(NodeId(10), children);
g.appearance.effects.push(Effect { id: "pathfinder.add".into(), params: serde_json::json!({}), visible: true });
let out = pathfinder_children(&g, None).unwrap();
let out = pathfinder_children(None, &g, None).unwrap();
assert_eq!(out.len(), 1);
}
@@ -382,9 +382,9 @@ fn compound_shapes_draw_the_area_their_shape_modes_make() {
for (mode, want) in [(ShapeMode::Add, 150.0), (ShapeMode::Subtract, 50.0), (ShapeMode::Intersect, 50.0), (ShapeMode::Exclude, 100.0)] {
let g = compound_shape(mode);
assert!(has_pathfinder(&g), "{mode:?}");
let area_path = compound_shape_area(&g, None).unwrap();
let area_path = compound_shape_area(None, &g, None).unwrap();
assert!((po::area(&area_path, FillRule::NonZero).abs() - want).abs() < 0.01, "{mode:?}");
let out = pathfinder_children(&g, None).unwrap();
let out = pathfinder_children(None, &g, None).unwrap();
assert_eq!(out.len(), 1, "{mode:?}");
assert!(out[0].appearance.items.is_empty(), "the group's own appearance paints it");
let art = evaluate_container(None, &g).unwrap();
@@ -401,7 +401,7 @@ fn compound_shapes_draw_the_area_their_shape_modes_make() {
back.extras_mut().shape_mode = ShapeMode::Subtract;
ch[0] = Arc::new(back);
}
assert!((po::area(&compound_shape_area(&g, None).unwrap(), FillRule::NonZero).abs() - 150.0).abs() < 0.01);
assert!((po::area(&compound_shape_area(None, &g, None).unwrap(), FillRule::NonZero).abs() - 150.0).abs() < 0.01);
// Hidden members take no part; the mark on a clip group is ignored.
let mut g = compound_shape(ShapeMode::Subtract);
if let Some(ch) = g.children_mut() {
@@ -409,10 +409,10 @@ fn compound_shapes_draw_the_area_their_shape_modes_make() {
front.visible = false;
ch[1] = Arc::new(front);
}
assert!((po::area(&compound_shape_area(&g, None).unwrap(), FillRule::NonZero).abs() - 100.0).abs() < 0.01);
assert!((po::area(&compound_shape_area(None, &g, None).unwrap(), FillRule::NonZero).abs() - 100.0).abs() < 0.01);
let mut clip = compound_shape(ShapeMode::Add);
clip.kind = NodeKind::Group { children: clip.children().unwrap().clone(), clip: true };
assert!(!clip.is_compound_shape() && compound_shape_area(&clip, None).is_none());
assert!(!clip.is_compound_shape() && compound_shape_area(None, &clip, None).is_none());
}
/// A compound shape inside another (and inside an Effect › Pathfinder group) takes part as its area,
@@ -425,10 +425,10 @@ fn nested_compound_shapes_take_part_as_their_area() {
let mut outer = Node::group(NodeId(20), vec![Arc::new(inner.clone()), Arc::new(cutter)]);
outer.extras_mut().compound_shape = true;
// (0–5 of the inner area) ∩ (2–12) = 2–5 by 10 high.
assert!((po::area(&compound_shape_area(&outer, None).unwrap(), FillRule::NonZero).abs() - 30.0).abs() < 0.01);
assert!((po::area(&compound_shape_area(None, &outer, None).unwrap(), FillRule::NonZero).abs() - 30.0).abs() < 0.01);
let mut effect = Node::group(NodeId(30), vec![Arc::new(inner)]);
effect.appearance.effects.push(Effect { id: "pathfinder.add".into(), params: serde_json::json!({}), visible: true });
let out = pathfinder_children(&effect, None).unwrap();
let out = pathfinder_children(None, &effect, None).unwrap();
assert_eq!(out.len(), 1);
assert!((area(&out[0]) - 50.0).abs() < 0.01);
assert_eq!(out[0].appearance.fill_paint().color(), Some(Color::rgb(0.0, 1.0, 0.0)), "styled like the compound shape");
+2 -1
View File
@@ -101,5 +101,6 @@ pub use geometry::{GeomContext, apply_geometry, apply_geometry_bez, apply_geomet
#[cfg(test)]
pub(crate) use geometry::{MAX_EFFECT_LINES, apply_geometry_within};
pub use params::{
default_params, has_geometry, has_plugin_effect, is_geometry, is_raster, merged_params, new_effect, plugin_revision, scale_effect, upgrade_params,
default_params, has_geometry, has_painted_plugin_effect, has_plugin_effect, is_geometry, is_raster, merged_params, new_effect, plugin_revision,
scale_effect, upgrade_params,
};
+2 -1
View File
@@ -43,7 +43,8 @@ fn with_hooks<R>(doc: Option<&Document>, f: impl FnOnce(Hooks) -> R) -> R {
vectorcraft_text::register_envelope_ink();
let outline = |n: &Node| outline_object(doc, n);
let tiles = |name: &str, xf: Affine, region: Rect| Some(doc?.pattern(name)?.instances_in(xf, region));
f(Hooks { outline: Some(&outline), appearance: Some(&bake_appearance), pattern: Some(&tiles), symbols: doc })
let appearance = |n: &Node| bake_appearance(doc, n);
f(Hooks { outline: Some(&outline), appearance: Some(&appearance), pattern: Some(&tiles), symbols: doc })
}
/// One level of evaluation of live object `n` ([`live::expand_live_hooks`]).
+8
View File
@@ -147,6 +147,14 @@ pub fn has_plugin_effect(n: &Node) -> bool {
n.appearance.effects.iter().any(plugin) || n.appearance.items.iter().any(|i| i.effects().iter().any(plugin))
}
/// Does `n` have a visible live-effect plug-in effect, its own or a fill's or stroke's, whose
/// plug-in paints the pieces it returns? What such an effect draws also depends on the document's
/// swatches and patterns, which its pieces may name.
pub fn has_painted_plugin_effect(n: &Node) -> bool {
let painted = |e: &Effect| e.visible && vectorcraft_plugins::effect::plugin_id(&e.id).is_some_and(vectorcraft_plugins::effect::paints_output);
n.appearance.effects.iter().any(painted) || n.appearance.items.iter().any(|i| i.effects().iter().any(painted))
}
/// The plug-in registry's revision, which changes whenever a plug-in is installed or removed.
pub fn plugin_revision() -> u64 {
vectorcraft_plugins::registry::revision()
+14 -17
View File
@@ -336,7 +336,7 @@ fn outline_art_with(
_ if vectorcraft_doc::live::is_live(n) => evaluated_with(doc, n, outline, nodes),
_ => return None,
};
outline_nested_text(&mut art, false);
outline_nested_text(doc, &mut art, false);
art.opacity = 1.0;
art.blend = Default::default();
art.isolate = false;
@@ -443,21 +443,22 @@ fn instances_as_art(doc: Option<&Document>, n: &mut Node, outline: OutlineHook,
/// 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) {
/// [`crate::clip_outline`] clips by. 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 => outline_text(c).map(|o| Node { visible: c.visible, ..o }),
NodeKind::Text(_) => as_art(None, c, None),
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(Arc::make_mut(c), clipping);
outline_nested_text(doc, Arc::make_mut(c), clipping);
}
}
}
@@ -606,20 +607,16 @@ pub(crate) fn geometry_effects(n: &Node) -> Vec<&Effect> {
/// 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). 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.
pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect]) {
reshape_leaves_within(art, fx, Budget::EFFECTS);
}
/// [`reshape_leaves`], with the swatches and patterns that painted live-effect plug-ins name looked
/// up in `doc` ([`reshape_leaves_apart`]).
pub(crate) fn reshape_leaves_in(art: &mut Node, fx: &[&Effect], doc: Option<&Document>) {
reshape_leaves_apart(art, fx, Budget::EFFECTS, false, doc);
/// 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`
/// ([`reshape_leaves_apart`]).
pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect], doc: Option<&Document>) {
reshape_leaves_within(art, fx, Budget::EFFECTS, doc);
}
/// [`reshape_leaves`] whose paths keep to `budget` together, or to what they cost.
pub(crate) fn reshape_leaves_within(art: &mut Node, fx: &[&Effect], budget: Budget) {
reshape_leaves_apart(art, fx, budget, false, None);
pub(crate) fn reshape_leaves_within(art: &mut Node, fx: &[&Effect], budget: Budget, doc: Option<&Document>) {
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
@@ -1247,7 +1244,7 @@ pub(crate) fn outlined_members(doc: Option<&Document>, n: &Node) -> Node {
/// ([`evaluated_with`]).
pub(crate) fn outlined_members_with(doc: Option<&Document>, n: &Node, outline: OutlineHook, nodes: &SymbolNodes) -> Node {
let mut art = evaluated_with(doc, n, outline, nodes);
outline_nested_text(&mut art, false);
outline_nested_text(doc, &mut art, false);
art
}
+1 -1
View File
@@ -434,7 +434,7 @@ mod tests {
let expanded = expand_revolve_art(&n).unwrap();
let count = |n: &Node| n.children().unwrap()[0].children().unwrap().len();
assert!(count(&expanded) < count(&live) * 3 / 4, "hidden back surfaces are removed");
assert_eq!(crate::bake_appearance(&n), Some(live.clone()), "other baking retains the full surface");
assert_eq!(crate::bake_appearance(None, &n), Some(live.clone()), "other baking retains the full surface");
n.appearance.effects[0].params["expandVisibleOnly"] = json!(false);
assert_eq!(revolve_art(&n), Some(live.clone()), "the live view ignores the expansion setting");
assert_eq!(expand_revolve_art(&n), Some(live));
+2 -2
View File
@@ -325,7 +325,7 @@ fn export_and_expand_bake_faces_with_their_own_ids() {
let mut n = square_node();
n.id = d.alloc_id();
n.appearance.effects.push(fx(id, json!({"bevel": "rounded"})));
assert_eq!(crate::bake_appearance(&n), solid_art(&n), "{id}");
assert_eq!(crate::bake_appearance(None, &n), solid_art(&n), "{id}");
let mut stroke = |_: &mut Document, _: &PathData, _: FillRule, _: &StrokeLayer| None;
let art = crate::expand_leaf(&mut d, &n, &mut stroke).unwrap();
let mut ids = std::collections::HashSet::new();
@@ -351,7 +351,7 @@ fn a_twisted_tapered_hollow_inflate_exports_and_expands() {
let params = json!({"twist": 120, "taper": 50, "cap": false, "bothSides": true, "rotationX": -20, "rotationY": 35});
n.appearance.effects.push(fx(INFLATE, params));
let art = solid_art(&n).unwrap();
assert_eq!(crate::bake_appearance(&n), Some(art.clone()));
assert_eq!(crate::bake_appearance(None, &n), Some(art.clone()));
assert!(faces(&art).len() > 20, "{} faces", faces(&art).len());
let mut stroke = |_: &mut Document, _: &PathData, _: FillRule, _: &StrokeLayer| None;
let expanded = crate::expand_leaf(&mut d, &n, &mut stroke).unwrap();
+5 -5
View File
@@ -394,7 +394,7 @@ fn stacked_effects_on_art_stay_within_the_anchor_budget() {
let leaf = |x: f64| Arc::new(Node::path(id, shapes::rectangle(Rect::new(x, 0.0, x + 100.0, 100.0)), Appearance::default()));
let mut art = Node::group(id, (0..10).map(|k| leaf(k as f64 * 120.0)).collect());
let rough = fx("distort.roughen", json!({"size": 50, "detail": 100}));
crate::reshape::reshape_leaves_within(&mut art, &[&rough, &rough, &rough], Budget { anchors: 5_000, lines: usize::MAX });
crate::reshape::reshape_leaves_within(&mut art, &[&rough, &rough, &rough], Budget { anchors: 5_000, lines: usize::MAX }, None);
let mut total = 0;
let mut reshaped = 0;
for c in art.children().into_iter().flatten() {
@@ -473,7 +473,7 @@ fn reshape_leaves_within_optimizes_transform_before_terminal_offset_path() {
let offset = fx("path.offsetPath", json!({"offset": 4.0, "joins": "round"}));
let start = std::time::Instant::now();
crate::reshape::reshape_leaves_within(&mut art, &[&transform, &offset], Budget::EFFECTS);
crate::reshape::reshape_leaves_within(&mut art, &[&transform, &offset], Budget::EFFECTS, None);
let elapsed = start.elapsed();
assert!(elapsed < std::time::Duration::from_secs(2));
@@ -500,7 +500,7 @@ fn reshape_leaves_within_preserves_order_when_not_terminal_or_not_style_safe() {
let mut art_evenodd = Node::group(id, vec![Arc::new(leaf)]);
let transform = fx("distort.transform", json!({"moveH": 2.0, "moveV": 2.0, "copies": 2}));
let offset = fx("path.offsetPath", json!({"offset": 4.0}));
crate::reshape::reshape_leaves_within(&mut art_evenodd, &[&transform, &offset], Budget::EFFECTS);
crate::reshape::reshape_leaves_within(&mut art_evenodd, &[&transform, &offset], Budget::EFFECTS, None);
let p = art_evenodd.children().unwrap()[0].path_data().unwrap();
assert_eq!(p.subpaths.len(), 1, "EvenOdd does not swap; copies union into 1 outer subpath");
@@ -508,7 +508,7 @@ fn reshape_leaves_within_preserves_order_when_not_terminal_or_not_style_safe() {
let leaf_round = Arc::new(Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 20.0, 20.0)), Appearance::default()));
let mut art_round = Node::group(id, vec![leaf_round]);
let round_corners = fx("stylize.roundCorners", json!({"radius": 2.0}));
crate::reshape::reshape_leaves_within(&mut art_round, &[&transform, &offset, &round_corners], Budget::EFFECTS);
crate::reshape::reshape_leaves_within(&mut art_round, &[&transform, &offset, &round_corners], Budget::EFFECTS, None);
let p = art_round.children().unwrap()[0].path_data().unwrap();
assert_eq!(p.subpaths.len(), 1, "Non-terminal Offset Path does not swap; copies union into 1 outer subpath");
@@ -516,7 +516,7 @@ fn reshape_leaves_within_preserves_order_when_not_terminal_or_not_style_safe() {
let leaf_miter = Arc::new(Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 20.0, 20.0)), Appearance::default()));
let mut art_miter = Node::group(id, vec![leaf_miter]);
let offset_miter = fx("path.offsetPath", json!({"offset": 4.0, "joins": "miter"}));
crate::reshape::reshape_leaves_within(&mut art_miter, &[&transform, &offset_miter], Budget::EFFECTS);
crate::reshape::reshape_leaves_within(&mut art_miter, &[&transform, &offset_miter], Budget::EFFECTS, None);
let p = art_miter.children().unwrap()[0].path_data().unwrap();
assert_eq!(p.subpaths.len(), 1, "Miter join does not swap; copies union into 1 outer subpath");
}
+3 -3
View File
@@ -189,7 +189,7 @@ fn make(s: &mut Session, p: &Value) -> Result<Value> {
/// that has a shape. A shape taken from inside a group, or from the art of a blend, an envelope or
/// a warped group, also takes that object's opacity and blend mode.
fn top_style(d: &Document, n: &Node, hook: effects::OutlineHook) -> Option<Style> {
if let Some(art) = effects::shape_art(n, Some(hook)) {
if let Some(art) = effects::shape_art(Some(d), n, Some(hook)) {
let mut lv = vec![];
pf_leaves(d, &art, &mut lv);
let inside = matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) && !art.is_compound_shape();
@@ -199,7 +199,7 @@ fn top_style(d: &Document, n: &Node, hook: effects::OutlineHook) -> Option<Style
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } if !n.is_compound_shape() => {
children.iter().rev().find_map(|c| top_style(d, c, hook)).map(|s| s.within(n))
}
NodeKind::SymbolInstance { .. } => effects::member_leaves(n, Some(hook)).last().map(style_of),
NodeKind::SymbolInstance { .. } => effects::member_leaves(Some(d), n, Some(hook)).last().map(style_of),
_ => {
let mut lv = vec![];
pf_leaves(d, n, &mut lv);
@@ -267,7 +267,7 @@ fn expand(s: &mut Session, _: &Value) -> Result<Value> {
for id in &shapes {
let shape = d.node(*id).cloned().ok_or(EngineError::NoNode(*id))?;
let hook = |c: &Node| effects::outline_object(Some(d), c);
let area = effects::compound_shape_area(&shape, Some(&hook))
let area = effects::compound_shape_area(Some(d), &shape, Some(&hook))
.ok_or_else(|| EngineError::Other("the compound shape is too complex to expand".into()))?;
if area.is_empty() {
return Err(EngineError::Other(format!("{LABEL} produced an empty result")));
+18 -15
View File
@@ -687,7 +687,7 @@ fn expand_live_with_transparent_image_patterns(
let Some(n) = d.node(*id) else { continue };
// Random Transform effects take their seed from the ids: bake them first ([`bake_seeded`]),
// and leave the object live when that can't be done.
let Some(mut g) = bake_seeded(&vectorcraft_render::effects::expand_live_deep(Some(d), n)) else { continue };
let Some(mut g) = bake_seeded(d, &vectorcraft_render::effects::expand_live_deep(Some(d), n)) else { continue };
// An object whose art would take the expanded art past the budget stays live.
let size = g.count_with_masks();
if size > budget - made.get() {
@@ -856,7 +856,7 @@ fn pattern_tiles(d: &mut Document, def: &PatternDef, xf: Affine, bounds: Rect, b
if !moves_only(xf) && def.art.iter().any(|a| has_instances(a)) {
return Some(vec![]);
}
let Some(def) = seeds_baked(def) else {
let Some(def) = seeds_baked(d, def) else {
return Some(vec![]);
};
let brushes = vectorcraft_brush::library(d);
@@ -889,13 +889,14 @@ fn pattern_tiles(d: &mut Document, def: &PatternDef, xf: Affine, bounds: Rect, b
/// effect baked into filled art ([`bake_appearance`](vectorcraft_render::effects::bake_appearance))
/// while it keeps its own id: the effect takes its seed from the id, and the canvas draws the art
/// with those ids for every tile, while the tiles get ids of their own. `None` when something
/// other than a path has such an effect.
fn seeds_baked(def: &PatternDef) -> Option<std::borrow::Cow<'_, PatternDef>> {
/// other than a path has such an effect. The swatches and patterns that live-effect plug-ins
/// painting their pieces name are looked up in `d`.
fn seeds_baked<'a>(d: &Document, def: &'a PatternDef) -> Option<std::borrow::Cow<'a, PatternDef>> {
if !def.art.iter().any(|a| has_seeded(a)) {
return Some(std::borrow::Cow::Borrowed(def));
}
let mut def = def.clone();
def.art = def.art.iter().map(|a| bake_seeded(a).map(Arc::new)).collect::<Option<_>>()?;
def.art = def.art.iter().map(|a| bake_seeded(d, a).map(Arc::new)).collect::<Option<_>>()?;
Some(std::borrow::Cow::Owned(def))
}
@@ -1001,8 +1002,9 @@ pub(crate) fn has_seeded(n: &Node) -> bool {
}
/// `n` with the paths in it that have a random Transform effect baked ([`seeds_baked`]). `None`
/// when something else has one.
fn bake_seeded(n: &Node) -> Option<Node> {
/// when something else has one. The swatches and patterns that live-effect plug-ins painting their
/// pieces name are looked up in `d`.
fn bake_seeded(d: &Document, n: &Node) -> Option<Node> {
if !has_seeded(n) {
return Some(n.clone());
}
@@ -1010,17 +1012,17 @@ fn bake_seeded(n: &Node) -> Option<Node> {
if !matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) {
return None;
}
vectorcraft_render::effects::bake_appearance(n)?
vectorcraft_render::effects::bake_appearance(Some(d), n)?
} else {
n.clone()
};
if !matches!(n.kind, NodeKind::Compound { .. }) {
for c in n.children_mut().into_iter().flatten() {
*c = Arc::new(bake_seeded(c)?);
*c = Arc::new(bake_seeded(d, c)?);
}
}
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
m.art = Arc::new(bake_seeded(&m.art)?);
m.art = Arc::new(bake_seeded(d, &m.art)?);
}
Some(n)
}
@@ -1041,7 +1043,7 @@ fn has_unbakeable_effects(n: &Node) -> bool {
/// paths baked ([`baked`], [`outline_brushed`]) and its live shapes turned into plain paths (a
/// stretch would rebuild their corners round), in its opacity masks too.
fn prepared(d: &mut Document, brushes: &[vectorcraft_brush::Brush], n: &Node) -> Node {
let mut t = baked(n);
let mut t = baked(d, n);
outline_brushed(d, brushes, &mut t);
drop_live(&mut t);
prepare_masks(d, brushes, &mut t);
@@ -1082,14 +1084,15 @@ fn scales_evenly(a: Affine) -> bool {
}
/// `n` with the strokes and geometry effects of each path in it baked into filled art
/// ([`bake_appearance`](vectorcraft_render::effects::bake_appearance)).
fn baked(n: &Node) -> Node {
if let Some(b) = vectorcraft_render::effects::bake_appearance(n) {
/// ([`bake_appearance`](vectorcraft_render::effects::bake_appearance)), with the swatches and
/// patterns that live-effect plug-ins painting their pieces name looked up in `d`.
fn baked(d: &Document, n: &Node) -> Node {
if let Some(b) = vectorcraft_render::effects::bake_appearance(Some(d), n) {
return b;
}
let mut n = n.clone();
for c in n.children_mut().into_iter().flatten() {
*c = Arc::new(baked(c));
*c = Arc::new(baked(d, c));
}
n
}
+1 -1
View File
@@ -307,7 +307,7 @@ pub(crate) fn pf_leaves(d: &Document, n: &Node, out: &mut Vec<Node>) {
match &n.kind {
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
let hook = |c: &Node| vectorcraft_render::effects::outline_object(Some(d), c);
match vectorcraft_render::effects::compound_shape_art(n, Some(&hook)) {
match vectorcraft_render::effects::compound_shape_art(Some(d), n, Some(&hook)) {
Some(art) => out.push(art),
None => children.iter().for_each(|c| pf_leaves(d, c, out)),
}
+8 -8
View File
@@ -68,7 +68,7 @@ fn make_and_expand_each_shape_mode() {
assert_eq!(members, vec![back, front], "the members, in their stacking order");
assert!(shape.children().unwrap().iter().all(|m| m.extras().shape_mode.name() == mode));
assert_eq!(shape.appearance.fill_paint().color(), Some(fill), "{mode}");
let drawn = effects::compound_shape_area(&shape, None).unwrap();
let drawn = effects::compound_shape_area(None, &shape, None).unwrap();
assert!((area(&drawn) - want).abs() < 0.5, "{mode}: {}", area(&drawn));
// Expand: the path, or compound path, of the area, painted as the compound shape.
let made = run(&mut s, "object.compoundShape.expand", json!({}));
@@ -118,7 +118,7 @@ fn a_selected_member_takes_a_shape_mode() {
assert_eq!(cmd::selected_member_modes(&s), vec![ShapeMode::Add]);
run(&mut s, "object.compoundShape.setMode", json!({"mode": "intersect"}));
assert_eq!(cmd::selected_member_modes(&s), vec![ShapeMode::Intersect]);
assert!((area(&effects::compound_shape_area(&node(&s, id), None).unwrap()) - 5_000.0).abs() < 0.5);
assert!((area(&effects::compound_shape_area(None, &node(&s, id), None).unwrap()) - 5_000.0).abs() < 0.5);
// The same mode again is no edit.
let undo = s.doc().unwrap().history.undo.len();
run(&mut s, "object.compoundShape.setMode", json!({"mode": "intersect"}));
@@ -208,7 +208,7 @@ fn make(s: &mut Session, mode: &str) -> NodeId {
fn drawn(s: &Session, id: NodeId) -> f64 {
let d = &s.doc().unwrap().doc;
let hook = |c: &Node| effects::outline_object(Some(d), c);
area(&effects::compound_shape_area(&node(s, id), Some(&hook)).unwrap())
area(&effects::compound_shape_area(None, &node(s, id), Some(&hook)).unwrap())
}
/// A bar (0–100 across, 10–30 down) behind a blend of two 20 pt squares at 0 and 80 (0–20 down)
@@ -243,7 +243,7 @@ fn a_blend_is_a_member_as_its_steps() {
let doc = s.doc().unwrap().doc.clone();
let back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap();
assert_eq!(back.node(id), doc.node(id));
assert!((area(&effects::compound_shape_area(back.node(id).unwrap(), None).unwrap()) - more).abs() < 0.5);
assert!((area(&effects::compound_shape_area(None, back.node(id).unwrap(), None).unwrap()) - more).abs() < 0.5);
run(&mut s, "select.set", json!({"ids": [id.0]}));
let made = run(&mut s, "object.compoundShape.expand", json!({}));
assert!((node_area(&node(&s, NodeId(made["ids"][0].as_u64().unwrap()))) - more).abs() < 0.5, "{mode}");
@@ -472,8 +472,8 @@ fn branching_symbol_art_stops_at_its_budget() {
}
let d = &s.doc().unwrap().doc;
let hook = |c: &Node| effects::outline_object(Some(d), c);
assert!(effects::compound_shape_area(&node(&s, id), Some(&hook)).is_none(), "past the budget");
let leaves = effects::member_leaves(&node(&s, group), Some(&hook));
assert!(effects::compound_shape_area(None, &node(&s, id), Some(&hook)).is_none(), "past the budget");
let leaves = effects::member_leaves(None, &node(&s, group), Some(&hook));
assert!(!leaves.is_empty() && leaves.len() as u64 <= effects::SYMBOL_ART_MAX_NODES, "{} shapes", leaves.len());
}
@@ -563,7 +563,7 @@ fn a_warped_symbol_instance_is_a_member_as_it_draws() {
assert!((warped - 400.0).abs() > 10.0, "the warp changes the area: {warped}");
assert!((drawn(&s, id) - (warped + 100.0)).abs() < 1.0, "{} vs {}", drawn(&s, id), warped + 100.0);
let hook = |c: &Node| effects::outline_object(Some(d), c);
let alone: f64 = effects::member_leaves(&node(&s, a), Some(&hook)).iter().map(node_area).sum();
let alone: f64 = effects::member_leaves(None, &node(&s, a), Some(&hook)).iter().map(node_area).sum();
assert!((alone - warped).abs() < 1.0, "the instance alone: {alone} vs {warped}");
}
@@ -616,7 +616,7 @@ fn type_in_symbol_art_counts_as_its_outlines() {
let unwarped = {
let d = &s.doc().unwrap().doc;
let hook = |c: &Node| effects::outline_object(Some(d), c);
effects::member_leaves(&node(&s, a), Some(&hook)).iter().map(node_area).sum::<f64>()
effects::member_leaves(None, &node(&s, a), Some(&hook)).iter().map(node_area).sum::<f64>()
};
assert!(unwarped > 200.0, "the glyphs: {unwarped}");
run(&mut s, "select.set", json!({"ids": [a.0]}));
+113
View File
@@ -409,6 +409,119 @@ fn a_painted_group_effect_links_to_the_document_s_swatches() {
assert_eq!((img.pixel(50, 50), img.pixel(15, 15)), (GREEN, WHITE), "the group's piece, green");
}
/// The output of a painted effect that returns one green square linked to swatch `Leaf`.
fn leaf_square() -> String {
let mut leaf = solid("#00ff00");
leaf["swatch"] = json!("Leaf");
json!({"objects": [{"path": square_at(40.0), "fills": [leaf]}]}).to_string()
}
/// A Pathfinder operation and baked appearance take a painted effect's pieces, swatch links
/// included, as the canvas draws them: a group with a painted effect inside a group with a live
/// Pathfinder effect draws the green piece, and baking a path's appearance gives the piece linked
/// to the swatch.
#[test]
fn pathfinder_and_baked_appearance_take_painted_pieces_with_swatch_links() {
let mut s = session();
let id = rect(&mut s, 10.0, "#ff0000");
run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"}));
install_wat(&mut s, &tpl::returning(&painting("test.engine.leafpaint", true), &leaf_square()));
run(&mut s, "select.set", json!({"ids": [id.0]}));
let inner = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
run(&mut s, "effect.apply", json!({"ids": [inner.0], "effect": "plugin.test.engine.leafpaint"}));
let outer = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
run(&mut s, "effect.apply", json!({"ids": [outer.0], "effect": "pathfinder.add"}));
let img = drawn(&s);
assert_eq!((img.pixel(50, 50), img.pixel(15, 15)), (GREEN, WHITE), "the Pathfinder operation takes the green piece");
let id = rect(&mut s, 120.0, "#ff0000");
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.leafpaint"}));
let st = s.doc().unwrap();
let baked = vectorcraft_render::effects::bake_appearance(Some(&st.doc), st.doc.node(id).unwrap()).unwrap();
let mut fills = vec![];
baked.walk(&mut |n| {
if let Some(p) = n.path_data() {
fills.push((p.bounds(), n.appearance.fill().map(|f| f.paint.clone())));
}
});
let mut leaf = Paint::solid(Color::from_hex("#00ff00").unwrap());
if let Paint::Solid { swatch, .. } = &mut leaf {
*swatch = Some("Leaf".into());
}
assert_eq!(fills, [(square_at(40.0).bounds(), Some(leaf))]);
}
/// A path with a painted effect takes part in a Pathfinder operation as its pieces, each with its
/// own paint and the path's opacity: Divide on a group holding it makes one face per piece,
/// painted as the piece is.
#[test]
fn a_path_with_a_painted_effect_takes_part_in_pathfinder_as_its_pieces() {
let mut s = session();
let id = rect(&mut s, 10.0, "#ff0000");
let out = json!({"objects": [{"path": square_at(40.0), "fills": [solid("#00ff00")]}, {"path": square_at(80.0), "fills": [solid("#0000ff")]}]})
.to_string();
install_wat(&mut s, &tpl::returning(&painting("test.engine.twopaint", true), &out));
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.twopaint"}));
run(&mut s, "select.set", json!({"ids": [id.0]}));
let g = NodeId(run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap());
run(&mut s, "effect.apply", json!({"ids": [g.0], "effect": "pathfinder.divide"}));
let img = drawn(&s);
assert_eq!((img.pixel(50, 50), img.pixel(90, 50), img.pixel(15, 15)), (GREEN, BLUE, WHITE), "one face per piece");
run(&mut s, "select.set", json!({"ids": [id.0]}));
run(&mut s, "transparency.set", json!({"item": null, "opacity": 50}));
let st = s.doc().unwrap();
let hook = |c: &vectorcraft_doc::Node| vectorcraft_render::effects::outline_object(Some(&st.doc), c);
let faces = vectorcraft_render::effects::pathfinder_children(Some(&st.doc), st.doc.node(g).unwrap(), Some(&hook)).unwrap();
let faces: Vec<_> = faces.iter().map(|f| (f.appearance.fill().map(|f| f.paint.clone()), f.opacity)).collect();
let fill = |hex: &str| Some(Paint::solid(Color::from_hex(hex).unwrap()));
assert_eq!(faces, [(fill("#0000ff"), 0.5), (fill("#00ff00"), 0.5)]);
}
/// Art a renderer keeps for an object with a painted effect is made again when a swatch its pieces
/// name leaves the document (the object is then drawn as if the effect weren't there) and when
/// Undo brings the swatch back.
#[test]
fn kept_art_follows_the_swatches_painted_pieces_name() {
let mut s = session();
let id = rect(&mut s, 10.0, "#ff0000");
run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"}));
install_wat(&mut s, &tpl::returning(&painting("test.engine.keptpaint", true), &leaf_square()));
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.keptpaint"}));
let mut r = vectorcraft_render::Renderer::new();
let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut draw = |s: &Session| {
let img = r.render(&s.doc().unwrap().doc, 200, 200, vectorcraft_geom::Affine::IDENTITY, &opts);
(img.pixel(50, 50), img.pixel(15, 15))
};
assert_eq!(draw(&s), (GREEN, WHITE), "the piece, green");
run(&mut s, "swatch.delete", json!({"name": "Leaf"}));
assert_eq!(draw(&s), (WHITE, RED), "without the swatch, the object as it is");
run(&mut s, "edit.undo", json!({}));
assert_eq!(draw(&s), (GREEN, WHITE), "the swatch back, the piece again");
}
/// Art a renderer keeps for an object whose plug-in starts to paint its pieces when it is replaced
/// follows the swatches those pieces name from then on.
#[test]
fn kept_art_follows_the_swatches_once_a_replaced_plugin_paints() {
let mut s = session();
let id = rect(&mut s, 10.0, "#ff0000");
run(&mut s, "swatch.new", json!({"name": "Leaf", "color": "#00ff00"}));
install_wat(&mut s, &tpl::returning(&painting("test.engine.laterpaint", false), &leaf_square()));
run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "plugin.test.engine.laterpaint"}));
let mut r = vectorcraft_render::Renderer::new();
let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut draw = |s: &Session| {
let img = r.render(&s.doc().unwrap().doc, 200, 200, vectorcraft_geom::Affine::IDENTITY, &opts);
(img.pixel(50, 50), img.pixel(15, 15))
};
assert_eq!(draw(&s), (RED, WHITE), "the square in the object's paint");
install_wat(&mut s, &tpl::returning(&painting("test.engine.laterpaint", true), &leaf_square()));
assert_eq!(draw(&s), (GREEN, WHITE), "the piece, green");
run(&mut s, "swatch.delete", json!({"name": "Leaf"}));
assert_eq!(draw(&s), (WHITE, RED), "without the swatch, the object as it is");
}
/// Every painted piece counts against the effects' budget, however small its path: two painted
/// effects in a stack that each return 350 tiny triangles make no more than 100,000 objects, not
/// 350 × 350 of them.
+1 -1
View File
@@ -5852,7 +5852,7 @@ fn each_pathfinder_operation_on_a_group_draws_as_vectorcraft_draws_it() {
// Each result's box and paints; Outline's lines start where each square's path starts, so
// the results are compared in a sorted order.
let faces = |g: &Node| -> Vec<String> {
let mut faces: Vec<String> = vectorcraft_effects::pathfinder_children(g, None)
let mut faces: Vec<String> = vectorcraft_effects::pathfinder_children(None, g, None)
.unwrap()
.iter()
.map(|n| {
+11 -7
View File
@@ -314,17 +314,21 @@ impl Inputs {
/// What raster contents read from the document by name or key, and the color settings, as a frame
/// finds them ([`Renderer::doc_inputs`]): hashes of the patterns' tilings
/// ([`crate::pattern::hash_tiling`]), of the symbols' names and art and of the images' keys and
/// pixels, the address of the color settings, and whether the patterns' and symbols' art has a
/// plug-in effect. Art, pixels and previews are hashed by the addresses of their shared values,
/// and names, keys, tiles, tile types and overlaps are hashed by value. The live cache keeps these
/// inputs with each evaluated live object, with each mesh's quads and with what each walk of a
/// live object or of effect art found when it found no placed file (`crate::live`), and
/// [`Renderer::fx_art`] keeps them with art that can be made from evaluated live objects.
/// ([`crate::pattern::hash_tiling`]), of the symbols' names and art, of the images' keys and
/// pixels and of the swatches' and patterns' names, the address of the color settings, and whether
/// the patterns' and symbols' art has a plug-in effect. Art, pixels and previews are hashed by the
/// addresses of their shared values, and names, keys, tiles, tile types and overlaps are hashed by
/// value. The live cache keeps these inputs with each evaluated live object, with each mesh's quads
/// and with what each walk of a live object or of effect art found when it found no placed file
/// (`crate::live`), and [`Renderer::fx_art`] keeps them with art that can be made from evaluated
/// live objects.
pub(crate) struct DocInputs {
pub(super) patterns: u64,
pub(super) symbols: u64,
pub(super) images: u64,
/// The swatches' and patterns' names, which the pieces of live-effect plug-ins that paint them
/// may name (`crate::live`).
pub(crate) names: u64,
pub(crate) cms: usize,
/// Whether an object in a pattern's or a symbol's art, or in an opacity mask there, has a
/// visible plug-in effect.
+7 -3
View File
@@ -470,15 +470,19 @@ impl Renderer {
(key, Arc::as_ptr(&blob.bytes) as usize, blob.proxy.as_ref().map(|p| Arc::as_ptr(p) as usize)).hash(h);
}
});
let found = (patterns, symbols, images, Arc::as_ptr(&cms) as usize);
let names = hash(&mut |h| {
doc.swatches_iter().for_each(|s| s.name.hash(h));
doc.patterns.iter().for_each(|p| p.name.hash(h));
});
let found = (patterns, symbols, images, names, Arc::as_ptr(&cms) as usize);
let d = match &self.doc_inputs {
Some((_, d)) if (d.patterns, d.symbols, d.images, d.cms) == found => d.clone(),
Some((_, d)) if (d.patterns, d.symbols, d.images, d.names, d.cms) == found => d.clone(),
_ => {
let doc_art = || doc.patterns.iter().flat_map(|p| &p.art).chain(doc.symbols.iter().map(|s| &s.art));
let art = doc_art().map(Arc::downgrade).collect();
let pixels = doc.images.values().flat_map(|b| std::iter::once(&b.bytes).chain(&b.proxy)).map(Arc::downgrade).collect();
let plugins = doc_art().any(|a| crate::holds_plugin_effect(a));
Arc::new(DocInputs { patterns, symbols, images, cms: found.3, plugins, _held: (art, pixels, Arc::downgrade(&cms)) })
Arc::new(DocInputs { patterns, symbols, images, names, cms: found.4, plugins, _held: (art, pixels, Arc::downgrade(&cms)) })
}
};
self.doc_inputs = Some((at, d.clone()));
+5 -3
View File
@@ -53,9 +53,10 @@ pub(super) fn evaluates_live(n: &Node) -> bool {
/// Can the art [`Renderer::fx_art`] makes for `n` change while `n` stays the same, other than
/// through live objects it evaluates? It can when `n` holds type (laid out in the fonts installed),
/// a symbol instance (made from its symbol's art), an object with a 3D effect or a color
/// adjustment (shaded or recolored through the color settings), or one with Vector Halftone (its
/// dots sampled from everything it draws), in it or in the opacity masks of
/// the objects in it.
/// adjustment (shaded or recolored through the color settings), one with Vector Halftone (its
/// dots sampled from everything it draws), or one with a live-effect plug-in that paints its pieces
/// (drawn only while the swatches and patterns they name are in the document), in it or in the
/// opacity masks of the objects in it.
pub(super) fn reads_outside(n: &Node) -> bool {
let mut reads = false;
n.walk(&mut |c| {
@@ -64,6 +65,7 @@ pub(super) fn reads_outside(n: &Node) -> bool {
|| effects::has_solid(c)
|| effects::has_adjustment(c)
|| effects::addons::halftone::has_halftone(c)
|| effects::has_painted_plugin_effect(c)
|| c.extras().mask.as_ref().is_some_and(|m| reads_outside(&m.art))
});
reads
+21 -16
View File
@@ -99,17 +99,19 @@ pub fn expand_live(n: &Node) -> Vec<Node> {
/// the fonts, the plug-ins and the color settings, hashed: the art of the symbols, the tilings of
/// the patterns and the reads of the placed files it shows ([`hash_resources`]), the font
/// generation ([`vectorcraft_text::FontDb::generation`]) when it shows type or a placed file, the
/// plug-in revision ([`crate::effects::plugin_revision`]) when it shows an object with a plug-in
/// effect, and the color settings (`cms`, their address). Art is hashed by the addresses of its
/// `Arc`s, which copies of the document share: a copy of the document has the same key. It also
/// returns weak references to the reads found, and what the walk found ([`Walked`]) when it found
/// no placed file.
fn resources_key(doc: &Document, cms: usize, n: &Node) -> (u64, Vec<Weak<ReadDocument>>, Option<Walked>) {
/// plug-in revision ([`crate::effects::plugin_revision`]) and `names` (the hash of the document's
/// swatch and pattern names, [`DocInputs`]) when it shows an object with a plug-in effect, since a
/// plug-in that paints its pieces ([`crate::effects::has_painted_plugin_effect`]) draws them only
/// while the swatches and patterns they name are in the document, and the color settings (`cms`,
/// their address). Art is hashed by the addresses of its `Arc`s, which copies of the document
/// share: a copy of the document has the same key. It also returns weak references to the reads
/// found, and what the walk found ([`Walked`]) when it found no placed file.
fn resources_key(doc: &Document, cms: usize, names: u64, n: &Node) -> (u64, Vec<Weak<ReadDocument>>, Option<Walked>) {
let mut h = DefaultHasher::new();
let mut shown = Shown::default();
hash_resources(doc, n, &mut shown, &mut h);
let walked = Walked { hash: h.finish(), text: shown.text, plugins: shown.plugins };
(walked.key(cms), shown.reads, (!shown.placed).then_some(walked))
(walked.key(cms, names), shown.reads, (!shown.placed).then_some(walked))
}
/// What [`hash_resources`] found in a node: the hash of what it walked, whether it found type or
@@ -123,9 +125,11 @@ struct Walked {
impl Walked {
/// The [`resources_key`] of the node walked: the walk's hash, the font generation when it found
/// type or a placed file, the plug-in revision when it found a plug-in effect, and the color
/// settings `cms`.
fn key(self, cms: usize) -> u64 {
/// type or a placed file, the plug-in revision and the hash of the document's swatch and pattern
/// names (`names`) when it found a plug-in effect, and the color settings `cms`. Whether a
/// plug-in paints its pieces depends on the plug-ins installed, so the names count for every
/// plug-in effect.
fn key(self, cms: usize, names: u64) -> u64 {
let mut h = DefaultHasher::new();
self.hash.hash(&mut h);
if self.text {
@@ -133,6 +137,7 @@ impl Walked {
}
if self.plugins {
crate::effects::plugin_revision().hash(&mut h);
names.hash(&mut h);
}
cms.hash(&mut h);
h.finish()
@@ -445,13 +450,13 @@ impl Renderer {
&& std::ptr::eq(node.as_ptr(), Arc::as_ptr(n))
&& read.same_art(&inputs)
{
return (walked.key(inputs.cms), inputs, vec![]);
return (walked.key(inputs.cms, inputs.names), inputs, vec![]);
}
#[cfg(test)]
{
self.live.walked += 1;
}
let (key, reads, walked) = resources_key(doc, inputs.cms, n);
let (key, reads, walked) = resources_key(doc, inputs.cms, inputs.names, n);
if let Some(walked) = walked {
// The bytes count the key and the entry, and the node's allocation that the weak
// reference keeps.
@@ -478,7 +483,7 @@ impl Renderer {
self.live.evaluated += 1;
}
let hook = |c: &Node| crate::effects::outline_object(Some(doc), c);
let v: Expanded = match crate::effects::pathfinder_children(a, Some(&hook)) {
let v: Expanded = match crate::effects::pathfinder_children(Some(doc), a, Some(&hook)) {
Some(children) => Arc::new(children),
None => Arc::new(crate::effects::expand_live(Some(doc), a).into_iter().map(Arc::new).collect()),
};
@@ -788,7 +793,7 @@ mod tests {
let envelope = Node::new(d.alloc_id(), envelope);
let key = |d: &Document| {
std::thread::scope(|s| {
std::thread::Builder::new().stack_size(256 << 10).spawn_scoped(s, || resources_key(d, 0, &envelope).0).unwrap().join().unwrap()
std::thread::Builder::new().stack_size(256 << 10).spawn_scoped(s, || resources_key(d, 0, 0, &envelope).0).unwrap().join().unwrap()
})
};
let before = key(&d);
@@ -820,8 +825,8 @@ mod tests {
frame: Box::new(Affine::IDENTITY),
};
let envelope = Node::new(d.alloc_id(), envelope);
let before = resources_key(&d, 0, &envelope).0;
let before = resources_key(&d, 0, 0, &envelope).0;
d.symbols[2].art = y.clone();
assert_ne!(resources_key(&d, 0, &envelope).0, before, "the art of every instance is hashed");
assert_ne!(resources_key(&d, 0, 0, &envelope).0, before, "the art of every instance is hashed");
}
}
+3 -1
View File
@@ -421,7 +421,9 @@ is a piece drawn by itself, in the order returned, with paints of its own:
- An effect on a fill (`_context.item` `{"fill": i}`) returns only `fills`, which replace that fill on each piece; an
effect on a stroke returns only `strokes`. The object's other fills and strokes stay where they are.
- The effects after it in the stack apply to the pieces together, the reference box being the bounds of all of them.
Bounds, hit tests and raster effects use the pieces as they are drawn. Each piece counts against the budget of the
Bounds, hit tests, raster effects, Pathfinder effects, compound shapes and Envelope Options › Distort Appearance use
the pieces as they are drawn. For an effect on a fill or stroke, Pathfinder effects and compound shapes use the
object's path, as they do for every other effect on a fill or stroke. Each piece counts against the budget of the
object's geometry effects as well as its anchors, so all the painted effects in a stack make at most 100,000 pieces
together; a run whose pieces would pass the budget draws its input as it was.
- The result is checked whole before any of it is drawn: the limits of a filter's output (100,000 objects, 4,000,000