diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index 72b4ef833..a88953def 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -858,7 +858,8 @@ fn names_of_loop<'d>(d: &'d Document, looped: &[Def<'_>], r: &Relink) -> Option< let names: Vec> = looped.iter().map(|def| def.names_in(d)).collect(); if names.is_empty() || names.iter().any(Vec::is_empty) { return None; - } // The definitions compared once the name at each position is picked. + } + // The definitions compared once the name at each position is picked. let mut compare_at: Vec> = vec![vec![]; looped.len()]; for (i, uses) in uses_of(looped).iter().enumerate() { compare_at[uses.iter().copied().fold(i, usize::max)].push(i); @@ -1025,21 +1026,30 @@ impl Clipboard { // when every new name and graphic style id is known; with `in_use_order`, so are the // styles added, which a loop of uses may place before a pattern they paint with. let (mut added_symbols, mut added_patterns, mut added_styles) = (HashSet::new(), HashSet::new(), vec![]); - // Each definition, with the loop it starts. - let steps = - groups.iter().flat_map(|(group, looped)| group.iter().enumerate().map(move |(i, def)| (*def, (*looped && i == 0).then_some(group)))); - for (def, starts) in steps { + // Each definition, whether it is in a loop, and the loop it starts. + let steps = groups + .iter() + .flat_map(|(group, looped)| group.iter().enumerate().map(move |(i, def)| (*def, *looped, (*looped && i == 0).then_some(group)))); + // In a loop the document does not have all at once, every definition uses one that is + // added under a free name, so each one is added too. + let mut fresh = false; + for (def, looped, starts) in steps { + fresh &= looped; // A loop of uses that the document has all at once is known under those names before // its definitions are compared. - if let Some(group) = starts - && let Some(names) = names_of_loop(d, group, &out.relink) - { - let names: Vec = names.into_iter().map(str::to_string).collect(); - for (def, name) in group.iter().zip(names) { - let (res, own) = def.key(); - if own != name { - out.relink.renames.entry(res).or_default().insert(own.to_string(), name); + if let Some(group) = starts { + match names_of_loop(d, group, &out.relink) { + Some(names) => { + let names: Vec = names.into_iter().map(str::to_string).collect(); + for (def, name) in group.iter().zip(names) { + let (res, own) = def.key(); + if own != name { + out.relink.renames.entry(res).or_default().insert(own.to_string(), name); + } + } + fresh = false; } + None => fresh = true, } } match def { @@ -1047,7 +1057,7 @@ impl Clipboard { let mut s = def.clone(); out.relink.attrs(&mut s.attrs); let theirs = if r == Res::CharStyle { &mut d.char_styles } else { &mut d.para_styles }; - let p = place(theirs, &s, same_doc, PartialEq::eq); + let p = place(theirs, &s, same_doc, |a, b| !fresh && a == b); if in_use_order && let Some(name) = p.new_name(&def.name) { added_styles.push((r, name, def)); } @@ -1065,7 +1075,8 @@ impl Clipboard { }; let r = &out.relink; let p = place(&mut d.symbols, def, same_doc, |a, b| { - options.is_none_or(|o| theirs.get(&a.name) == Some(o)) + !fresh + && options.is_none_or(|o| theirs.get(&a.name) == Some(o)) && same_art(r, std::slice::from_ref(&a.art), std::slice::from_ref(&b.art)) }); if let Some(name) = p.new_name(&def.name) { @@ -1079,7 +1090,7 @@ impl Clipboard { Def::Pattern(def) => { // Tile options alike, and the art once relinked. let bare = |p: &PatternDef| PatternDef { name: String::new(), art: vec![], ..p.clone() }; - let same = |n: &str| d.pattern(n).is_some_and(|x| bare(x) == bare(def) && same_art(&out.relink, &x.art, &def.art)); + let same = |n: &str| !fresh && d.pattern(n).is_some_and(|x| bare(x) == bare(def) && same_art(&out.relink, &x.art, &def.art)); let p = existing(&def.name, d.patterns.iter().map(|p| p.name.as_str()), same_doc, same).unwrap_or_else(|| { // Its swatch comes along under the same name. let name = unique_name(&def.name, |n| d.pattern(n).is_some() || d.swatch_name_taken(n)); diff --git a/crates/engine/src/cmd/textstyles/tests.rs b/crates/engine/src/cmd/textstyles/tests.rs index 91c0a3621..579de23d4 100644 --- a/crates/engine/src/cmd/textstyles/tests.rs +++ b/crates/engine/src/cmd/textstyles/tests.rs @@ -943,6 +943,42 @@ fn a_style_and_the_pattern_that_uses_it_load_together() { assert_eq!((s.doc().unwrap().doc.clone(), s.doc().unwrap().history.undo.len()), (doc, undo)); } +/// A style and the pattern it paints with, whose type is set in that style, are both added when +/// the document has a style of that name and attributes but another pattern of that name: the +/// copy of the style paints with the copy of the pattern, and a second load adds nothing. +#[test] +fn a_style_in_a_loop_is_not_matched_to_a_style_that_paints_with_another_pattern() { + let (mut src, id) = session_with_text("Tile"); + src.execute("charStyle.new", &json!({"name": "Accent", "attrs": {"kerning": 40}})).unwrap(); + src.execute("charStyle.apply", &json!({"name": "Accent", "id": id})).unwrap(); + src.execute("object.pattern.make", &json!({"name": "Dots", "edit": false, "ids": [id]})).unwrap(); + let other = src.execute("text.create", &json!({"x": 10, "y": 90, "text": "B"})).unwrap()["id"].as_u64().unwrap(); + src.execute("charStyle.apply", &json!({"name": "Accent", "id": other})).unwrap(); + src.execute("paint.setFill", &json!({"swatch": "Dots", "ids": [other]})).unwrap(); + src.execute("charStyle.new", &json!({"name": "Painted", "id": other})).unwrap(); + let mut source = (*src.doc().unwrap().doc).clone(); + let painted = source.char_styles.iter().find(|s| s.name == "Painted").unwrap().attrs.clone(); + source.char_styles.retain(|s| s.name != "Painted"); + source.char_styles.iter_mut().find(|s| s.name == "Accent").unwrap().attrs = painted.clone(); + let data = vectorcraft_format::base64_encode(&vectorcraft_format::save(&source, false)); + let (mut s, _) = session_with_text("X"); + let square = s.execute("shape.rectangle", &json!({"x": 200, "y": 200, "width": 30, "height": 30})).unwrap()["id"].clone(); + s.execute("object.pattern.make", &json!({"name": "Dots", "edit": false, "ids": [square]})).unwrap(); + let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); + doc.char_styles.push(TextStyleDef { name: "Accent".into(), attrs: painted }); + let load = json!({"dataBase64": data, "name": "Other.vectorcraft"}); + let r = s.execute("charStyle.load", &load).unwrap(); + assert_eq!(r["added"], json!(["Accent 2"]), "{r}"); + let d = s.doc().unwrap().doc.clone(); + let fill = |name: &str| super::style_attrs(&d, false, name).unwrap()["fill"].to_string(); + assert!(fill("Accent").contains("\"Dots\"") && fill("Accent 2").contains("\"Dots 2\""), "{} {}", fill("Accent"), fill("Accent 2")); + let NodeKind::Text(text) = &d.pattern("Dots 2").unwrap().art[0].kind else { panic!("pattern art should be type") }; + assert_eq!(text.runs[0].style.style_name.as_deref(), Some("Accent 2")); + let r = s.execute("charStyle.load", &load).unwrap(); + assert_eq!(r["added"], json!([]), "{r}"); + assert_eq!(s.doc().unwrap().doc, d); +} + /// A style name from a saved document stays the name used by type in imported pattern art. #[test] fn loaded_style_keeps_the_name_used_by_pattern_type() {