mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
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:
1 parent
9ea1f1325d
commit
43bf2906d3
3 files changed
+71
-44
No files matched your search
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user