diff --git a/crates/engine/src/commands/anim.rs b/crates/engine/src/commands/anim.rs index d4dec045..0747290e 100644 --- a/crates/engine/src/commands/anim.rs +++ b/crates/engine/src/commands/anim.rs @@ -73,11 +73,19 @@ fn paste(s: &mut Session, p: &Value) -> Result { 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 = 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 diff --git a/crates/engine/src/commands/edit.rs b/crates/engine/src/commands/edit.rs index bcc6a910..b021e4e6 100644 --- a/crates/engine/src/commands/edit.rs +++ b/crates/engine/src/commands/edit.rs @@ -299,46 +299,6 @@ fn copy_expression_only(s: &mut Session, _: &Value) -> Result { } /// Paste the keyframe clipboard at the CTI onto the selected layers (same property paths). -fn paste_keys(s: &mut Session, reversed: bool) -> Result { - 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 { 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 { Ok(Value::Null) } -fn paste_reversed(s: &mut Session, _: &Value) -> Result { - 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 { + 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 { ), 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), diff --git a/crates/engine/src/tests_menu_cmds.rs b/crates/engine/src/tests_menu_cmds.rs index 71694f69..abf79d70 100644 --- a/crates/engine/src/tests_menu_cmds.rs +++ b/crates/engine/src/tests_menu_cmds.rs @@ -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 = pr.keys.iter().map(|k| l.comp_time(k.time).seconds()).collect(); + let vals: Vec = 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"); +}