diff --git a/crates/engine/src/commands/animation.rs b/crates/engine/src/commands/animation.rs index a1060c85..fb2d80c9 100644 --- a/crates/engine/src/commands/animation.rs +++ b/crates/engine/src/commands/animation.rs @@ -312,6 +312,18 @@ fn reveal(s: &mut Session, p: &Value) -> Result { let (w, h) = effectcraft_render::source_size(&s.project, l); let anchor = [w as f64 / 2.0, h as f64 / 2.0]; let center = [comp.width as f64 / 2.0, comp.height as f64 / 2.0]; + // Properties at the top of the layer's tree (Time Remap): enabled time remapping is a + // modification in itself. + for pr in l.props.props() { + let hit = match kind.as_str() { + "keyframes" => pr.is_animated(), + "animation" => pr.is_animated() || pr.has_expression(), + _ => pr.match_id == "timeRemap" || modified(pr, "", anchor, center), + }; + if hit { + out.push(json!({"layer": lid.0, "prop": pr.uid})); + } + } for top in l.props.groups() { let hit = |pr: &Property| match kind.as_str() { "keyframes" => pr.is_animated(), diff --git a/crates/engine/src/tests_timeline.rs b/crates/engine/src/tests_timeline.rs index 82e2f68e..bbd9e453 100644 --- a/crates/engine/src/tests_timeline.rs +++ b/crates/engine/src/tests_timeline.rs @@ -712,6 +712,25 @@ fn deleting_time_remap_turns_time_remapping_off() { assert_eq!(prop(&s, l, "timeRemap").keys.len(), 1); } +/// U (keyframes), the animation reveal and UU (modified) take in Time Remap, which sits at the +/// top of the layer's tree rather than in a group. +#[test] +fn reveal_shortcuts_take_in_time_remap() { + let (mut s, l) = precomp_setup(); + let revealed = |s: &mut Session, kind: &str| -> Vec { + let r = s.execute("anim.reveal", json!({"kind": kind})).unwrap(); + r["props"].as_array().unwrap().iter().filter(|p| p["layer"] == l).filter_map(|p| p["prop"].as_u64()).collect() + }; + for kind in ["keyframes", "animation", "modified"] { + assert!(revealed(&mut s, kind).is_empty(), "{kind}: nothing to reveal before remapping"); + } + s.execute("layer.enableTimeRemap", json!({})).unwrap(); + let uid = prop(&s, l, "timeRemap").uid; + for kind in ["keyframes", "animation", "modified"] { + assert_eq!(revealed(&mut s, kind), vec![uid], "{kind}"); + } +} + /// Enable Time Remapping on a layer extended past its source's end (its last frame frozen) puts /// the end key at the source's end, not at the extended out point (#290). #[test] diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 7280d858..a008b5be 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -3907,6 +3907,28 @@ mod tests { assert!(rows.iter().any(|r| r.trim() == ">Transform"), "{rows:?}"); } + /// U and UU show Time Remap on a layer with time remapping enabled. + #[test] + fn u_and_uu_reveal_time_remap() { + let mut s = effectcraft_engine::Session::default(); + let inner = s.execute("comp.new", json!({"name": "Inner", "width": 32, "height": 32, "duration": 2})).unwrap()["comp"].as_u64().unwrap(); + s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64, "duration": 4})).unwrap(); + let nested = s.execute("layer.addItem", json!({"item": inner})).unwrap()["layer"].as_u64().unwrap(); + let mut app = EffectcraftApp::new(s); + let ctx = egui::Context::default(); + crate::menus::invoke(&mut app, &ctx, "layer.enableTimeRemap", json!({"layers": [nested]})).unwrap(); + app.session.execute("layer.select", json!({"layers": [nested]})).unwrap(); + for uu in [false, true] { + app.ui.timeline.layer_reveal.clear(); + app.last_reveal = uu.then(|| ("keyframes".into(), f64::MAX)); + crate::menus::invoke(&mut app, &ctx, "anim.reveal", json!({"kind": "keyframes"})).unwrap(); + assert_eq!(app.ui.timeline.layer_reveal.get(&nested), Some(&vec!["props".to_string()]), "UU: {uu}"); + let rows = labels(&app); + let at = rows.iter().position(|r| r == "Inner").unwrap(); + assert_eq!(rows[at + 1].trim(), "Time Remap", "UU: {uu}: {rows:?}"); + } + } + /// Enabling time remapping twirls the layer open on Time Remap (#157); disabling it reveals /// nothing. #[test]