From 73af2379d009a3c5978bb7d3267ec2d56a618b67 Mon Sep 17 00:00:00 2001 From: Jerome CAMUS Date: Fri, 30 Oct 2026 08:38:28 +0100 Subject: [PATCH] Fix #2091 clear stale key and vertex selections around mask copy/paste Selecting a mask group now clears the previous key selection, so Copy/Cut act on the mask. Pasting masks clears the source mask's vertex selection, so Clear removes the pasted mask rather than points of the source. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/prop.rs | 5 ++--- crates/engine/src/cmd/prop_groups.rs | 2 ++ crates/engine/src/tests/menu_cmds.rs | 29 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/prop.rs b/crates/engine/src/cmd/prop.rs index 50c261020..b4432dd2f 100644 --- a/crates/engine/src/cmd/prop.rs +++ b/crates/engine/src/cmd/prop.rs @@ -794,6 +794,8 @@ fn select_prop(s: &mut Session, p: &Value) -> Result { if !add { s.state.selected_props.clear(); s.state.selected_vertices.clear(); + // Groups (masks, effects) have no keys of their own: don't leave the old selection's behind. + s.state.selected_keys.clear(); } if !s.state.selected_layers.contains(&lid) { s.state.selected_layers = vec![lid]; @@ -811,9 +813,6 @@ fn select_prop(s: &mut Session, p: &Value) -> Result { && let Some(pr) = s.active_comp().and_then(|c| c.layer(lid)).and_then(|l| l.props.find(uid)) { let keys: Vec = pr.keys.iter().map(|k| KeyRef { layer: lid, prop: uid, time: k.time }).collect(); - if !add { - s.state.selected_keys.clear(); - } s.state.selected_keys.extend(keys); } Ok(Value::Null) diff --git a/crates/engine/src/cmd/prop_groups.rs b/crates/engine/src/cmd/prop_groups.rs index 8976e89fc..b3e9baf14 100644 --- a/crates/engine/src/cmd/prop_groups.rs +++ b/crates/engine/src/cmd/prop_groups.rs @@ -183,6 +183,8 @@ pub(crate) fn paste_masks(s: &mut Session, p: &Value) -> Result { } proj.next_id = next + 1; st.selected_props = out.clone(); + // The source mask's points (selected with it) must not outlive its selection. + st.selected_vertices.clear(); Ok(out) })?; Ok(json!({"masks": pasted.iter().map(|(_, u)| *u).collect::>()})) diff --git a/crates/engine/src/tests/menu_cmds.rs b/crates/engine/src/tests/menu_cmds.rs index bf4daf1a4..d30fc156a 100644 --- a/crates/engine/src/tests/menu_cmds.rs +++ b/crates/engine/src/tests/menu_cmds.rs @@ -2006,3 +2006,32 @@ fn copy_with_an_explicit_layer_ignores_the_selected_mask() { assert_eq!(s.state.clipboard.len(), 1, "the named layer is copied"); assert!(s.state.mask_clipboard.is_empty()); } + +#[test] +fn selecting_a_mask_drops_stale_key_selection_so_copy_copies_the_mask() { + let mut s = comp(); + let a = solid(&mut s, "#ff0000"); + s.execute("layer.select", json!({"layers": [a]})).unwrap(); + s.execute("layer.addMask", json!({})).unwrap(); + let mask = layer(&s, a).masks().unwrap().groups().next().unwrap().uid; + s.state.selected_keys = vec![crate::KeyRef { layer: effectcraft_project::LayerId(a), prop: 1, time: Default::default() }]; + s.execute("prop.select", json!({"layer": a, "prop": mask})).unwrap(); + assert!(s.state.selected_keys.is_empty()); + assert_eq!(s.execute("edit.copy", json!({})).unwrap()["masks"], json!(1)); +} + +#[test] +fn pasting_a_mask_onto_its_own_layer_drops_the_source_vertex_selection() { + let mut s = comp(); + let a = solid(&mut s, "#ff0000"); + s.execute("layer.select", json!({"layers": [a]})).unwrap(); + s.execute("layer.addMask", json!({})).unwrap(); + let mask = layer(&s, a).masks().unwrap().groups().next().unwrap().uid; + s.execute("prop.select", json!({"layer": a, "prop": mask})).unwrap(); + assert!(!s.state.selected_vertices.is_empty()); + s.execute("edit.copy", json!({})).unwrap(); + s.execute("edit.paste", json!({})).unwrap(); + assert!(s.state.selected_vertices.is_empty()); + s.execute("edit.clear", json!({})).unwrap(); + assert_eq!(layer(&s, a).masks().unwrap().groups().count(), 1, "the pasted mask goes, the source stays"); +}