mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 17:44:02 +00:00
Merge pull request #6560 from dotCooCoo/fix/pasted-svg-filter-names
Effects: pasted art keeps an SVG filter whose name the document gives another filter
This commit is contained in:
@@ -21,8 +21,8 @@ use serde_json::{Map, Value, json};
|
||||
use vectorcraft_brush::Brush;
|
||||
use vectorcraft_color::{Color, Paint, Swatch};
|
||||
use vectorcraft_doc::{
|
||||
Appearance, AppearanceItem, Document, GraphicStyle, Node, NodeKind, PatternDef, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration,
|
||||
TextStyleDef,
|
||||
Appearance, AppearanceItem, Document, Effect, GraphicStyle, Node, NodeKind, PatternDef, Symbol, SymbolExportType, SymbolNineSlice,
|
||||
SymbolRegistration, TextStyleDef,
|
||||
};
|
||||
|
||||
use super::super::brushsym::{set_natural_size, stored_natural_size};
|
||||
@@ -140,6 +140,31 @@ fn node_refs(n: &Node, f: Visit) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The `(name, xml)` of `e` when it is an SVG filter effect.
|
||||
fn filter_of(e: &Effect) -> Option<(String, String)> {
|
||||
let text = |k: &str| e.params.get(k).and_then(Value::as_str).map(str::to_string);
|
||||
if e.id != Appearance::SVG_FILTER {
|
||||
return None;
|
||||
}
|
||||
Some((text("name")?, text("xml")?))
|
||||
}
|
||||
|
||||
/// The SVG filter effects of appearance `a` (its own and its fills' and strokes').
|
||||
fn appearance_filters(a: &Appearance, out: &mut BTreeSet<(String, String)>) {
|
||||
let items = a.items.iter().flat_map(|i| i.effects().iter());
|
||||
out.extend(a.effects.iter().chain(items).filter_map(filter_of));
|
||||
}
|
||||
|
||||
/// The SVG filter effects in the subtree of `n` (opacity-mask art included).
|
||||
fn node_filters(n: &Node, out: &mut BTreeSet<(String, String)>) {
|
||||
n.walk(&mut |m| {
|
||||
appearance_filters(&m.appearance, out);
|
||||
if let Some(mask) = &m.extras().mask {
|
||||
node_filters(&mask.art, out);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A global or spot colour of its own (not a tint of another swatch): what colours link to.
|
||||
fn is_base(w: &Swatch) -> bool {
|
||||
(w.global || w.spot) && matches!(w.paint, Paint::Solid { swatch: None, .. })
|
||||
@@ -527,6 +552,9 @@ impl SwatchChoices {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Relink {
|
||||
renames: BTreeMap<Res, BTreeMap<String, String>>,
|
||||
/// An SVG filter effect's `(name, xml)` on the clipboard → its `(name, xml)` in the document
|
||||
/// ([`Clipboard::filter_links`]).
|
||||
filters: BTreeMap<(String, String), (String, String)>,
|
||||
/// Graphic style id on the clipboard → its id in the document. Links to other ids are dropped.
|
||||
styles: BTreeMap<u32, u32>,
|
||||
/// Swatch (its name in the document) → the colour linked colours show at 100 %.
|
||||
@@ -566,6 +594,15 @@ impl Relink {
|
||||
});
|
||||
}
|
||||
fn appearance(&self, a: &mut Appearance) {
|
||||
if !self.filters.is_empty() {
|
||||
let items = a.items.iter_mut().flat_map(|i| i.effects_mut().iter_mut());
|
||||
for e in a.effects.iter_mut().chain(items) {
|
||||
if let Some((name, xml)) = filter_of(e).and_then(|k| self.filters.get(&k)) {
|
||||
e.params["name"] = json!(name);
|
||||
e.params["xml"] = json!(xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
for it in &mut a.items {
|
||||
self.paint(it.paint_mut());
|
||||
if let AppearanceItem::Stroke(l) = it
|
||||
@@ -1013,6 +1050,7 @@ impl Clipboard {
|
||||
/// of definitions that use each other is matched on its own ([`loop_groups`]).
|
||||
fn import_used(&self, d: &mut Document, used: &Used, choices: &SwatchChoices, same_doc: bool, in_use_order: bool) -> Imported {
|
||||
let mut out = Imported::default();
|
||||
out.relink.filters = self.filter_links(d, same_doc);
|
||||
self.import_images(d, used, same_doc, &mut out);
|
||||
self.import_swatches(d, used, choices, same_doc, &mut out);
|
||||
// Brushes whose art shows no symbols or patterns, before the symbols and patterns whose art
|
||||
@@ -1170,10 +1208,52 @@ impl Clipboard {
|
||||
}
|
||||
vectorcraft_brush::store(d, &lib);
|
||||
}
|
||||
out.relinks = !(out.relink.renames.is_empty() && out.relink.styles.is_empty() && out.relink.colors.is_empty() && used.styles.is_empty());
|
||||
out.relinks = !(out.relink.renames.is_empty()
|
||||
&& out.relink.filters.is_empty()
|
||||
&& out.relink.styles.is_empty()
|
||||
&& out.relink.colors.is_empty()
|
||||
&& used.styles.is_empty());
|
||||
out
|
||||
}
|
||||
|
||||
/// How the pasted SVG filter effects relink ([`Relink::filters`]). Each names the document's
|
||||
/// filter of its name; one that draws another definition than that filter takes the document's
|
||||
/// filter when pasted back into the document it came from, and elsewhere keeps its own under a
|
||||
/// free name, so editing or deleting the document's filter leaves it alone.
|
||||
fn filter_links(&self, d: &Document, same_doc: bool) -> BTreeMap<(String, String), (String, String)> {
|
||||
let mut found = BTreeSet::new();
|
||||
for n in &self.nodes {
|
||||
node_filters(n, &mut found);
|
||||
}
|
||||
for s in &self.symbols {
|
||||
node_filters(&s.art, &mut found);
|
||||
}
|
||||
for a in self.patterns.iter().flat_map(|p| p.art.iter()) {
|
||||
node_filters(a, &mut found);
|
||||
}
|
||||
for a in self.brushes.iter().flat_map(Brush::art) {
|
||||
node_filters(a, &mut found);
|
||||
}
|
||||
for g in &self.graphic_styles {
|
||||
appearance_filters(&g.appearance, &mut found);
|
||||
}
|
||||
let mut taken: BTreeSet<String> = d.svg_filters.iter().map(|f| f.name.clone()).chain(found.iter().map(|(n, _)| n.clone())).collect();
|
||||
let mut links = BTreeMap::new();
|
||||
for (name, xml) in found {
|
||||
let Some(held) = d.svg_filters.iter().find(|f| f.name == name).filter(|f| f.xml != xml.trim()) else { continue };
|
||||
let to = if same_doc {
|
||||
(name.clone(), held.xml.clone())
|
||||
} else {
|
||||
let new = unique_name(&name, |n| taken.contains(n));
|
||||
taken.insert(new.clone());
|
||||
let drawn = vectorcraft_render::effects::svg_filter::with_id(&xml, &new).unwrap_or_else(|| xml.clone());
|
||||
(new, drawn)
|
||||
};
|
||||
links.insert((name, xml), to);
|
||||
}
|
||||
links
|
||||
}
|
||||
|
||||
/// Image blobs: a different blob under a key the document uses gets a new key.
|
||||
fn import_images(&self, d: &mut Document, used: &Used, same_doc: bool, out: &mut Imported) {
|
||||
for (key, blob) in self.images.iter().filter(|(k, _)| used.has(Res::Image, k)) {
|
||||
|
||||
@@ -207,6 +207,58 @@ fn deleting_a_filter_in_use_leaves_the_effect_drawing_nothing() {
|
||||
assert_eq!(drawn(&s, a), json!(SHIFT_MORE));
|
||||
}
|
||||
|
||||
/// The `(name, xml)` of every `svg.filter` effect on the document's objects.
|
||||
fn filter_effects(s: &Session) -> Vec<(String, String)> {
|
||||
let mut out = vec![];
|
||||
s.doc().unwrap().doc.walk(|n| {
|
||||
for e in n.appearance.effects.iter().filter(|e| e.id == vectorcraft_render::effects::SVG_FILTER) {
|
||||
out.push((e.params["name"].as_str().unwrap().to_string(), e.params["xml"].as_str().unwrap().to_string()));
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Pasted art whose filter has a name the document gives another filter keeps its own filter
|
||||
/// under a free name, so editing or deleting the document's filter leaves it alone. Pasted back
|
||||
/// into the document it came from, it takes the document's filter as it is now.
|
||||
#[test]
|
||||
fn pasted_art_keeps_a_filter_another_of_its_name_would_replace() {
|
||||
let mut s = session();
|
||||
run(&mut s, "svgFilter.define", json!({"xml": SHIFT}));
|
||||
let svg = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><defs>{SHIFT_MORE}</defs><rect width="30" height="30" filter="url(#Shift)"/></svg>"#
|
||||
);
|
||||
run(&mut s, "clipboard.importSvg", json!({"svg": svg}));
|
||||
run(&mut s, "edit.pasteInPlace", json!({}));
|
||||
let pasted = filter_effects(&s);
|
||||
assert_eq!(pasted.len(), 1);
|
||||
assert_eq!(pasted[0].0, "Shift 2", "renamed: {pasted:?}");
|
||||
assert!(pasted[0].1.contains("id=\"Shift 2\"") && pasted[0].1.contains("dx=\"9\""), "its own filter: {pasted:?}");
|
||||
assert_eq!(names(&mut s), ["Shift"]);
|
||||
run(&mut s, "svgFilter.delete", json!({"name": "Shift"}));
|
||||
assert_eq!(filter_effects(&s), pasted, "the document's filter leaves it alone");
|
||||
// The same filter as the document's keeps its name.
|
||||
let mut t = session();
|
||||
run(&mut t, "svgFilter.define", json!({"xml": SHIFT}));
|
||||
let svg = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><defs>{SHIFT}</defs><rect width="30" height="30" filter="url(#Shift)"/></svg>"#
|
||||
);
|
||||
run(&mut t, "clipboard.importSvg", json!({"svg": svg}));
|
||||
run(&mut t, "edit.pasteInPlace", json!({}));
|
||||
assert_eq!(filter_effects(&t), [("Shift".to_string(), SHIFT.to_string())]);
|
||||
// Copied, then the filter edited: pasted back, the copy draws the edited filter.
|
||||
let mut u = session();
|
||||
let a = rect(&mut u, 10.0);
|
||||
run(&mut u, "svgFilter.define", json!({"xml": SHIFT}));
|
||||
run(&mut u, "svgFilter.apply", json!({"name": "Shift", "ids": [a.0]}));
|
||||
run(&mut u, "select.set", json!({"ids": [a.0]}));
|
||||
run(&mut u, "edit.copy", json!({}));
|
||||
run(&mut u, "svgFilter.define", json!({"xml": SHIFT_MORE, "replace": "Shift"}));
|
||||
run(&mut u, "edit.pasteInPlace", json!({}));
|
||||
let both = filter_effects(&u);
|
||||
assert_eq!(both, [("Shift".to_string(), SHIFT_MORE.to_string()), ("Shift".to_string(), SHIFT_MORE.to_string())]);
|
||||
}
|
||||
|
||||
/// Art in a symbol's definition that uses a filter follows its edits too.
|
||||
#[test]
|
||||
fn a_symbols_art_follows_a_filter_edit() {
|
||||
|
||||
+3
-1
@@ -3739,7 +3739,9 @@ step. `svgFilter.apply {name, ids?|id?, item?}` applies one as the live effect `
|
||||
in one undo step. The effect (`{name, xml, scale, origin}`) names the document's filter: editing the filter changes
|
||||
what every object that uses it draws, in the edit's one undo step, and an application after an edit takes the edited
|
||||
filter. Deleting a filter that objects use leaves their effects in place, still named, drawing nothing (their `xml`
|
||||
empty) until a filter of that name is defined or imported again. Opened from such an effect, Apply SVG Filter shows the
|
||||
empty) until a filter of that name is defined or imported again. A pasted effect that names one of the document's
|
||||
filters but draws another definition keeps its own filter under a free name (`Soft 2`); pasted back into the document
|
||||
it was copied from, it takes the document's filter. Opened from such an effect, Apply SVG Filter shows the
|
||||
name above the list, not selectable. `effect.apply {effect: "svg.filter", params: {xml}}` takes a filter
|
||||
directly and names it by its `id`; an `xml` that isn't one such filter is an error. `ui.svgFilterDialog {}` opens Apply
|
||||
SVG Filter (New, Edit, Delete, Import…), and from the Appearance panel it edits which filter an applied effect runs;
|
||||
|
||||
Reference in New Issue
Block a user