diff --git a/crates/engine/src/keyframe_text.rs b/crates/engine/src/keyframe_text.rs index 21727bc1c..0c090e5c4 100644 --- a/crates/engine/src/keyframe_text.rs +++ b/crates/engine/src/keyframe_text.rs @@ -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 = 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) diff --git a/crates/engine/src/tests_keyframe_text.rs b/crates/engine/src/tests_keyframe_text.rs index 968482f60..ef9ad509c 100644 --- a/crates/engine/src/tests_keyframe_text.rs +++ b/crates/engine/src/tests_keyframe_text.rs @@ -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] diff --git a/crates/ui-egui/tests/ui_keyframes.rs b/crates/ui-egui/tests/ui_keyframes.rs index 2eebb068f..b137a06fc 100644 --- a/crates/ui-egui/tests/ui_keyframes.rs +++ b/crates/ui-egui/tests/ui_keyframes.rs @@ -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));