diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index 243c69f671..291c221756 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -712,15 +712,6 @@ fn same_brush(r: &Relink, ours: &Brush, theirs: &Brush) -> bool { relinked(&Relink::default(), ours) == relinked(r, theirs) } -/// Does brush `b`'s art show symbols or patterns? -fn shows_definitions(b: &Brush) -> bool { - let mut found = false; - for a in b.art() { - node_refs(a, &mut |r| found |= matches!(r, Ref::Name(Res::Symbol | Res::Pattern, _))); - } - found -} - /// A symbol's Symbol Options, which its document keeps by symbol name: copied with the symbol and /// given to it under the name pasting adds it as. #[derive(Clone, Debug, PartialEq)] @@ -754,7 +745,7 @@ impl SymbolOptions { } /// A character or paragraph style, symbol (with the Symbol Options copied with it), pattern or -/// brush (placed with the others only when they are placed in use order) to place +/// brush to place /// ([`Clipboard::import_used`]). #[derive(Clone, Copy)] enum Def<'c> { @@ -1070,49 +1061,40 @@ impl Clipboard { } /// Add the resources the objects use to `d` (inside the paste's undo step) and work out how - /// pasted copies relink to them. An error, with `d` unchanged, when a pasted SVG filter finds no - /// free name ([`Self::filter_links`]). + /// pasted copies relink to them. Each style, symbol, pattern and brush is placed after those it + /// uses, as [`import_text_styles`] places them, so that a style that paints with a pattern added + /// under a free name ("Dots 2") is compared and added with that name. An error, with `d` + /// unchanged, when a pasted SVG filter finds no free name ([`Self::filter_links`]). pub(crate) fn import_into(&self, d: &mut Document, choices: &SwatchChoices, same_doc: bool) -> Result { - self.import_used(d, &self.used(), choices, same_doc, false) + self.import_used(d, &self.used(), choices, same_doc) } - /// [`Self::import_into`] for the resources `used` names. The character and paragraph styles - /// come before the symbols and patterns, whose art may set type in them; with `in_use_order`, - /// each style, symbol, pattern and brush comes after the styles, symbols, patterns and brushes it uses - /// ([`use_order`]), so that it is compared and relinked with their names here, and each loop - /// 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) -> Result { + /// [`Self::import_into`] for the resources `used` names. Each style, symbol, pattern and brush + /// comes after the styles, symbols, patterns and brushes it uses ([`use_order`]), so that it is + /// compared and relinked with their names here, and each loop of definitions that use each + /// other is matched on its own ([`loop_groups`]). Pasted back into its own document + /// (`same_doc`), each definition of a loop is the document's of its name. + fn import_used(&self, d: &mut Document, used: &Used, choices: &SwatchChoices, same_doc: bool) -> Result { 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 - // they may brush; the others once those are in. With `in_use_order`, the brushes are - // placed in use order with the others. - let mut added_brushes: HashSet = - if in_use_order { HashSet::new() } else { self.import_brushes(d, used, same_doc, false, &mut out).into_iter().collect() }; let mut defs: Vec> = vec![]; for (r, list) in [(Res::CharStyle, &self.char_styles), (Res::ParaStyle, &self.para_styles)] { defs.extend(list.iter().filter(|s| used.has(r, &s.name)).map(|s| Def::Style(r, s))); } defs.extend(self.symbols.iter().filter(|s| used.has(Res::Symbol, &s.name)).map(|s| Def::Symbol(s, self.symbol_options.get(&s.name)))); defs.extend(self.patterns.iter().filter(|p| used.has(Res::Pattern, &p.name)).map(Def::Pattern)); - if in_use_order { - defs.extend(self.brushes.iter().filter(|b| used.has(Res::Brush, &b.name)).map(Def::Brush)); - } + defs.extend(self.brushes.iter().filter(|b| used.has(Res::Brush, &b.name)).map(Def::Brush)); // Groups to place in order; true for a loop of definitions that use each other. - let groups: Vec<(Vec>, bool)> = if in_use_order { - let (order, rest) = use_order(defs); - let loops = loop_groups(&rest).into_iter().map(|(at, looped)| (at.into_iter().filter_map(|i| rest.get(i).copied()).collect(), looped)); - std::iter::once((order, false)).chain(loops).collect() - } else { - vec![(defs, false)] - }; - // Styles (their colors relinked first), symbols and patterns (compared once relinked, - // symbols with their options too). The symbols and patterns added are relinked at the end, - // 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![]); + let (order, rest) = use_order(defs); + let loops = loop_groups(&rest).into_iter().map(|(at, looped)| (at.into_iter().filter_map(|i| rest.get(i).copied()).collect(), looped)); + let groups: Vec<(Vec>, bool)> = std::iter::once((order, false)).chain(loops).collect(); + // Styles (their colors relinked first), symbols, patterns and brushes (compared once + // relinked, symbols with their options too). The styles, symbols, patterns and brushes + // added are relinked at the end, when every new name and graphic style id is known: a loop + // of uses may place one before a definition it uses. + let (mut added_symbols, mut added_patterns, mut added_brushes, mut added_styles) = (HashSet::new(), HashSet::new(), HashSet::new(), vec![]); // Each definition, whether it is in a loop, and the loop it starts. let steps = groups .iter() @@ -1122,8 +1104,8 @@ impl Clipboard { let mut fresh = false; // The names in the document of the loop being placed, when it has them all at once. let mut matched: BTreeMap<(Res, String), String> = BTreeMap::new(); - // The document's brush library, read the first time a brush is placed in use order and - // stored again after each brush added; nothing else in the loop changes it. + // The document's brush library, read the first time a brush is placed and stored again + // after each brush added; nothing else in the loop changes it. let mut library: Option> = None; for (def, looped, starts) in steps { fresh &= looped; @@ -1131,8 +1113,9 @@ impl Clipboard { matched.clear(); } // 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 { + // its definitions are compared. In its own document, each definition is the + // document's of its name, as outside a loop. + if let Some(group) = starts.filter(|_| !same_doc) { matched.clear(); match names_of_loop(d, group, &out.relink) { Some(names) => { @@ -1161,7 +1144,7 @@ impl Clipboard { 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, |a, b| !fresh && a == b); - if in_use_order && let Some(name) = p.new_name(&def.name) { + if let Some(name) = p.new_name(&def.name) { added_styles.push((r, name, def)); } out.placed(r, &def.name, &p); @@ -1215,9 +1198,6 @@ impl Clipboard { } } } - if !in_use_order { - added_brushes.extend(self.import_brushes(d, used, same_doc, true, &mut out)); - } self.import_graphic_styles(d, used, same_doc, &mut out); for (r, name, def) in added_styles { let theirs = if r == Res::CharStyle { &mut d.char_styles } else { &mut d.para_styles }; @@ -1338,28 +1318,6 @@ impl Clipboard { } } - /// The brushes whose art shows symbols or patterns (`late`), or the others: each one the - /// document has (its art compared once relinked) is used, the others are added. → the names of - /// those added, whose art is relinked at the end. - fn import_brushes(&self, d: &mut Document, used: &Used, same_doc: bool, late: bool, out: &mut Imported) -> Vec { - let brushes: Vec<&Brush> = self.brushes.iter().filter(|b| used.has(Res::Brush, &b.name) && shows_definitions(b) == late).collect(); - if brushes.is_empty() { - return vec![]; - } - let mut lib = vectorcraft_brush::library(d); - let mut added = vec![]; - for b in brushes { - let r = &out.relink; - let p = place(&mut lib, b, same_doc, |ours, theirs| same_brush(r, ours, theirs)); - added.extend(p.new_name(&b.name)); - out.placed(Res::Brush, &b.name, &p); - } - if !added.is_empty() { - vectorcraft_brush::store(d, &lib); - } - added - } - /// Global and spot colours (a name conflict merges or adds as `choices` say), then the tint /// swatches of the tints used and the gradient swatches. fn import_swatches(&self, d: &mut Document, used: &Used, choices: &SwatchChoices, same_doc: bool, out: &mut Imported) { @@ -1444,7 +1402,7 @@ pub(crate) fn import_text_styles(src: &Document, d: &mut Document, chars: bool, let used = Used::of_text_styles(&Defs::of(src, &library), chars, paras); let clip = Clipboard::with_used(src, &used, &library, vec![]); let before = [&d.char_styles, &d.para_styles].map(|l| l.iter().map(|s| s.name.clone()).collect::>()); - let out = clip.import_used(d, &used, &SwatchChoices::default(), false, true)?; + let out = clip.import_used(d, &used, &SwatchChoices::default(), false)?; let mut placed: Vec = vec![]; for (para, r, list) in [(false, Res::CharStyle, &clip.char_styles), (true, Res::ParaStyle, &clip.para_styles)] { for s in list { diff --git a/crates/engine/src/tests/clipboard.rs b/crates/engine/src/tests/clipboard.rs index b5ebf080ea..16ce8d8c77 100644 --- a/crates/engine/src/tests/clipboard.rs +++ b/crates/engine/src/tests/clipboard.rs @@ -492,6 +492,222 @@ fn pasting_again_reuses_what_an_earlier_paste_added() { assert!(doc(&s).swatch("Brand 3").is_none() && doc(&s).char_styles.len() == 1); } +/// The active document, to edit its definitions directly. +fn doc_mut(s: &mut Session) -> &mut Document { + std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc) +} + +/// The pattern a paint paints with. +fn pattern_of(p: &Paint) -> Option<&str> { + match p { + Paint::Pattern { pattern, .. } => Some(pattern), + _ => None, + } +} + +/// The pattern the fill of the active document's character style `name` paints with. +fn style_pattern(s: &Session, name: &str) -> Option { + let attrs = &doc(s).char_styles.iter().find(|x| x.name == name)?.attrs; + pattern_of(&serde_json::from_value::(attrs.get("fill")?.clone()).ok()?).map(str::to_string) +} + +/// A pattern "Dots" made from a `size` square of `color` in the active document. +fn dots(s: &mut Session, size: f64, color: &str) { + let square = rect(s, 300.0, 200.0, size, size); + s.execute("paint.setFill", &json!({"ids": [square.0], "color": color})).unwrap(); + s.execute("object.pattern.make", &json!({"name": "Dots", "edit": false, "ids": [square.0]})).unwrap(); +} + +/// Type "Hi" filled with the pattern "Dots" (a small red square), set in a character style +/// "Patterned" whose fill is that pattern. +fn patterned_type(s: &mut Session) -> NodeId { + dots(s, 10.0, "#ff0000"); + let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hi"})).unwrap()); + s.execute("paint.setFill", &json!({"ids": [t.0], "swatch": "Dots"})).unwrap(); + s.execute("charStyle.new", &json!({"name": "Patterned", "id": t.0})).unwrap(); + s.execute("charStyle.apply", &json!({"name": "Patterned", "id": t.0})).unwrap(); + t +} + +/// The character style name of a type object's first run, and the pattern that run paints with. +fn type_links(s: &Session, id: NodeId) -> (Option, Option) { + let NodeKind::Text(tx) = node(s, id).kind else { panic!("not type") }; + (tx.runs[0].style.style_name.clone(), pattern_of(&tx.runs[0].style.fill).map(str::to_string)) +} + +/// Type set in a style that paints with a pattern, pasted into a document with another pattern of +/// that name: the pattern comes in as "Dots 2", and the style paints with "Dots 2" too, so applying +/// it later paints with the pasted pattern. Pasting again adds nothing. +#[test] +fn pasted_styles_paint_with_the_renamed_pattern_they_use() { + let mut s = session(); + let t = patterned_type(&mut s); + copy(&mut s, &[t]); + new_doc(&mut s); + dots(&mut s, 30.0, "#0000ff"); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert!(doc(&s).pattern("Dots 2").is_some(), "{v}"); + assert_eq!(style_pattern(&s, "Patterned").as_deref(), Some("Dots 2"), "{v}"); + let named = |n: &str| Some(n.to_string()); + assert_eq!(type_links(&s, ids_of(&v)[0]), (named("Patterned"), named("Dots 2"))); + let other = id_of(&s.execute("text.create", &json!({"x": 10, "y": 150, "text": "Yo"})).unwrap()); + s.execute("charStyle.apply", &json!({"name": "Patterned", "id": other.0})).unwrap(); + assert_eq!(type_links(&s, other).1.as_deref(), Some("Dots 2"), "the applied style paints with the pasted pattern"); + let counts = |s: &Session| (doc(s).patterns.len(), doc(s).char_styles.len()); + let before = counts(&s); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!(v["added"], 0, "{v}"); + assert_eq!(counts(&s), before); + assert_eq!(type_links(&s, ids_of(&v)[0]), (named("Patterned"), named("Dots 2"))); +} + +/// A document whose "Patterned" style has the pasted style's attributes but whose "Dots" is +/// another pattern: that style paints with the document's "Dots", so the pasted one comes in as +/// "Patterned 2", painting with "Dots 2", and the document's own is left as it was. Pasting again +/// uses the numbered copies. +#[test] +fn pasted_styles_are_not_matched_to_styles_that_paint_with_another_pattern() { + let mut s = session(); + let t = patterned_type(&mut s); + let style = doc(&s).char_styles.iter().find(|x| x.name == "Patterned").unwrap().clone(); + copy(&mut s, &[t]); + new_doc(&mut s); + dots(&mut s, 30.0, "#0000ff"); + doc_mut(&mut s).char_styles.push(style); + let own = (doc(&s).char_styles.clone(), doc(&s).pattern("Dots").cloned()); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!(style_pattern(&s, "Patterned 2").as_deref(), Some("Dots 2"), "{v}"); + let d = doc(&s); + assert_eq!((&d.char_styles[..own.0.len()], d.pattern("Dots")), (&own.0[..], own.1.as_ref()), "the document's own are unchanged"); + let named = |n: &str| Some(n.to_string()); + let links = (named("Patterned 2"), named("Dots 2")); + assert_eq!(type_links(&s, ids_of(&v)[0]), links); + let counts = |s: &Session| (doc(s).patterns.len(), doc(s).char_styles.len()); + let before = counts(&s); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!(v["added"], 0, "{v}"); + assert_eq!(counts(&s), before); + assert_eq!(type_links(&s, ids_of(&v)[0]), links); +} + +/// A style that paints with a pattern whose tile sets type in that same style is pasted with the +/// pattern: when the pattern comes in as "Dots 2", the style paints with "Dots 2", and the tile's +/// type is set in the style. Pasting again adds nothing. The commands do not make that loop, so the +/// source is a document edited directly. +#[test] +fn a_pasted_style_and_the_pattern_whose_tile_uses_it_come_in_together() { + let mut s = session(); + let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Tile"})).unwrap()); + s.execute("charStyle.new", &json!({"name": "Accent", "attrs": {"kerning": 40}})).unwrap(); + s.execute("charStyle.apply", &json!({"name": "Accent", "id": t.0})).unwrap(); + s.execute("object.pattern.make", &json!({"name": "Dots", "edit": false, "ids": [t.0]})).unwrap(); + let other = id_of(&s.execute("text.create", &json!({"x": 10, "y": 90, "text": "B"})).unwrap()); + s.execute("charStyle.apply", &json!({"name": "Accent", "id": other.0})).unwrap(); + s.execute("paint.setFill", &json!({"swatch": "Dots", "ids": [other.0]})).unwrap(); + s.execute("charStyle.new", &json!({"name": "Painted", "id": other.0})).unwrap(); + let d = doc_mut(&mut s); + let painted = d.char_styles.iter().find(|x| x.name == "Painted").unwrap().attrs.clone(); + d.char_styles.retain(|x| x.name != "Painted"); + d.char_styles.iter_mut().find(|x| x.name == "Accent").unwrap().attrs = painted; + assert_eq!(style_pattern(&s, "Accent").as_deref(), Some("Dots")); + copy(&mut s, &[other]); + new_doc(&mut s); + dots(&mut s, 30.0, "#0000ff"); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert!(doc(&s).pattern("Dots 2").is_some(), "{v}"); + assert_eq!(style_pattern(&s, "Accent").as_deref(), Some("Dots 2"), "{v}"); + let NodeKind::Text(tile) = &doc(&s).pattern("Dots 2").unwrap().art[0].kind else { panic!("the tile is type") }; + assert_eq!(tile.runs[0].style.style_name.as_deref(), Some("Accent")); + let after = doc(&s).clone(); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!(v["added"], 0, "{v}"); + assert!(doc(&s).patterns == after.patterns && doc(&s).char_styles == after.char_styles, "nothing is added"); + // Back in the source document, the loop is the document's own. + s.execute("document.activate", &json!({"index": 0})).unwrap(); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!((v["added"].as_u64(), v["renamed"].as_array().map(Vec::len)), (Some(0), Some(0)), "{v}"); +} + +/// A symbol whose art paints with a pattern is not matched to the document's symbol of that name +/// and art when the document's pattern of that name is another one: it comes in as "Mark 2", +/// painting with "Dots 2". +#[test] +fn pasted_symbols_are_not_matched_to_symbols_that_paint_with_another_pattern() { + let mut s = session(); + dots(&mut s, 10.0, "#ff0000"); + let r = rect(&mut s, 20.0, 20.0, 50.0, 50.0); + s.execute("paint.setFill", &json!({"ids": [r.0], "swatch": "Dots"})).unwrap(); + sel(&mut s, &[r]); + let mark = id_of(&s.execute("symbol.new", &json!({"name": "Mark"})).unwrap()); + copy(&mut s, &[mark]); + // The document gets "Mark" and "Dots" from a first paste; then its "Dots" gets another tile. + new_doc(&mut s); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!(v["renamed"], json!([]), "{v}"); + let tile = &mut doc_mut(&mut s).patterns.iter_mut().find(|p| p.name == "Dots").unwrap().tile; + *tile = Rect::new(tile.x0, tile.y0, tile.x1 + 30.0, tile.y1 + 30.0); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert!(renamed(&v).contains(&("symbol".to_string(), "Mark".to_string(), "Mark 2".to_string())), "{v}"); + let art = |s: &Session, name: &str| fill_of(&doc(s).symbols.iter().find(|x| x.name == name).unwrap().art); + assert_eq!((art(&s, "Mark").as_deref(), art(&s, "Mark 2").as_deref()), (Some("Dots"), Some("Dots 2"))); + assert!(matches!(node(&s, ids_of(&v)[0]).kind, NodeKind::SymbolInstance { ref symbol, .. } if symbol == "Mark 2")); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!(v["added"], 0, "{v}"); +} + +/// The pattern the first filled object in `n` paints with. +fn fill_of(n: &Node) -> Option { + let mut found = None; + n.walk(&mut |m| { + if found.is_none() { + found = pattern_of(&m.appearance.fill_paint()).map(str::to_string); + } + }); + found +} + +/// A brush whose art sets type in a character style is not matched to the document's brush of +/// that name and art when the document's style of that name is another one: the style comes in as +/// "Accent 2", and the brush as a copy whose type is set in "Accent 2". +#[test] +fn pasted_brushes_are_not_matched_to_brushes_that_use_another_style() { + let mut s = session(); + let t = id_of(&s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ink"})).unwrap()); + s.execute("charStyle.new", &json!({"name": "Accent", "attrs": {"kerning": 40}})).unwrap(); + s.execute("charStyle.apply", &json!({"name": "Accent", "id": t.0})).unwrap(); + let name = s.execute("brush.new", &json!({"type": "art", "name": "Lettering", "ids": [t.0]})).unwrap()["name"].as_str().unwrap().to_string(); + let path = rect(&mut s, 200.0, 200.0, 40.0, 10.0); + s.execute("brush.apply", &json!({"name": name, "ids": [path.0]})).unwrap(); + let brush = vectorcraft_brush::find(doc(&s), &name).unwrap(); + copy(&mut s, &[path]); + new_doc(&mut s); + s.execute("charStyle.new", &json!({"name": "Accent", "attrs": {"kerning": 10}})).unwrap(); + let d = doc_mut(&mut s); + let mut lib = vectorcraft_brush::library(d); + lib.push(brush); + vectorcraft_brush::store(d, &lib); + let v = s.execute("edit.paste", &json!({})).unwrap(); + let names = renamed(&v); + let copied = names.iter().find(|(k, from, _)| k == "brush" && *from == name).map(|(_, _, to)| to.clone()); + assert!(names.contains(&("charStyle".to_string(), "Accent".to_string(), "Accent 2".to_string())), "{v}"); + let copied = copied.unwrap_or_else(|| panic!("the brush is added under a free name: {v}")); + let style_of = |s: &Session, b: &str| { + let b = vectorcraft_brush::find(doc(s), b).unwrap(); + let mut found = None; + for a in b.art() { + a.walk(&mut |m| { + if let NodeKind::Text(tx) = &m.kind { + found = found.take().or_else(|| tx.runs[0].style.style_name.clone()); + } + }); + } + found + }; + assert_eq!((style_of(&s, &name).as_deref(), style_of(&s, &copied).as_deref()), (Some("Accent"), Some("Accent 2"))); + let v = s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!(v["added"], 0, "{v}"); +} + #[test] fn brushes_travel_with_their_strokes() { let mut s = session(); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index abaaa5a2d1..11dc8cb895 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1435,6 +1435,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Applying a style to several objects, and Redefine Graphic Style, give the objects one shared copy of the style's appearance instead of a copy each; an object that is edited later gets its own copy, and an object whose placed gradients are fitted to its own bounds gets its own. The undo history's memory estimate counts a shared appearance once. Saved files are unchanged (`crates/engine/src/tests/styles.rs`). - **Several text styles at once:** in the Character Styles and Paragraph Styles panels, Shift-click highlights a range of styles and Ctrl/Cmd-click adds or removes one, without applying them; the panel menu's Select All Unused (`charStyle.unused`, `paraStyle.unused`) highlights the styles no type uses (in the layers, opacity masks, symbols, graph designs, pattern tiles and the art of brushes), never the Normal style; Delete, the button or the menu item, deletes every highlighted style but Normal in one undo step (`charStyle.delete {names}`, `paraStyle.delete {names}`). The menu's Duplicate copies every highlighted style in one undo step and highlights the copies. A highlighted row dragged onto Create New Style or Delete carries every highlighted style: dropped on Create New Style it copies them all and highlights the copies, and dropped on Delete it deletes them all but Normal and clears the highlight; a row from outside the highlight carries only its own style. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html). - **Load text styles from another document:** the Character Styles and Paragraph Styles panel menus have Load Character Styles… or Load Paragraph Styles… and Load All Styles…, which copy the styles of a document picked in an open dialog, with the swatches and patterns they paint with, in one undo step (`charStyle.load`, `paraStyle.load`, `textStyle.loadAll`); a style the document already has, under its name or a numbered one, is not added again, any other whose name is taken gets a number, and the file's Normal styles are not loaded. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html). + - **Pasted text styles and their patterns:** Paste, and the symbol, brush, pattern and Libraries imports that add art the same way, place each text style, symbol, pattern and brush after the ones it uses. A character style that paints with a pattern added under a free name ("Dots 2") paints with "Dots 2", and it is not matched to a document style of its name that paints with the document's own "Dots"; it gets a number instead ("Patterned 2"). A symbol whose art paints with such a pattern, and a brush whose art sets type in a renamed style, are added as numbered copies in the same way. Pasting again uses the numbered copies. The help page says pasted art brings the swatches it uses ([help guide, p. 134](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf)); the order and the matching are VectorCraft's own rules. Tests: `pasted_styles_paint_with_the_renamed_pattern_they_use`, `pasted_styles_are_not_matched_to_styles_that_paint_with_another_pattern`, `a_pasted_style_and_the_pattern_whose_tile_uses_it_come_in_together`, `pasted_symbols_are_not_matched_to_symbols_that_paint_with_another_pattern` and `pasted_brushes_are_not_matched_to_brushes_that_use_another_style` in `crates/engine/src/tests/clipboard.rs`. - **Neutral wording:** labels, MCP tool text, docs and packaging use neutral names, and `cargo xtask brands` (part of `cargo xtask ci`) fails on vendor names. - **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize. The Libraries panel holds gradients and patterns: its + button adds the selection's fill or stroke gradient or pattern, a gradient or pattern swatch dragged from the Swatches panel is added, and a click paints the selection through the active Fill/Stroke proxy, a pattern bringing its definition and swatch into the document. A spot colour stays a spot colour: the library keeps its ink name and tint, and using it in any document links the paint to that document's spot swatch of that name, adding it when missing (#5812).