Text styles: loading imports once and reads the brush library once

charStyle.load, paraStyle.load and textStyle.loadAll imported the styles
into a copy of the document to see whether anything changed, then ran the
same import again inside the edit. The edit now takes the copy the first
import made. While a typing session is open the import still runs inside
the edit, because the edit ends the session first and that can change the
document.

Matching a loop of definitions parsed the document's brush library again
for every comparison, and placing brushes in use order parsed it again for
every brush. Each now reads it once.

The params text and docs/mcp.md now say that the symbols and brushes the
patterns use come along, that `skipped` lists names in the file, and that
charStyle.load and paraStyle.load list only the styles of their own kind.
This commit is contained in:
dotCooCoo
2026-11-08 12:59:34 +07:00
committed by bflatastic
parent ea3fe16896
commit cbe374df62
3 changed files with 38 additions and 22 deletions
+20 -12
View File
@@ -720,8 +720,9 @@ impl<'c> Def<'c> {
}
}
/// Is it the definition `d` has under `name`, once relinked by `r`?
fn same_in(&self, d: &Document, name: &str, r: &Relink) -> bool {
/// Is it the definition `d` has under `name`, once relinked by `r`? (`brushes`: the brush
/// library of `d`.)
fn same_in(&self, d: &Document, brushes: &Brushes<'_>, name: &str, r: &Relink) -> bool {
match self {
Def::Style(res, s) => {
let theirs = if *res == Res::CharStyle { &d.char_styles } else { &d.para_styles };
@@ -738,18 +739,19 @@ impl<'c> Def<'c> {
let bare = |p: &PatternDef| PatternDef { name: String::new(), art: vec![], ..p.clone() };
d.pattern(name).is_some_and(|x| bare(x) == bare(p) && same_art(r, &x.art, &p.art))
}
Def::Brush(b) => vectorcraft_brush::library(d).iter().any(|x| x.name == name && same_brush(r, x, b)),
Def::Brush(b) => brushes.get().iter().any(|x| x.name == name && same_brush(r, x, b)),
}
}
/// The names of its kind in `d` that are its name or a numbered copy of it.
fn names_in(&self, d: &Document) -> Vec<String> {
/// The names of its kind in `d` (`brushes`: its brush library) that are its name or a numbered
/// copy of it.
fn names_in(&self, d: &Document, brushes: &Brushes<'_>) -> Vec<String> {
let (res, name) = self.key();
let names: Vec<String> = match res {
Res::CharStyle => d.char_styles.iter().map(|s| s.name.clone()).collect(),
Res::ParaStyle => d.para_styles.iter().map(|s| s.name.clone()).collect(),
Res::Symbol => d.symbols.iter().map(|s| s.name.clone()).collect(),
Res::Brush => vectorcraft_brush::library(d).into_iter().map(|b| b.name).collect(),
Res::Brush => brushes.get().iter().map(|b| b.name.clone()).collect(),
_ => d.patterns.iter().map(|p| p.name.clone()).collect(),
};
names.into_iter().filter(|n| copy_of(name, n)).collect()
@@ -859,9 +861,12 @@ fn components(uses: &[Vec<usize>]) -> Vec<(Vec<usize>, bool)> {
/// numbered copy of it. The names are picked one definition at a time, in order, and each
/// definition is compared as soon as the names of those it uses are picked, so a pick that does
/// not fit is dropped with every combination that follows from it; at most 65,536 comparisons are
/// made. `None` when no combination fits, or none was found within that many comparisons.
/// made. `None` when no combination fits, or none was found within that many comparisons. The brush
/// library of `d` is read once, the first time a brush is compared.
fn names_of_loop(d: &Document, looped: &[Def<'_>], r: &Relink) -> Option<Vec<String>> {
let names: Vec<Vec<String>> = looped.iter().map(|def| def.names_in(d)).collect();
let library = OnceCell::new();
let brushes = Brushes::Lazy(d, &library);
let names: Vec<Vec<String>> = looped.iter().map(|def| def.names_in(d, &brushes)).collect();
if names.is_empty() || names.iter().any(Vec::is_empty) {
return None;
}
@@ -876,7 +881,7 @@ fn names_of_loop(d: &Document, looped: &[Def<'_>], r: &Relink) -> Option<Vec<Str
guess.renames.entry(res).or_default().insert(own.to_string(), names[k][pick[k]].clone());
let fits = compare_at[k].iter().all(|&i| {
budget = budget.saturating_sub(1);
looped[i].same_in(d, &names[i][pick[i]], &guess)
looped[i].same_in(d, &brushes, &names[i][pick[i]], &guess)
});
if fits && k + 1 == looped.len() {
return Some(names.iter().zip(&pick).map(|(l, &p)| l[p].clone()).collect());
@@ -1046,6 +1051,9 @@ 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.
let mut library: Option<Vec<Brush>> = None;
for (def, looped, starts) in steps {
fresh &= looped;
if !looped {
@@ -1125,11 +1133,11 @@ impl Clipboard {
out.placed(Res::Pattern, &def.name, &p);
}
Def::Brush(b) => {
let mut lib = vectorcraft_brush::library(d);
let lib = library.get_or_insert_with(|| vectorcraft_brush::library(d));
let r = &out.relink;
let p = place(&mut lib, b, same_doc, |ours, theirs| !fresh && same_brush(r, ours, theirs));
let p = place(lib, b, same_doc, |ours, theirs| !fresh && same_brush(r, ours, theirs));
if let Some(name) = p.new_name(&b.name) {
vectorcraft_brush::store(d, &lib);
vectorcraft_brush::store(d, lib);
added_brushes.insert(name);
}
out.placed(Res::Brush, &b.name, &p);
+13 -6
View File
@@ -125,7 +125,7 @@ macro_rules! style_cmds {
}
/// The params of `charStyle.load`, `paraStyle.load` and `textStyle.loadAll`.
const LOAD_PARAMS: &str = "{path? | data?: file text | dataBase64?, name?: file name (default: the path's)} copy the styles of this kind (textStyle.loadAll: character and paragraph styles) from a document VectorCraft opens (see document.formats) into this one, after its own, in one undo step, with the swatches and patterns they paint with and the styles of either kind that type in those patterns is set in, as Paste brings those of pasted type (a global or spot color whose name this document gives another color is this document's). A style that is the same as the style here of its name, or of its name with a number (`Body 2`), isn't added again; any other is added under a free name (`Body 2`). The file's Normal styles aren't loaded, and a style whose attributes don't read or are out of the range charStyle.new takes, or whose name is empty, is skipped → {added: [names here], existing: [names already here], skipped: [names]} (textStyle.loadAll: {charStyles: {…}, paraStyles: {…}})";
const LOAD_PARAMS: &str = "{path? | data?: file text | dataBase64?, name?: file name (default: the path's)} copy the styles of this kind (textStyle.loadAll: character and paragraph styles) from a document VectorCraft opens (see document.formats) into this one, after its own, in one undo step, with the swatches and patterns they paint with, the symbols and brushes those patterns use, and the styles of either kind that type in those patterns and symbols is set in, as Paste brings those of pasted type (a global or spot color whose name this document gives another color is this document's). A style that is the same as the style here of its name, or of its name with a number (`Body 2`), isn't added again; any other is added under a free name (`Body 2`). The file's Normal styles aren't loaded, and a style whose attributes don't read or are out of the range charStyle.new takes, or whose name is empty, is skipped → {added: [names here], existing: [names already here], skipped: [names in the file]} (textStyle.loadAll: {charStyles: {…}, paraStyles: {…}}); charStyle.load and paraStyle.load list only the styles of their own kind";
pub fn specs() -> Vec<CommandSpec> {
let mut v = style_cmds!(
@@ -972,12 +972,19 @@ fn load(s: &mut Session, p: &Value, kinds: &[Kind]) -> Result<Value> {
}
}
let (chars, paras) = (kinds.contains(&Kind::Char), kinds.contains(&Kind::Para));
let current = s.doc()?.doc.clone();
let mut trial = (*current).clone();
let mut trial = (*s.doc()?.doc).clone();
let mut placed = import_text_styles(&src, &mut trial, chars, paras);
// An undo step only when the document changes.
if trial != *current {
placed = s.edit(label, |d, _| Ok(import_text_styles(&src, d, chars, paras)))?;
// An undo step only when the document changes. The step takes the document the import made,
// unless the edit first ends a typing session, which can change the document it starts from.
if trial != *s.doc()?.doc {
if super::typing_in_progress(s) {
placed = s.edit(label, |d, _| Ok(import_text_styles(&src, d, chars, paras)))?;
} else {
s.edit(label, |d, _| {
*d = trial;
Ok(())
})?;
}
}
let reply = |para: bool| {
let names = |added: bool| placed.iter().filter(|p| p.para == para && p.added == added).map(|p| p.name.as_str()).collect::<Vec<_>>();
+5 -4
View File
@@ -5934,13 +5934,14 @@ uses, in panel order and never the Normal style, and the panel highlights them;
(Load Character Styles… and Load Paragraph Styles… in the panel menus, where `ui.loadCharStyles` and
`ui.loadParaStyles` pick the file) copy the styles of that kind from another document VectorCraft opens (`path`, `data`
or `dataBase64`), and `textStyle.loadAll` (Load All Styles…, `ui.loadAllStyles`) copies both kinds, each in one undo
step. The swatches and patterns the styles paint with, and the styles of either kind that type in those patterns is set
in, come along as Paste brings those of pasted type; a global or spot color whose name the document gives another color
step. The swatches and patterns the styles paint with, the symbols and brushes those patterns use, and the styles of
either kind that type in those patterns and symbols is set in, come along as Paste brings those of pasted type; a global or spot color whose name the document gives another color
is the document's. A loaded style that is the same as the style of
its name, or of its name with a number (`Body 2`), is not added again; any other is added under a free name. The
document's styles and the type that uses them keep their attributes, and the file's Normal styles are not loaded. The
reply lists the names `added`, `existing` and `skipped` (a style whose attributes do not read or are out of the range
`charStyle.new` takes, or whose name is empty); `textStyle.loadAll` replies `{charStyles, paraStyles}`. A Return the Type tool types (or `text.editRange`
reply lists the names `added` and `existing` in the document and the names `skipped` in the file (a style whose
attributes do not read or are out of the range `charStyle.new` takes, or whose name is empty); `textStyle.loadAll`
replies `{charStyles, paraStyles}`, and `charStyle.load` and `paraStyle.load` list only the styles of their own kind. A Return the Type tool types (or `text.editRange`
inserting `\n`) continues the style of the paragraph it splits; deleting a break keeps the first paragraph's style;
`text.setText` gives every paragraph the first one's attributes. Threaded text keeps each paragraph's attributes as
the story re-flows (a paragraph split between frames has them in both).