From 8ebcc88c2e7f218cb56fbce022176f129e595a18 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 03:03:53 -0800 Subject: [PATCH] Effects: a pasted SVG filter at the size limit still draws under its new name Pasted art whose SVG filter is renamed to a free name ("Soft 2") had the new name written into the filter's id. For a filter at the 64 KB limit the longer id made it too long to draw, so the pasted art lost the effect. Paste now takes the first free name under which the filter still fits: a shorter start of its name ("Sof"), else a free letter or digit. The filter's id is always the effect's name. --- crates/engine/src/cmd/clipboard/resources.rs | 19 ++++++++++++--- crates/engine/src/tests/svgfilter.rs | 25 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) 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() {