Clipboard: keyframe text in current After Effects' layout, with a tab after each column (#1908)

This commit is contained in:
tylerjenningsw
2026-10-20 09:42:08 -04:00
parent 8ace9aac7f
commit 30dc57f790
3 changed files with 17 additions and 16 deletions
+5 -5
View File
@@ -55,7 +55,7 @@ pub(crate) fn text(project: &Project, comp: &Comp, keys: &[KeyRef], statics: &[(
_ if *id == "orientation" => vec![format!("X {unit}")],
_ => ["X", "Y", "Z"].iter().take(dims).map(|axis| format!("{axis} {unit}")).collect(),
};
blocks.push_str(&format!("Transform\t{}\n\tFrame\t{}\n", pr.name, columns.join("\t")));
blocks.push_str(&format!("Transform\t{}\n\tFrame\t{}\t\n", pr.name, columns.join("\t")));
for (frame, mut values) in rows {
// A 2D layer ignores the Z it may keep (from pasted 3D keys): write the one it shows.
if !layer.is_3d()
@@ -64,7 +64,7 @@ pub(crate) fn text(project: &Project, comp: &Comp, keys: &[KeyRef], statics: &[(
*z = if *id == "scale" { 100.0 } else { 0.0 };
}
let values: Vec<String> = values.into_iter().map(num).collect();
blocks.push_str(&format!("\t{frame}\t{}\n", values.join("\t")));
blocks.push_str(&format!("\t{frame}\t{}\t\n", values.join("\t")));
}
blocks.push('\n');
}
@@ -86,10 +86,10 @@ pub(crate) fn text(project: &Project, comp: &Comp, keys: &[KeyRef], statics: &[(
},
_ => comp.pixel_aspect,
};
// After Effects starts the layer's properties with a `Layer` line; parsers such as
// ae-keyframes reject properties without one.
// The layout of current After Effects: a `Layer` line before the layer's properties (parsers
// such as ae-keyframes reject properties without one), and a tab after every column.
Some(format!(
"Adobe After Effects 8.0 Keyframe Data\n\n\tUnits Per Second\t{}\n\tSource Width\t{w}\n\tSource Height\t{h}\n\tSource Pixel Aspect Ratio\t{}\n\tComp Pixel Aspect Ratio\t{}\n\nLayer\n{blocks}End of Keyframe Data\n",
"Adobe After Effects 9.0 Keyframe Data\n\n\tUnits Per Second\t{}\n\tSource Width\t{w}\n\tSource Height\t{h}\n\tSource Pixel Aspect Ratio\t{}\n\tComp Pixel Aspect Ratio\t{}\n\nLayer\n{blocks}\nEnd of Keyframe Data\n",
num(fps),
num(par),
num(comp.pixel_aspect)
+10 -9
View File
@@ -50,10 +50,11 @@ fn copied_keys_are_after_effects_text_in_comp_frames() {
assert_eq!(
s.state.keyframe_text.as_deref(),
Some(
"Adobe After Effects 8.0 Keyframe Data\n\n\tUnits Per Second\t23.976\n\tSource Width\t200\n\tSource Height\t100\n\tSource Pixel Aspect Ratio\t1\n\tComp Pixel Aspect Ratio\t1\n\n\
"Adobe After Effects 9.0 Keyframe Data\n\n\tUnits Per Second\t23.976\n\tSource Width\t200\n\tSource Height\t100\n\tSource Pixel Aspect Ratio\t1\n\tComp Pixel Aspect Ratio\t1\n\n\
Layer\n\
Transform\tPosition\n\tFrame\tX pixels\tY pixels\tZ pixels\n\t12\t960\t540\t0\n\t19\t1100\t500\t0\n\n\
Transform\tRotation\n\tFrame\tdegrees\n\t15\t-45\n\n\
Transform\tPosition\n\tFrame\tX pixels\tY pixels\tZ pixels\t\n\t12\t960\t540\t0\t\n\t19\t1100\t500\t0\t\n\n\
Transform\tRotation\n\tFrame\tdegrees\t\n\t15\t-45\t\n\n\
\n\
End of Keyframe Data\n"
)
);
@@ -72,7 +73,7 @@ fn a_stretched_layer_writes_the_frames_its_keys_show_at() {
s.execute("prop.select", json!({"layer": l.0, "path": "transform/opacity"})).unwrap();
s.execute("edit.copy", json!({})).unwrap();
let text = s.state.keyframe_text.clone().unwrap();
assert!(text.contains("Transform\tOpacity\n\tFrame\tpercent\n\t0\t0\n\t12\t100\n\n"), "{text}");
assert!(text.contains("Transform\tOpacity\n\tFrame\tpercent\t\n\t0\t0\t\n\t12\t100\t\n\n"), "{text}");
}
#[test]
@@ -82,7 +83,7 @@ fn copied_static_values_have_no_frame() {
s.execute("prop.select", json!({"layer": l.0, "path": "transform/position"})).unwrap();
s.execute("edit.copy", json!({})).unwrap();
let text = s.state.keyframe_text.clone().unwrap();
assert!(text.contains("Transform\tPosition\n\tFrame\tX pixels\tY pixels\tZ pixels\n\t\t100\t200\t0\n\n"), "{text}");
assert!(text.contains("Transform\tPosition\n\tFrame\tX pixels\tY pixels\tZ pixels\t\n\t\t100\t200\t0\t\n\n"), "{text}");
assert_eq!(s.state.value_clipboard.len(), 1, "the internal clipboard keeps the value");
}
@@ -98,7 +99,7 @@ fn a_copied_transform_group_lists_what_the_layer_shows() {
let text = s.state.keyframe_text.clone().unwrap();
let names: Vec<&str> = text.lines().filter_map(|line| line.strip_prefix("Transform\t")).collect();
assert_eq!(names, ["Anchor Point", "Position", "Scale", "Rotation", "Opacity"], "a 2D layer has no Orientation or X / Y Rotation");
assert!(text.contains("\tFrame\tX percent\tY percent\tZ percent\n\t0\t50\t50\t100\n\t24\t150\t150\t100\n"), "{text}");
assert!(text.contains("\tFrame\tX percent\tY percent\tZ percent\t\n\t0\t50\t50\t100\t\n\t24\t150\t150\t100\t\n"), "{text}");
// 3D: Orientation and X / Y Rotation. Z Rotation stays Rotation, and Orientation's three
// values sit under one label, as After Effects writes them.
edit_layer(&mut s, l, |l| l.switches.three_d = true);
@@ -107,7 +108,7 @@ fn a_copied_transform_group_lists_what_the_layer_shows() {
let text = s.state.keyframe_text.clone().unwrap();
let names: Vec<&str> = text.lines().filter_map(|line| line.strip_prefix("Transform\t")).collect();
assert_eq!(names, ["Anchor Point", "Position", "Scale", "Orientation", "X Rotation", "Y Rotation", "Rotation", "Opacity"]);
assert!(text.contains("Transform\tOrientation\n\tFrame\tX degrees\n\t\t0\t0\t0\n"), "{text}");
assert!(text.contains("Transform\tOrientation\n\tFrame\tX degrees\t\n\t\t0\t0\t0\t\n"), "{text}");
}
fn copy_transform(s: &mut Session, lid: LayerId) -> String {
@@ -155,8 +156,8 @@ fn a_2d_layer_writes_the_z_it_shows() {
s.state.selected_keys = ["transform/position", "transform/scale"].map(|p| KeyRef { layer: l, prop: uid(&s, l, p), time: Tick::ZERO }).to_vec();
s.execute("keys.copy", json!({})).unwrap();
let text = s.state.keyframe_text.clone().unwrap();
assert!(text.contains("Transform\tPosition\n\tFrame\tX pixels\tY pixels\tZ pixels\n\t0\t100\t200\t0\n"), "{text}");
assert!(text.contains("Transform\tScale\n\tFrame\tX percent\tY percent\tZ percent\n\t0\t50\t60\t100\n"), "{text}");
assert!(text.contains("Transform\tPosition\n\tFrame\tX pixels\tY pixels\tZ pixels\t\n\t0\t100\t200\t0\t\n"), "{text}");
assert!(text.contains("Transform\tScale\n\tFrame\tX percent\tY percent\tZ percent\t\n\t0\t50\t60\t100\t\n"), "{text}");
}
#[test]
+2 -2
View File
@@ -108,8 +108,8 @@ fn ctrl_c_and_ctrl_v_copy_and_paste_keys_at_the_current_time() {
// The system clipboard has the key as After Effects keyframe text (for other applications),
// so it isn't empty and Ctrl+V sends a paste event.
let [text] = copied.as_slice() else { panic!("one copy: {copied:?}") };
assert!(text.starts_with("Adobe After Effects 8.0 Keyframe Data\n"), "{text}");
assert!(text.contains("Transform\tOpacity\n\tFrame\tpercent\n\t30\t100\n"), "{text}");
assert!(text.starts_with("Adobe After Effects 9.0 Keyframe Data\n"), "{text}");
assert!(text.contains("Transform\tOpacity\n\tFrame\tpercent\t\n\t30\t100\t\n"), "{text}");
assert!(h.state().session.state.clip_is_keys);
// Move to 3 s, Ctrl+V: the key lands there.
h.state_mut().session.set_time(effectcraft_engine::time::Tick::from_seconds_f64(3.0));