Edit: a copied compound member takes the compound's graphic style link

Make Compound Path leaves each member's graphic style id in place. A
member copied or cut on its own took the compound's appearance but kept
that old id, so the clipboard carried an unrelated style and the pasted
path was linked to a style that did not match its look.

The copied member or piece now takes the compound's link while it has
that style's look, and no link otherwise (no style on the compound, or
a gradient pinned to the compound's bounds). It also takes the
compound's knockout setting with the rest of its transparency.
This commit is contained in:
dotCooCoo
2026-11-10 13:28:28 -08:00
parent 73ada5181d
commit c92dad3838
4 changed files with 78 additions and 6 deletions
+15 -3
View File
@@ -264,6 +264,8 @@ impl Clipboard {
.and_then(|p| *compound_bounds.entry(p.id).or_insert_with(|| p.geometric_bounds()));
// A compound's member or piece copied on its own takes the compound's look (its paint,
// transparency, opacity mask, pixel alignment and fill rule), so it pastes as it looked.
// It is linked to the compound's graphic style while it still has that style's look
// (a gradient pinned to the compound's bounds does not), else to none.
let inherit_appearance = |mut piece: Node| {
if let Some(parent) = compound_parent
&& let (NodeKind::Compound { rule: compound_rule, .. }, NodeKind::Path { rule, .. }) = (&parent.kind, &mut piece.kind)
@@ -276,11 +278,21 @@ impl Clipboard {
piece.opacity = parent.opacity;
piece.blend = parent.blend;
piece.isolate = parent.isolate;
piece.knockout = parent.knockout;
piece.knockout_shape = parent.knockout_shape;
let (mask, pixel_aligned) = (parent.extras().mask.clone(), parent.extras().pixel_aligned);
let mut extras = piece.extras_mut();
extras.mask = mask;
extras.pixel_aligned = pixel_aligned;
{
let mut extras = piece.extras_mut();
extras.mask = mask;
extras.pixel_aligned = pixel_aligned;
}
let link = parent
.extras()
.graphic_style
.and_then(|g| d.graphic_style_by_id(g))
.filter(|g| super::style::in_sync(&piece, g))
.map(|g| g.id);
piece.extras_mut().graphic_style = link;
}
piece
};
+1 -1
View File
@@ -166,7 +166,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Copy",
["Edit"],
Some("Cmd+C"),
"{selectedArtboards?: [n…] (the artboards the Artboard tool had selected, which the journal records for a replay; it runs artboard.copy with any tool chosen), art?: bool (default: the Artboard tool's moveArt option; the journal records it beside selectedArtboards), lockedAndHidden?: bool (the Artboard tool chosen, as artboard.copy)} the selection with what it uses (a compound-path member selected on its own, whole or in part, with Direct or Group Selection, is copied as an ordinary path with the compound's paint, transparency, opacity mask and fill rule, a gradient keeping the compound's fit; the compound is copied when it is selected, or when every member is selected whole outside isolation mode), and the selected ruler guides (pasted on the layer new art goes to, moved as the objects are; alone, where they were); the Artboard tool chosen: artboard.copy; with an object of a symbol instance's art selected, the instance is copied whole → {copied, guides}",
"{selectedArtboards?: [n…] (the artboards the Artboard tool had selected, which the journal records for a replay; it runs artboard.copy with any tool chosen), art?: bool (default: the Artboard tool's moveArt option; the journal records it beside selectedArtboards), lockedAndHidden?: bool (the Artboard tool chosen, as artboard.copy)} the selection with what it uses (a compound-path member selected on its own, whole or in part, with Direct or Group Selection, is copied as an ordinary path with the compound's paint, transparency, opacity mask, fill rule and graphic style link (while it has that style's look), a gradient keeping the compound's fit; the compound is copied when it is selected, or when every member is selected whole outside isolation mode), and the selected ruler guides (pasted on the layer new art goes to, moved as the objects are; alone, where they were); the Artboard tool chosen: artboard.copy; with an object of a symbol instance's art selected, the instance is copied whole → {copied, guides}",
has_selection_guides_or_artboard_tool,
copy
),
@@ -604,6 +604,66 @@ fn a_copied_member_with_its_own_appearance_takes_the_compounds() {
assert_eq!(s.clipboard.nodes[0].appearance, compound_look);
}
/// A member copied or cut on its own, whole or in part, takes the compound's graphic style link
/// with its look, or no link when the compound has none or the member no longer has the style's
/// look (a gradient pinned to the compound's bounds); the style the member had before Make Compound
/// Path does not go with it.
#[test]
fn a_copied_member_takes_the_compounds_graphic_style() {
let style_of = |s: &Session, id: NodeId| s.doc().unwrap().doc.node(id).unwrap().extras().graphic_style;
let clipboard_styles = |s: &Session| s.clipboard.graphic_styles.iter().map(|g| g.name.clone()).collect::<Vec<_>>();
for (compound_styled, gradient) in [(false, false), (true, false), (true, true)] {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
let m0 = rect(&mut s, 10.0, 10.0, 40.0, 40.0);
run(&mut s, "paint.setFill", json!({"ids": [m0.0], "color": "#00ff00"}));
run(&mut s, "select.set", json!({"ids": [m0.0]}));
run(&mut s, "graphicStyle.new", json!({"name": "Member"}));
let member_style = style_of(&s, m0);
assert!(member_style.is_some(), "the path is linked to the style made from it");
let m1 = rect(&mut s, 60.0, 10.0, 40.0, 40.0);
run(&mut s, "select.set", json!({"ids": [m0.0, m1.0]}));
let compound = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap());
let fill =
if gradient { json!({"ids": [compound.0], "gradient": {"kind": "linear"}}) } else { json!({"ids": [compound.0], "color": "#ff0000"}) };
run(&mut s, "paint.setFill", fill);
assert_eq!(style_of(&s, m0), member_style, "the member keeps its old link inside the compound");
let compound_style = if compound_styled {
run(&mut s, "select.set", json!({"ids": [compound.0]}));
run(&mut s, "graphicStyle.new", json!({"name": "Compound"}));
style_of(&s, compound)
} else {
None
};
assert_eq!(compound_style.is_some(), compound_styled);
// A gradient fitted to the compound is pinned to its bounds on the copy, which is no longer
// the style's look.
let link = compound_style.filter(|_| !gradient);
let expected: Vec<String> = if link.is_some() { vec!["Compound".into()] } else { vec![] };
let case = format!("compound styled: {compound_styled}, gradient: {gradient}");
s.select_tool("groupSelection", vectorcraft_engine::ViewInfo::default()).unwrap();
run(&mut s, "select.set", json!({"ids": [m0.0]}));
run(&mut s, "edit.copy", json!({}));
assert_eq!(s.clipboard.nodes[0].id, m0);
assert_eq!(s.clipboard.nodes[0].extras().graphic_style, link, "whole member, {case}");
assert_eq!(clipboard_styles(&s), expected, "whole member, {case}");
run(&mut s, "select.segments", json!({"id": m0.0, "segments": [[0, 0]]}));
run(&mut s, "edit.copy", json!({}));
assert_eq!(s.clipboard.nodes.len(), 1);
assert_eq!(s.clipboard.nodes[0].extras().graphic_style, link, "one segment, {case}");
assert_eq!(clipboard_styles(&s), expected, "one segment, {case}");
run(&mut s, "select.set", json!({"ids": [m0.0]}));
run(&mut s, "edit.cut", json!({}));
assert_eq!(members_of(&s, compound), Some(vec![m1]));
let pasted = NodeId(run(&mut s, "edit.pasteInPlace", json!({}))["ids"][0].as_u64().unwrap());
assert_eq!(style_of(&s, pasted), link, "Cut and Paste in Place, {case}");
check_session(&s).unwrap();
}
}
#[test]
fn every_segment_of_every_member_picked_copies_and_cuts_the_compound() {
let mut s = Session::new();
+2 -2
View File
@@ -1712,8 +1712,8 @@ With `ids`, `guides: true` also deletes the selected ruler guides. The command r
boolean.
`edit.copy` takes a compound-path member selected on its own (whole or in part, with Direct or Group Selection) as an
ordinary path with the compound's paint, transparency, opacity mask and fill rule. A gradient fitted to the compound
keeps the compound's fit. Copy takes the whole compound when the compound itself is selected, or when every member is
ordinary path with the compound's paint, transparency, opacity mask and fill rule. The path is linked to the compound's
graphic style while it has that style's look, and otherwise to no style. A gradient fitted to the compound keeps the compound's fit. Copy takes the whole compound when the compound itself is selected, or when every member is
selected whole outside isolation mode, which includes the one member of a compound of one. In an isolated compound
path each member is taken as itself. `edit.cut` removes the same objects it copies, so cutting one member leaves the
other members in the compound, a compound of one member included. When Clear deletes every anchor or segment of every