From cbe374df628168d49bab1938bf81549664b5719f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 19:34:38 -0800 Subject: [PATCH] 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. --- crates/engine/src/cmd/clipboard/resources.rs | 32 ++++++++++++-------- crates/engine/src/cmd/textstyles.rs | 19 ++++++++---- docs/mcp.md | 9 +++--- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index 91e9bbb3e..4488b4746 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -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 { + /// 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 { let (res, name) = self.key(); let names: Vec = 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]) -> Vec<(Vec, 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> { - let names: Vec> = looped.iter().map(|def| def.names_in(d)).collect(); + let library = OnceCell::new(); + let brushes = Brushes::Lazy(d, &library); + let names: Vec> = 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 = 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> = 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); diff --git a/crates/engine/src/cmd/textstyles.rs b/crates/engine/src/cmd/textstyles.rs index ffc75acc9..f8ce2d691 100644 --- a/crates/engine/src/cmd/textstyles.rs +++ b/crates/engine/src/cmd/textstyles.rs @@ -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 { let mut v = style_cmds!( @@ -972,12 +972,19 @@ fn load(s: &mut Session, p: &Value, kinds: &[Kind]) -> Result { } } 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::>(); diff --git a/docs/mcp.md b/docs/mcp.md index 020712101..7fdd4fc4b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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).