Merge pull request #1711

This commit is contained in:
Brandon Thomas committed 2026-10-18 09:35:35 -04:00
commit 415e353dc0
3 files changed
+50 -4

No files matched your search

+5
View File
@@ -115,6 +115,11 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
if !items.is_empty() {
return super::prop_groups::duplicate_contents(s, &items);
}
// Text animators or their selectors selected → duplicate them inside their text layers.
let items = super::prop_groups::selected_animator_items(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| {
+20 -3
View File
@@ -100,6 +100,22 @@ pub(crate) fn selected_contents(s: &Session) -> Vec<(LayerId, Uid)> {
out
}
/// Text animators and their selectors selected on the active comp's text layers, top to bottom.
/// A selector inside a selected animator isn't listed, since it is copied with it.
pub(crate) fn selected_animator_items(s: &Session) -> Vec<(LayerId, Uid)> {
let mut out = vec![];
let Some(c) = s.active_comp().filter(|_| !s.state.selected_props.is_empty()) else { return out };
for l in c.layers.iter().filter(|l| matches!(l.source, LayerSource::Text)) {
if let Some(animators) = l.props.group("text/animators") {
let selected = |u: Uid| s.state.selected_props.contains(&(l.id, u));
let mut uids = vec![];
collect_selected(animators, &selected, &mut uids, 0);
out.extend(uids.into_iter().map(|u| (l.id, u)));
}
}
out
}
fn collect_selected(g: &PropGroup, selected: &dyn Fn(Uid) -> bool, out: &mut Vec<Uid>, depth: usize) {
if depth > super::shape_stroke::MAX_DEPTH {
return;
@@ -174,8 +190,9 @@ 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.
/// Edit ▸ Duplicate with shape items, text animators or selectors 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)?;
@@ -183,7 +200,7 @@ pub(crate) fn duplicate_contents(s: &mut Session, items: &[(LayerId, Uid)]) -> R
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 parent = layer_mut(proj, cid, *lid)?.props.parent_of_mut(*uid).ok_or_else(|| bad(c, "the group 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);
+25 -1
View File
@@ -2,7 +2,7 @@
//! undo/redo and serde round-trips).
use effectcraft_keyframe::Value as KV;
use effectcraft_project::{Node, Project, PropGroup};
use effectcraft_project::{LayerId, Node, Project, PropGroup};
use serde_json::json;
use crate::Session;
@@ -244,3 +244,27 @@ fn menus_have_text_animation_entries() {
assert!(s.is_enabled("layer.enablePerChar3D"));
assert!(!s.is_enabled("layer.addTextSelector"), "needs an animator");
}
/// Duplicate with a Range Selector selected under a text animator copies the selector, not the
/// whole text layer (#1665); a selected animator is copied the same way.
#[test]
fn duplicate_copies_selected_text_selector_not_the_layer() {
let (mut s, t) = session();
s.execute("layer.addTextAnimator", json!({"layer": t, "properties": ["tracking"]})).unwrap();
let anims = |s: &Session| s.active_comp().unwrap().layer(LayerId(t)).unwrap().props.group("text/animators").unwrap().clone();
let selectors = |s: &Session| -> Vec<u64> { anims(s).groups().next().unwrap().sub("selectors").unwrap().groups().map(|g| g.uid).collect() };
let sel = selectors(&s)[0];
s.execute("prop.select", json!({"layer": t, "prop": sel, "selectKeys": false})).unwrap();
s.execute("edit.duplicate", json!({})).unwrap();
assert_eq!(s.active_comp().unwrap().layers.len(), 1, "no second text layer");
assert_eq!(selectors(&s).len(), 2, "the selector is copied");
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(selectors(&s), vec![sel]);
let anim = anims(&s).groups().next().unwrap().uid;
s.execute("prop.select", json!({"layer": t, "prop": anim, "selectKeys": false})).unwrap();
s.execute("edit.duplicate", json!({})).unwrap();
assert_eq!(s.active_comp().unwrap().layers.len(), 1);
assert_eq!(anims(&s).groups().count(), 2, "the animator is copied");
}