diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index f457712415..fa69f1f820 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -595,6 +595,8 @@ struct Relink { styles: BTreeMap, /// Swatch (its name in the document) → the colour linked colours show at 100 %. colors: BTreeMap, + /// Leave graphic style links as they are ([`Clipboard::show_merged_colors`]). + keep_styles: bool, } impl Relink { @@ -674,7 +676,9 @@ impl Relink { /// Relink a node tree (the mirror of [`node_refs`]). fn node(&self, n: &mut Node) { self.appearance(&mut n.appearance); - n.extras_mut().graphic_style = n.extras().graphic_style.and_then(|id| self.styles.get(&id).copied()); + if !self.keep_styles { + n.extras_mut().graphic_style = n.extras().graphic_style.and_then(|id| self.styles.get(&id).copied()); + } if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) { self.node(Arc::make_mut(&mut m.art)); } @@ -1109,6 +1113,30 @@ impl Clipboard { self.import_used(d, &self.used(), choices, same_doc) } + /// Give the objects' colors linked to a global or spot swatch whose conflict with `d` + /// `choices` merge the color that swatch has in `d`, as pasting into `d` gives them (Paste + /// without Formatting sets its pie legend labels' contrast from the wedge colors before it + /// pastes). Nothing changes for the document the objects came from (`same_doc`). + pub(crate) fn show_merged_colors(&mut self, d: &Document, choices: &SwatchChoices, same_doc: bool) { + if same_doc { + return; + } + let used = self.used(); + let colors: BTreeMap = self + .conflicts(d, &used) + .into_iter() + .filter(|(w, _)| choices.of(&w.name) == SwatchChoice::Merge) + .filter_map(|(w, _)| d.global_color(&w.name).map(|c| (w.name.clone(), c))) + .collect(); + if colors.is_empty() { + return; + } + let relink = Relink { colors, keep_styles: true, ..Default::default() }; + for n in &mut self.nodes { + relink.node(n); + } + } + /// [`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 diff --git a/crates/engine/src/cmd/typemenu.rs b/crates/engine/src/cmd/typemenu.rs index 0701f6eeca..da930d7e59 100644 --- a/crates/engine/src/cmd/typemenu.rs +++ b/crates/engine/src/cmd/typemenu.rs @@ -1783,6 +1783,13 @@ fn strip_formatting(n: &mut vectorcraft_doc::Node) { fn paste_plain(s: &mut Session, p: &Value) -> Result { let saved = s.clipboard.nodes.clone(); + // Pasted colors linked to a swatch merged with the document's show its color, so a pasted pie's legend labels take + // the contrast of the wedges as pasted. A `swatchConflict` the paste refuses is left to its error. + if let (Ok(choices), Ok(st)) = (super::clipboard::SwatchChoices::parse("edit.pasteWithoutFormatting", p), s.doc()) { + let same_doc = s.clipboard.source_doc == Some(st.uid); + let d = std::sync::Arc::clone(&st.doc); + s.clipboard.show_merged_colors(&d, &choices, same_doc); + } for n in &mut s.clipboard.nodes { strip_formatting(n); } diff --git a/crates/engine/src/tests/clipboard.rs b/crates/engine/src/tests/clipboard.rs index 368bc6de10..519974f33d 100644 --- a/crates/engine/src/tests/clipboard.rs +++ b/crates/engine/src/tests/clipboard.rs @@ -1034,6 +1034,68 @@ fn a_repainted_wedge_sets_its_label_color_when_pasted_without_formatting() { assert_eq!(pasted, labels(&s, p), "copied as {before:?}"); } +/// Wedges linked to a global swatch that the destination document gives a dark color, merged on Paste without +/// Formatting, give their legend labels white characters, as a redraw does; added under a new name, they keep the +/// copied light color and black labels. +#[test] +fn wedges_merged_into_a_dark_swatch_give_white_labels_when_pasted_without_formatting() { + let csv = ",North,South\nP,1,1"; + fn labels(s: &Session, id: NodeId) -> Vec<(String, Paint)> { + let mut out = vec![]; + node(s, id).walk(&mut |n| { + if let NodeKind::Text(t) = &n.kind + && ["North", "South"].contains(&t.plain_text().as_str()) + { + out.push((t.plain_text(), t.first_style().fill.clone())); + } + }); + out + } + let both = |c: Color| vec![("North".to_string(), Paint::solid(c)), ("South".to_string(), Paint::solid(c))]; + for (conflict, expected) in [("merge", Color::WHITE), ("add", Color::BLACK)] { + let mut s = session(); + let g = id_of(&s.execute("graph.create", &json!({"type": "pie", "x": 50, "y": 50, "width": 200, "height": 200, "csv": csv})).unwrap()); + s.execute("graph.setType", &json!({"id": g.0, "pieLegend": "wedges"})).unwrap(); + s.execute("swatch.new", &json!({"name": "Ink", "color": "#ffffff", "global": true})).unwrap(); + let mut wedges = vec![]; + node(&s, g).walk(&mut |m| { + if let NodeKind::Path { .. } = m.kind { + wedges.push(m.id.0); + } + }); + s.execute("paint.setFill", &json!({"ids": wedges, "swatch": "Ink"})).unwrap(); + s.execute("graph.setData", &json!({"id": g.0, "csv": csv})).unwrap(); + assert_eq!(labels(&s, g), both(Color::BLACK), "light wedges have black labels"); + copy(&mut s, &[g]); + new_doc(&mut s); + s.execute("swatch.new", &json!({"name": "Ink", "color": "#000000", "global": true})).unwrap(); + let p = ids_of(&s.execute("edit.pasteWithoutFormatting", &json!({"swatchConflict": conflict})).unwrap())[0]; + let pasted = labels(&s, p); + assert_eq!(pasted, both(expected), "{conflict}"); + s.execute("graph.setData", &json!({"id": p.0, "csv": csv})).unwrap(); + assert_eq!(pasted, labels(&s, p), "{conflict}: as a redraw sets them"); + } +} + +/// An object linked to a graphic style and to a swatch merged with the document's keeps its graphic style link when +/// pasted without formatting, and its color shows the document's swatch. +#[test] +fn a_merged_swatch_keeps_graphic_style_links_when_pasted_without_formatting() { + let mut s = session(); + let r = swatched_rect(&mut s, "Ink", "#ffffff"); + sel(&mut s, &[r]); + s.execute("graphicStyle.new", &json!({"name": "Sun"})).unwrap(); + copy(&mut s, &[r]); + new_doc(&mut s); + s.execute("swatch.new", &json!({"name": "Ink", "color": "#000000", "global": true})).unwrap(); + let v = s.execute("edit.pasteWithoutFormatting", &json!({})).unwrap(); + assert_eq!(v["merged"], 1); + let p = ids_of(&v)[0]; + let style = node(&s, p).extras().graphic_style.and_then(|id| doc(&s).graphic_style_by_id(id)).map(|g| g.name.clone()); + assert_eq!(style.as_deref(), Some("Sun")); + assert_eq!(fill(&s, p), linked(hex("#000000"), "Ink", 1.0)); +} + /// When one pie's wedge of a series is repainted and another pie's wedge of that series is not, each pasted legend label /// takes the contrast of the wedge it is in. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 4548f297d2..8bff12d9d5 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -7247,7 +7247,9 @@ to}]}`. `edit.pasteWithoutFormatting` sets the labels of a pasted graph in a new graph's label character style and paragraph attributes, keeping each label's alignment and where it starts. The pasted graph no longer keeps its labels' character and paragraph formats, so a later `graph.setData` or `graph.setType` sets the labels the same way. The rest of the -graph's art is pasted as it was copied. +graph's art is pasted as it was copied. A pie's legend labels in its wedges are white on a dark wedge and black on a +light one, by the wedge colors as pasted: a wedge linked to a swatch merged with the document's takes that swatch's +color first. A global or spot swatch whose name the document gives another colour is a conflict: `clipboard.conflicts` lists them, and `swatchConflict` answers: `"merge"` (the default: the objects take the document's swatch), `"add"` (the