diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index 5bea00948..22703b467 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -1219,7 +1219,9 @@ impl Clipboard { /// 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. + /// free name, so editing or deleting the document's filter leaves it alone. When that name would + /// make the filter too long to draw, the free name is a shorter start of its name ("Shif"), else + /// a free letter or digit; the filter's id is always its name. fn filter_links(&self, d: &Document, same_doc: bool) -> BTreeMap<(String, String), (String, String)> { let mut found = BTreeSet::new(); for n in &self.nodes { @@ -1244,9 +1246,20 @@ impl Clipboard { let to = if same_doc { (name.clone(), held.xml.clone()) } else { - let new = unique_name(&name, |n| taken.contains(n)); + // Free names made from the starts of the name, longest first, then free letters and + // digits: the first under which the filter still draws. + let mut ends: Vec = name.char_indices().map(|(i, _)| i).skip(1).collect(); + ends.push(name.len()); + let starts = ends.iter().rev().filter_map(|&end| name.get(..end).map(str::trim_end)).filter(|b| !b.is_empty()); + let shorter = starts.map(|b| unique_name(b, |n| taken.contains(n))); + let single = ('a'..='z').chain('0'..='9').map(String::from).filter(|n| !taken.contains(n)); + let renamed = shorter.chain(single).find_map(|new| { + let drawn = vectorcraft_render::effects::svg_filter::with_id(&xml, &new) + .filter(|x| vectorcraft_render::effects::svg_filter::check(x).is_ok())?; + Some((new, drawn)) + }); + let Some((new, drawn)) = renamed else { continue }; 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); diff --git a/crates/engine/src/tests/svgfilter.rs b/crates/engine/src/tests/svgfilter.rs index 4edcc6fd1..7606cebe4 100644 --- a/crates/engine/src/tests/svgfilter.rs +++ b/crates/engine/src/tests/svgfilter.rs @@ -259,6 +259,31 @@ fn pasted_art_keeps_a_filter_another_of_its_name_would_replace() { assert_eq!(both, [("Shift".to_string(), SHIFT_MORE.to_string()), ("Shift".to_string(), SHIFT_MORE.to_string())]); } +/// A pasted filter at the size limit, which "Shift 2" would make too long to draw, keeps its own +/// definition under a free name no longer than its own (a free letter for a one-letter name), and +/// its id is that name. +#[test] +fn pasted_art_with_a_filter_at_the_size_limit_still_draws_when_renamed() { + for (name, held, renamed) in [("Shift", SHIFT, "Shif"), ("S", r#""#, "a")] { + let mut s = session(); + run(&mut s, "svgFilter.define", json!({"xml": held})); + let (open, close) = (format!(r#""#); + let big = format!("{open}{}{close}", "a".repeat(vectorcraft_doc::SvgFilter::MAX_XML - open.len() - close.len())); + assert!(vectorcraft_render::effects::svg_filter::check(&big).is_ok()); + let svg = format!( + r#"{big}"# + ); + 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, renamed, "renamed: {}", pasted[0].0); + assert_eq!(vectorcraft_render::effects::svg_filter::check(&pasted[0].1), Ok(renamed.to_string()), "the pasted filter draws under its name"); + assert!(pasted[0].1.contains("dx=\"9\""), "its own filter"); + assert_eq!(names(&mut s), [name]); + } +} + /// Art in a symbol's definition that uses a filter follows its edits too. #[test] fn a_symbols_art_follows_a_filter_edit() {