diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index cf7edc981..155433bd1 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -245,14 +245,15 @@ impl Used { used } - /// Every character and paragraph style of `defs`, and the resources they use. - fn of_text_styles(defs: &Defs<'_>) -> Self { + /// Every character style of `defs` (with `chars`) and every paragraph style (with `paras`), and + /// the resources they use, styles of the other kind included. + fn of_text_styles(defs: &Defs<'_>, chars: bool, paras: bool) -> Self { let mut used = Used::default(); let mut todo = vec![]; - for s in defs.char_styles { + for s in defs.char_styles.iter().filter(|_| chars) { used.note(defs, Ref::Name(Res::CharStyle, &s.name), &mut todo); } - for s in defs.para_styles { + for s in defs.para_styles.iter().filter(|_| paras) { used.note(defs, Ref::Name(Res::ParaStyle, &s.name), &mut todo); } used.follow(defs, todo); @@ -715,13 +716,6 @@ impl<'c> Def<'c> { } } - /// Does it use one of `defs`, including itself? - fn uses_any(&self, defs: &[Def<'_>]) -> bool { - let mut found = false; - self.each_use(&mut |res, n| found |= defs.iter().any(|o| o.key() == (res, n))); - found - } - /// Is it the definition `d` has under `name`, once relinked by `r`? fn same_in(&self, d: &Document, name: &str, r: &Relink) -> bool { match self { @@ -758,32 +752,31 @@ impl<'c> Def<'c> { /// `defs` in rounds: each round takes those that use none of the others left, in their order. /// → (those, the rest, which use each other in a loop or use one that does, in their order) -fn use_order(mut left: Vec>) -> (Vec>, Vec>) { - let mut order = Vec::with_capacity(left.len()); - while !left.is_empty() { - let ready: Vec = left.iter().map(|d| !d.uses_any(&left)).collect(); - if !ready.contains(&true) { - break; +fn use_order(defs: Vec>) -> (Vec>, Vec>) { + let uses = uses_of(&defs); + // The round of each definition; `None` for one in a loop or one that uses such a definition. + let mut round: Vec> = vec![None; defs.len()]; + // A group outside a loop holds one definition, and comes after the groups it uses. + for (group, looped) in components(&uses) { + for v in group.into_iter().filter(|_| !looped) { + let r = uses[v].iter().try_fold(0, |r, &w| round.get(w).copied().flatten().map(|k| r.max(k + 1))); + round[v] = r; } - let mut flags = ready.into_iter(); - let (now, later): (Vec<_>, Vec<_>) = left.into_iter().partition(|_| flags.next().unwrap_or(false)); - order.extend(now); - left = later; } - (order, left) + let mut ready: Vec<(usize, usize)> = round.iter().enumerate().filter_map(|(i, r)| r.map(|r| (r, i))).collect(); + ready.sort_unstable(); + let order = ready.into_iter().filter_map(|(_, i)| defs.get(i).copied()).collect(); + let rest = defs.iter().zip(&round).filter(|(_, r)| r.is_none()).map(|(def, _)| *def).collect(); + (order, rest) } -/// `rest` ([`use_order`]'s definitions that use each other in a loop or use one that does) split -/// into its loops and the definitions outside a loop, each group after the groups it uses: -/// (positions in `rest`, in their order; true for a loop, definitions that use each other or one -/// that uses itself). -fn loop_groups(rest: &[Def<'_>]) -> Vec<(Vec, bool)> { +/// The positions in `defs` of the definitions each one uses, itself included. +fn uses_of(defs: &[Def<'_>]) -> Vec> { let mut at: BTreeMap<(Res, &str), Vec> = BTreeMap::new(); - for (i, def) in rest.iter().enumerate() { + for (i, def) in defs.iter().enumerate() { at.entry(def.key()).or_default().push(i); } - let uses: Vec> = rest - .iter() + defs.iter() .map(|def| { let mut to = vec![]; def.each_use(&mut |res, n| to.extend(at.get(&(res, n)).into_iter().flatten())); @@ -791,12 +784,26 @@ fn loop_groups(rest: &[Def<'_>]) -> Vec<(Vec, bool)> { to.dedup(); to }) - .collect(); - // Tarjan's strongly connected components: each one comes out after those it reaches. + .collect() +} + +/// `rest` ([`use_order`]'s definitions that use each other in a loop or use one that does) split +/// into its loops and the definitions outside a loop, each group after the groups it uses: +/// (positions in `rest`, in their order; true for a loop, definitions that use each other or one +/// that uses itself). +fn loop_groups(rest: &[Def<'_>]) -> Vec<(Vec, bool)> { + components(&uses_of(rest)) +} + +/// The strongly connected components of the graph `uses` (each node's successors), each after +/// those it reaches: (nodes, in their order; true for a loop, two or more nodes or one that uses +/// itself). +fn components(uses: &[Vec]) -> Vec<(Vec, bool)> { + // Tarjan's algorithm, with an explicit stack of calls. const NONE: usize = usize::MAX; - let (mut index, mut low, mut on) = (vec![NONE; rest.len()], vec![0; rest.len()], vec![false; rest.len()]); + let (mut index, mut low, mut on) = (vec![NONE; uses.len()], vec![0; uses.len()], vec![false; uses.len()]); let (mut stack, mut next, mut groups) = (vec![], 0, vec![]); - for root in 0..rest.len() { + for root in 0..uses.len() { if index[root] != NONE { continue; } @@ -1195,17 +1202,18 @@ pub(crate) struct LoadedStyle { pub added: bool, } -/// Add every character and paragraph style of `src` to `d`, with the swatches, patterns and -/// what their art uses that the styles' fill and stroke lead to, as Paste adds those of pasted -/// type ([`Clipboard::import_into`]; a global or spot color whose name `d` gives another color -/// is `d`'s). Each style, symbol and pattern is placed after those it uses, so that a style -/// follows a pattern added under a free name and a pattern follows a style renamed on the way -/// in. A style `d` has under its name, or a numbered copy of it ("Body 2"), that is the -/// same once relinked is used; any other is added under a free name. → each style of `src`, in -/// order, character styles first. -pub(crate) fn import_text_styles(src: &Document, d: &mut Document) -> Vec { +/// Add every character style of `src` (with `chars`) and every paragraph style (with `paras`) to +/// `d`, with the swatches, patterns and what their art uses (styles of the other kind included) +/// that the styles' fill and stroke lead to, as Paste adds those of pasted type +/// ([`Clipboard::import_into`]; a global or spot color whose name `d` gives another color is +/// `d`'s). Each style, symbol and pattern is placed after those it uses, so that a style follows +/// a pattern added under a free name and a pattern follows a style renamed on the way in. A style +/// `d` has under its name, or a numbered copy of it ("Body 2"), that is the same once relinked is +/// used; any other is added under a free name. → each style of `src` placed, in order, character +/// styles first. +pub(crate) fn import_text_styles(src: &Document, d: &mut Document, chars: bool, paras: bool) -> Vec { let library = OnceCell::new(); - let used = Used::of_text_styles(&Defs::of(src, &library)); + 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); diff --git a/crates/engine/src/cmd/textstyles.rs b/crates/engine/src/cmd/textstyles.rs index 62d94a89a..47677e78b 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, 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 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 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 whose name is empty, is skipped → {added: [names here], existing: [names already here], skipped: [names]} (textStyle.loadAll: {charStyles: {…}, paraStyles: {…}})"; pub fn specs() -> Vec { let mut v = style_cmds!( @@ -955,20 +955,24 @@ fn load(s: &mut Session, p: &Value, kinds: &[Kind]) -> Result { if src.preview_only { return Err(bad(c, "only this Affinity file's embedded preview could be read; use File › Open to see it with its warning")); } - // The source with only the styles that load. + // The source with only the styles that load. Those of a kind not loaded still come along when + // a pattern or symbol that a loaded style paints with sets type in them. let mut src = src.doc; let mut skipped = [vec![], vec![]]; for kind in [Kind::Char, Kind::Para] { - let (defs, names) = if kinds.contains(&kind) { loadable(&src, kind) } else { (vec![], vec![]) }; + let (defs, names) = loadable(&src, kind); *kind.defs_mut(&mut src) = defs; - skipped[usize::from(kind == Kind::Para)] = names; + if kinds.contains(&kind) { + skipped[usize::from(kind == Kind::Para)] = names; + } } + let (chars, paras) = (kinds.contains(&Kind::Char), kinds.contains(&Kind::Para)); let current = s.doc()?.doc.clone(); let mut trial = (*current).clone(); - let mut placed = import_text_styles(&src, &mut trial); + 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)))?; + placed = s.edit(label, |d, _| Ok(import_text_styles(&src, d, chars, paras)))?; } 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/crates/engine/src/cmd/textstyles/tests.rs b/crates/engine/src/cmd/textstyles/tests.rs index 9998bcafb..8d68fedc8 100644 --- a/crates/engine/src/cmd/textstyles/tests.rs +++ b/crates/engine/src/cmd/textstyles/tests.rs @@ -951,6 +951,34 @@ fn loaded_style_keeps_the_name_used_by_pattern_type() { assert_eq!(text.runs[0].style.style_name.as_deref(), Some(" Accent ")); } +/// Load Character Styles brings the paragraph style that type in a loaded style's pattern is set +/// in, renamed when the document has another style of that name, without listing it in the reply. +#[test] +fn loaded_pattern_type_keeps_its_paragraph_style() { + let (mut src, id) = session_with_text("Tile"); + src.execute("paraStyle.new", &json!({"name": "Centered", "attrs": {"justify": "Center"}})).unwrap(); + src.execute("paraStyle.apply", &json!({"name": "Centered", "id": id})).unwrap(); + src.execute("object.pattern.make", &json!({"name": "Tiles", "edit": false, "ids": [id]})).unwrap(); + let other = src.execute("text.create", &json!({"x": 10, "y": 90, "text": "B"})).unwrap()["id"].as_u64().unwrap(); + src.execute("paint.setFill", &json!({"swatch": "Tiles", "ids": [other]})).unwrap(); + src.execute("charStyle.new", &json!({"name": "Tiled", "id": other})).unwrap(); + let data = vectorcraft_format::base64_encode(&vectorcraft_format::save(&src.doc().unwrap().doc, false)); + let (mut s, _) = session_with_text("X"); + s.execute("paraStyle.new", &json!({"name": "Centered", "attrs": {"justify": "Right"}})).unwrap(); + let load = json!({"dataBase64": data, "name": "Other.vectorcraft"}); + let r = s.execute("charStyle.load", &load).unwrap(); + assert_eq!(r, json!({"added": ["Tiled"], "existing": [], "skipped": []})); + let d = s.doc().unwrap().doc.clone(); + assert_eq!(listed(&mut s, "paraStyle"), [NORMAL_PARA, "Centered", "Centered 2"]); + assert_eq!(super::style_attrs(&d, true, "Centered 2").map(Value::Object), Some(json!({"justify": "Center"}))); + let NodeKind::Text(text) = &d.pattern("Tiles").unwrap().art[0].kind else { panic!("pattern art should be type") }; + assert_eq!(text.para_styles().filter_map(|p| p.style_name.as_deref()).collect::>(), ["Centered 2"]); + // Loaded again, nothing is added. + let r = s.execute("charStyle.load", &load).unwrap(); + assert_eq!(r["added"], json!([]), "{r}"); + assert_eq!(s.doc().unwrap().doc, d); +} + /// A pattern whose tile paints with itself is found under its renamed copy on a second load. #[test] fn self_referencing_pattern_is_not_added_again() { @@ -971,7 +999,7 @@ fn self_referencing_pattern_is_not_added_again() { let different = dst.execute("shape.rectangle", &json!({"x": 200, "y": 200, "width": 30, "height": 30})).unwrap()["id"].clone(); dst.execute("object.pattern.make", &json!({"name": "Dots", "edit": false, "ids": [different]})).unwrap(); let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc); - super::super::clipboard::resources::import_text_styles(&source, target); + super::super::clipboard::resources::import_text_styles(&source, target, true, true); assert!(dst.doc().unwrap().doc.pattern("Dots 2").is_some()); assert!( dst.doc().unwrap().doc.pattern("Dots 2").unwrap().art[0] @@ -982,7 +1010,7 @@ fn self_referencing_pattern_is_not_added_again() { ); let before = dst.doc().unwrap().doc.clone(); let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc); - let again = super::super::clipboard::resources::import_text_styles(&source, target); + let again = super::super::clipboard::resources::import_text_styles(&source, target, true, true); assert!(again.iter().all(|s| !s.added)); assert_eq!(dst.doc().unwrap().doc, before); } @@ -1014,13 +1042,13 @@ fn separate_self_referencing_patterns_are_not_added_again() { dst.execute("object.pattern.make", &json!({"name": format!("Dots{k}"), "edit": false, "ids": [different]})).unwrap(); } let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc); - super::super::clipboard::resources::import_text_styles(&source, target); + super::super::clipboard::resources::import_text_styles(&source, target, true, true); for k in 0..9 { assert!(dst.doc().unwrap().doc.pattern(&format!("Dots{k} 2")).is_some(), "{k}"); } let before = dst.doc().unwrap().doc.clone(); let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc); - let again = super::super::clipboard::resources::import_text_styles(&source, target); + let again = super::super::clipboard::resources::import_text_styles(&source, target, true, true); assert!(again.iter().all(|s| !s.added)); assert_eq!(dst.doc().unwrap().doc.patterns.len(), before.patterns.len()); assert_eq!(dst.doc().unwrap().doc, before); @@ -1028,7 +1056,7 @@ fn separate_self_referencing_patterns_are_not_added_again() { let target = std::sync::Arc::make_mut(&mut dst.doc_mut().unwrap().doc); target.patterns.retain(|p| !p.name.starts_with("Dots8")); let left = target.patterns.len(); - let third = super::super::clipboard::resources::import_text_styles(&source, target); + let third = super::super::clipboard::resources::import_text_styles(&source, target, true, true); assert_eq!(dst.doc().unwrap().doc.patterns.len(), left + 1); assert!(third.iter().filter(|s| s.name != "Patterned8" && !s.name.starts_with("Patterned8 ")).all(|s| !s.added)); } diff --git a/docs/mcp.md b/docs/mcp.md index 45b7708d5..495bdae3f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5934,8 +5934,9 @@ 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 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 +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 +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 whose name is