Merge pull request #6994 from dotCooCoo/fix/paste-style-pattern-order

Paste: text styles that paint with a pattern are pasted after the patterns they use
This commit is contained in:
BFlat
2026-11-13 07:49:08 +07:00
committed by GitHub
3 changed files with 245 additions and 70 deletions
+28 -70
View File
@@ -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<Imported> {
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<Imported> {
/// [`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<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
// 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<String> =
if in_use_order { HashSet::new() } else { self.import_brushes(d, used, same_doc, false, &mut out).into_iter().collect() };
let mut defs: Vec<Def<'_>> = 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<Def<'_>>, 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<Def<'_>>, 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<Vec<Brush>> = 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<String> {
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::<BTreeSet<_>>());
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<LoadedStyle> = vec![];
for (para, r, list) in [(false, Res::CharStyle, &clip.char_styles), (true, Res::ParaStyle, &clip.para_styles)] {
for s in list {
+216
View File
@@ -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<String> {
let attrs = &doc(s).char_styles.iter().find(|x| x.name == name)?.attrs;
pattern_of(&serde_json::from_value::<Paint>(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<String>, Option<String>) {
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<String> {
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();
+1
View File
@@ -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).