M5.13: keyframe paste: several copied layers paste onto as many layers in order; Paste Reversed pastes like Paste

- Keys copied from several layers and pasted onto as many selected layers go first to first,
  second to second, as in After Effects; every target used to receive every copied property
  (the later ones overwriting the earlier).
- Edit ▸ Paste Reversed Keyframes mirrors the clipboard over its span and pastes it through
  `keys.paste`, so it now follows the same rules as Paste: the layer's time stretch, the
  selected target property, Hold-only properties, value conversion and roving keys (its own
  simplified paste ignored them and kept existing keys' easing on a time collision).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-05 08:02:16 +07:00
1 parent 9ea1f1325d
commit 43bf2906d3
3 files changed
+71 -44

No files matched your search

+9 -1
View File
@@ -73,11 +73,19 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_p(p, "path")?;
comp.layer(*targets.first()?)?.props.prop(path).map(|pr| pr.uid)
});
// Keys copied from several layers, pasted onto as many layers: the first copied layer's keys
// go to the first target and so on (else every target gets every copied property).
let mut sources: Vec<LayerId> = clip.iter().map(|c| c.layer).collect();
sources.dedup();
let paired = sources.len() > 1 && sources.len() == targets.len();
// (layer, prop uid, clip index)
let mut plan: Vec<(LayerId, Uid, usize)> = vec![];
for lid in &targets {
for (ti, lid) in targets.iter().enumerate() {
let Some(l) = comp.layer(*lid) else { continue };
for (ci, c) in clip.iter().enumerate() {
if paired && sources.get(ti) != Some(&c.layer) {
continue;
}
let Some(sample) = c.keys.first().map(|k| &k.value) else { continue };
let target = if clip.len() == 1 {
explicit
+29 -43
View File
@@ -299,46 +299,6 @@ fn copy_expression_only(s: &mut Session, _: &Value) -> Result<Value> {
}
/// Paste the keyframe clipboard at the CTI onto the selected layers (same property paths).
fn paste_keys(s: &mut Session, reversed: bool) -> Result<Value> {
let cid = s.active_comp_id().ok_or(EngineError::NoComp)?;
let ids = s.state.selected_layers.clone();
if ids.is_empty() {
return Err(EngineError::Other("select a layer to paste keyframes into".into()));
}
let clip = s.state.key_clipboard.clone();
let t = s.time();
let n = s.edit(if reversed { "Paste Reversed Keyframes" } else { "Paste Keyframes" }, None, |proj, st| {
let mut n = 0;
st.selected_keys.clear();
for lid in &ids {
let l = super::layer_mut(proj, cid, *lid)?;
let lt = l.layer_time(t);
for c in &clip {
let Some(pr) = l.props.prop_mut(&c.path) else { continue };
let span = c.keys.last().map(|k| k.time).unwrap_or(Tick::ZERO);
for k in &c.keys {
let mut k = k.clone();
if std::mem::discriminant(&k.value) != std::mem::discriminant(&pr.value) {
continue;
}
if reversed {
k.time = span - k.time;
std::mem::swap(&mut k.in_interp, &mut k.out_interp);
std::mem::swap(&mut k.in_ease, &mut k.out_ease);
std::mem::swap(&mut k.spatial_in, &mut k.spatial_out);
}
k.time += lt;
st.selected_keys.push(crate::KeyRef { layer: *lid, prop: pr.uid, time: k.time });
effectcraft_keyframe::set_key(&mut pr.keys, k);
n += 1;
}
}
}
Ok(n)
})?;
Ok(json!({"keys": n}))
}
fn paste_links(s: &mut Session, clip: LinkClip) -> Result<Value> {
let cid = s.active_comp_id().ok_or(EngineError::NoComp)?;
let ids = s.state.selected_layers.clone();
@@ -470,8 +430,34 @@ fn label(s: &mut Session, p: &Value) -> Result<Value> {
Ok(Value::Null)
}
fn paste_reversed(s: &mut Session, _: &Value) -> Result<Value> {
paste_keys(s, true)
/// Edit ▸ Paste Reversed Keyframes: the copied keys mirrored in time over their whole span (the
/// last key first, incoming and outgoing eases swapped), pasted like Edit ▸ Paste
/// (`keys.paste`: at the current time, into the selected or the same properties).
fn paste_reversed(s: &mut Session, p: &Value) -> Result<Value> {
let original = s.state.key_clipboard.clone();
let span = original.iter().flat_map(|c| c.keys.iter().map(|k| k.time)).max().unwrap_or(Tick::ZERO);
s.state.key_clipboard = original
.iter()
.map(|c| crate::KeyClip {
keys: c
.keys
.iter()
.rev()
.map(|k| {
let mut k = k.clone();
k.time = span - k.time;
std::mem::swap(&mut k.in_interp, &mut k.out_interp);
std::mem::swap(&mut k.in_ease, &mut k.out_ease);
std::mem::swap(&mut k.spatial_in, &mut k.spatial_out);
k
})
.collect(),
..c.clone()
})
.collect();
let r = s.execute("keys.paste", p.clone());
s.state.key_clipboard = original;
r
}
/// Lift (leave a gap) or extract (close the gap) the work area from the selected layers (all
@@ -667,7 +653,7 @@ pub fn specs() -> Vec<CommandSpec> {
),
cmd!("edit.copyExpressionOnly", "Copy Expression Only", ["Edit"], None, "{}", has_props_or_layers, copy_expression_only),
cmd!("edit.paste", "Paste", ["Edit"], Some("Cmd+V"), "{} (layers, keyframes at the CTI, or property links / expressions)", has_clip, paste),
cmd!("edit.pasteReversedKeyframes", "Paste Reversed Keyframes", ["Edit"], None, "{}", has_key_clip, paste_reversed),
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.splitLayer", "Split Layer", ["Edit"], Some("Cmd+Shift+D"), "{layers?}", has_layers, split),
+33
View File
@@ -981,3 +981,36 @@ fn select_equal_previous_and_following_keyframes() {
s.execute("keys.selectFollowing", json!({})).unwrap();
assert_eq!(times(&s), vec![1.0, 2.0, 3.0]);
}
#[test]
fn keys_from_several_layers_paste_in_order_and_paste_reversed_respects_stretch() {
let mut s = comp();
let a = solid(&mut s, "#ff0000");
let b = solid(&mut s, "#00ff00");
opacity_keys(&mut s, a, &[0.0, 1.0]);
s.execute("prop.addKey", json!({"layer": b, "path": "transform/opacity", "time": 0.0, "value": 77})).unwrap();
let c = solid(&mut s, "#0000ff");
let d = solid(&mut s, "#ffffff");
// Copy A's and B's Opacity keys, paste onto C and D: A → C, B → D (in order).
s.execute("keys.selectAll", json!({"layers": [a, b]})).unwrap();
s.execute("keys.copy", json!({})).unwrap();
s.execute("time.set", json!({"time": 2.0})).unwrap();
s.execute("layer.select", json!({"layers": [c, d]})).unwrap();
s.execute("keys.paste", json!({})).unwrap();
assert_eq!(key_times(&s, c), vec![2.0, 3.0], "A's two keys");
assert_eq!(key_times(&s, d), vec![2.0], "B's one key");
// Paste Reversed onto a layer stretched to 200 %: the reversed keys land 2 s apart there too.
let e = solid(&mut s, "#808080");
s.execute_checked("layer.timeStretch", json!({"layers": [e], "percent": 200})).unwrap();
s.execute("keys.selectAll", json!({"layers": [a]})).unwrap();
s.execute("keys.copy", json!({})).unwrap();
s.execute("time.set", json!({"time": 0.0})).unwrap();
s.execute("layer.select", json!({"layers": [e]})).unwrap();
s.execute("edit.pasteReversedKeyframes", json!({})).unwrap();
let pr = layer(&s, e).props.prop("transform/opacity").unwrap().clone();
let l = layer(&s, e);
let ct: Vec<f64> = pr.keys.iter().map(|k| l.comp_time(k.time).seconds()).collect();
let vals: Vec<f64> = pr.keys.iter().map(|k| k.value.as_f64()).collect();
assert!(ct.len() == 2 && (ct[1] - ct[0] - 1.0).abs() < 1e-6, "keys 1 s apart in comp time: {ct:?}");
assert_eq!(vals, vec![10.0, 0.0], "reversed");
}