Shapes: Duplicate duplicates the selected shape items, not the layer (#227)
Edit > Duplicate (Ctrl+D) with a shape group or other shape item
selected in the Timeline duplicated the whole shape layer. As in After
Effects it now duplicates the selected items inside their group, each
copy above its original and numbered on ("Rectangle 1" gives
"Rectangle 2"), and selects the copies. With only layers selected the
layers are duplicated as before.
Test: duplicate_with_shape_items_selected_duplicates_them_in_place,
ctrl_d_duplicates_the_selected_shape_group_in_its_layer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
95bb346c89
commit
0048569682
|
|
@ -109,6 +109,11 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
return Ok(json!({"effects": out}));
|
||||
}
|
||||
// Shape items selected → duplicate them inside their layers.
|
||||
let items = super::prop_groups::selected_contents(s);
|
||||
if !items.is_empty() {
|
||||
return super::prop_groups::duplicate_contents(s, &items);
|
||||
}
|
||||
}
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
let new = s.edit("Duplicate", None, |proj, st| {
|
||||
|
|
@ -701,7 +706,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
),
|
||||
cmd!("edit.pasteReversedKeyframes", "Paste Reversed Keyframes", ["Edit"], None, "{layers?, prop?|path?, time?}", has_key_clip, paste_reversed),
|
||||
cmd!("edit.clear", "Clear", ["Edit"], Some("Delete"), "{layers?}", layers_or_keys, delete),
|
||||
cmd!("edit.duplicate", "Duplicate", ["Edit"], Some("Cmd+D"), "{layers?}", has_layers, duplicate),
|
||||
cmd!("edit.duplicate", "Duplicate", ["Edit"], Some("Cmd+D"), "{layers?} (effects or shape items when they are selected)", has_layers, duplicate),
|
||||
cmd!("edit.splitLayer", "Split Layer", ["Edit"], Some("Cmd+Shift+D"), "{layers?}", has_layers, split),
|
||||
cmd!("edit.liftWorkArea", "Lift Work Area", ["Edit"], None, "{layers?}", has_comp, lift),
|
||||
cmd!("edit.extractWorkArea", "Extract Work Area", ["Edit"], None, "{layers?}", has_comp, extract),
|
||||
|
|
|
|||
|
|
@ -174,6 +174,30 @@ pub(crate) fn paste_contents(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({"contents": pasted.iter().map(|(_, u)| *u).collect::<Vec<_>>()}))
|
||||
}
|
||||
|
||||
/// Edit ▸ Duplicate with shape items selected: each is copied in place, above the original and
|
||||
/// named as After Effects does ("Rectangle 1" → "Rectangle 2"), and the copies are selected.
|
||||
pub(crate) fn duplicate_contents(s: &mut Session, items: &[(LayerId, Uid)]) -> Result<Value> {
|
||||
let c = "edit.duplicate";
|
||||
let cid = s.active_comp_id().ok_or(EngineError::NoComp)?;
|
||||
let copies = s.edit("Duplicate", None, |proj, st| {
|
||||
let mut next = proj.next_id;
|
||||
let mut out = vec![];
|
||||
for (lid, uid) in items {
|
||||
let parent = layer_mut(proj, cid, *lid)?.props.parent_of_mut(*uid).ok_or_else(|| bad(c, "the shape item is gone"))?;
|
||||
let Some((i, Node::Group(g))) = parent.children.iter().enumerate().find(|(_, n)| n.uid() == *uid) else { continue };
|
||||
let mut g = g.clone();
|
||||
g.reassign_uids(&mut next);
|
||||
g.name = super::effect::unique_name(parent, &g.name);
|
||||
out.push((*lid, g.uid));
|
||||
parent.children.insert(i, Node::Group(g));
|
||||
}
|
||||
proj.next_id = next + 1;
|
||||
st.selected_props = out.clone();
|
||||
Ok(out)
|
||||
})?;
|
||||
Ok(json!({"contents": copies.iter().map(|(_, u)| *u).collect::<Vec<_>>()}))
|
||||
}
|
||||
|
||||
fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = "prop.duplicateGroup";
|
||||
let (cid, lid, uid) = group_ref(s, p, c, true)?;
|
||||
|
|
|
|||
|
|
@ -1111,3 +1111,38 @@ fn shape_contents_copy_cut_and_paste_between_shape_layers() {
|
|||
s.execute("layer.select", json!({"layers": [sol]})).unwrap();
|
||||
assert!(s.execute("edit.paste", json!({})).is_err());
|
||||
}
|
||||
|
||||
/// Edit ▸ Duplicate with shape items selected duplicates them in their layer, above the
|
||||
/// original and named "Rectangle 2", not the layer (#227).
|
||||
#[test]
|
||||
fn duplicate_with_shape_items_selected_duplicates_them_in_place() {
|
||||
let mut s = comp();
|
||||
let a = s.execute("layer.newShape", json!({"kind": "rect"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let n = s.active_comp().unwrap().layers.len();
|
||||
let rect = contents_item(&s, a, "Rectangle 1");
|
||||
s.execute("prop.select", json!({"layer": a, "prop": rect})).unwrap();
|
||||
let r = s.execute("edit.duplicate", json!({})).unwrap();
|
||||
assert_eq!(contents_names(&s, a), ["Rectangle 2", "Rectangle 1"]);
|
||||
assert_eq!(s.active_comp().unwrap().layers.len(), n, "no new layer");
|
||||
let copy = r["contents"][0].as_u64().unwrap();
|
||||
assert_eq!(s.state.selected_props, vec![(effectcraft_project::LayerId(a), copy)]);
|
||||
let l = layer(&s, a);
|
||||
let path_uid = |g: u64| l.props.find_group(g).unwrap().sub("contents").unwrap().groups().next().unwrap().uid;
|
||||
assert_ne!(path_uid(copy), path_uid(rect), "the copy has its own properties");
|
||||
// Again, with the copy selected: Rectangle 3, above it.
|
||||
s.execute("edit.duplicate", json!({})).unwrap();
|
||||
assert_eq!(contents_names(&s, a), ["Rectangle 3", "Rectangle 2", "Rectangle 1"]);
|
||||
s.undo();
|
||||
s.undo();
|
||||
assert_eq!(contents_names(&s, a), ["Rectangle 1"]);
|
||||
// An item inside a group is duplicated in that group.
|
||||
let fill = layer(&s, a).props.find_group(rect).unwrap().sub("contents").unwrap().groups().find(|g| g.match_id == "fill").unwrap().uid;
|
||||
s.execute("prop.select", json!({"layer": a, "prop": fill})).unwrap();
|
||||
s.execute("edit.duplicate", json!({})).unwrap();
|
||||
let inner: Vec<String> = layer(&s, a).props.find_group(rect).unwrap().sub("contents").unwrap().groups().map(|g| g.name.clone()).collect();
|
||||
assert_eq!(inner, ["Rectangle Path 1", "Fill 2", "Fill 1"]);
|
||||
// With only the layer selected, the layer is duplicated as before.
|
||||
s.execute("layer.select", json!({"layers": [a]})).unwrap();
|
||||
s.execute("edit.duplicate", json!({})).unwrap();
|
||||
assert_eq!(s.active_comp().unwrap().layers.len(), n + 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,6 +171,16 @@ fn ctrl_c_and_ctrl_v_copy_a_shape_group_into_another_shape_layer() {
|
|||
assert_eq!(contents_names(&h, a), ["Rectangle 1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_d_duplicates_the_selected_shape_group_in_its_layer() {
|
||||
let (mut h, a, _, rect) = two_shape_layers();
|
||||
click(&mut h, &format!("timeline.group.{rect}.name"));
|
||||
h.input_mut().events.push(Event::Key { key: Key::D, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND });
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.active_comp().unwrap().layers.len(), 2, "no new layer");
|
||||
assert_eq!(contents_names(&h, a), ["Rectangle 2", "Rectangle 1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolbar_stroke_edits_the_selected_shape_layer() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
Loading…
Reference in New Issue