Paste: text styles that paint with a pattern are pasted after the patterns they use

Paste placed character and paragraph styles before the patterns, symbols
and brushes they use, and compared each one with the document's before a
pattern it painted with had its final name. A style that painted with a
pattern added as "Dots 2" kept painting with the document's own "Dots",
or was matched to a document style of its name that paints with that
"Dots". A symbol whose art paints with such a pattern, and a brush whose
art sets type in a style renamed on the way in, were matched the same way.

Paste, and the symbol, brush, pattern and Libraries imports that add art
through it, now place each style, symbol, pattern and brush after the
ones it uses, as Load Character Styles and Load Paragraph Styles already
did, and relink the styles they add at the end. Pasted back into its own
document, each definition stays the document's of its name.
This commit is contained in:
dotCooCoo
2026-11-12 15:59:36 -08:00
parent b4505a76de
commit addbcd918e
3 changed files with 245 additions and 70 deletions
+28 -70
View File
@@ -697,15 +697,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)]
@@ -739,7 +730,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> {
@@ -1055,49 +1046,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()
@@ -1107,8 +1089,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;
@@ -1116,8 +1098,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) => {
@@ -1146,7 +1129,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);
@@ -1200,9 +1183,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 };
@@ -1323,28 +1303,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) {
@@ -1429,7 +1387,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
@@ -1430,6 +1430,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).