diff --git a/crates/engine/src/commands/animation.rs b/crates/engine/src/commands/animation.rs index c700123d..a9425683 100644 --- a/crates/engine/src/commands/animation.rs +++ b/crates/engine/src/commands/animation.rs @@ -349,6 +349,25 @@ fn changed_from(g: &PropGroup, default: &PropGroup, out: &mut Vec) { } } +/// What of effect group `g` differs from `new`, a new instance of the effect: parameters with +/// another value, and in full the groups a new instance lacks (puppet meshes and pins, paint +/// strokes: what the user made). +fn effect_changes(g: &PropGroup, new: &PropGroup, out: &mut Vec) { + for c in &g.children { + match c { + Node::Prop(pr) => { + if new.get(&pr.match_id).is_some_and(|d| d.value != pr.value) && !out.contains(&pr.uid) { + out.push(pr.uid); + } + } + Node::Group(sg) => match new.sub(&sg.match_id) { + Some(d) => effect_changes(sg, d, out), + None => out.push(sg.uid), + }, + } + } +} + fn reveal(s: &mut Session, p: &Value) -> Result { let kind = str_p(p, "kind").unwrap_or("keyframes").to_string(); let cid = super::comp_id(s, p)?; @@ -389,12 +408,30 @@ fn reveal(s: &mut Session, p: &Value) -> Result { found.push(pr.uid); } }); - // Modified: effects and masks count as modifications in full (AE reveals them). - if kind == "modified" && matches!(top.match_id.as_str(), "effects" | "masks") { + // Modified: masks count as modifications in full (AE reveals them). + if kind == "modified" && top.match_id == "masks" { for g in top.groups() { found.push(g.uid); } } + // Modified: an effect's parameters that differ from a new instance's. + if kind == "modified" && top.match_id == "effects" { + let size = effectcraft_render::effect_bounds(&s.project, comp, l).0; + for g in top.groups() { + let spec = match &g.kind { + GroupKind::Effect { effect } => effectcraft_effects::find(effect), + _ => None, + }; + // A missing plug-in's defaults aren't known: the effect shows in full. + let Some(spec) = spec else { + found.push(g.uid); + continue; + }; + let mut next = 0; + let new = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), "", size); + effect_changes(g, &new, &mut found); + } + } if kind == "modified" && top.match_id == "text" { for g in top.group("animators").into_iter().flat_map(|a| a.groups()) { found.push(g.uid); diff --git a/crates/engine/src/tests_timeline.rs b/crates/engine/src/tests_timeline.rs index 009af9e5..665e18be 100644 --- a/crates/engine/src/tests_timeline.rs +++ b/crates/engine/src/tests_timeline.rs @@ -814,6 +814,27 @@ fn modified_reveal_takes_in_options_and_layer_styles() { assert_eq!(modified(&mut s, light), ["/lightOptions/intensity"]); } +/// UU shows the parameters of an effect that differ from a new instance's, not the whole effect: +/// a just-applied effect shows nothing, and a reset parameter goes again. +#[test] +fn modified_reveal_shows_changed_effect_parameters_only() { + let (mut s, l) = setup(); + s.execute("layer.select", json!({"layers": [l]})).unwrap(); + s.execute("effect.apply", json!({"effect": "ec.blur.gaussian"})).unwrap(); + let modified = |s: &mut Session| -> Vec { + let r = s.execute("anim.reveal", json!({"kind": "modified"})).unwrap(); + r["props"].as_array().unwrap().iter().filter_map(|p| p["prop"].as_u64()).map(|u| path_of(s, l, u)).collect() + }; + assert!(modified(&mut s).is_empty(), "{:?}", modified(&mut s)); + s.execute("prop.set", json!({"layer": l, "path": "effects/#1/blurriness", "value": 12})).unwrap(); + assert_eq!(modified(&mut s), ["/effects/ec.blur.gaussian/blurriness"]); + // Keyframed at its default value: animated, so still shown. + s.execute("prop.set", json!({"layer": l, "path": "effects/#1/blurriness", "value": 0})).unwrap(); + assert!(modified(&mut s).is_empty(), "{:?}", modified(&mut s)); + s.execute("prop.toggleAnimation", json!({"layer": l, "path": "effects/#1/blurriness"})).unwrap(); + assert_eq!(modified(&mut s), ["/effects/ec.blur.gaussian/blurriness"]); +} + /// 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 72f10429..8cb05101 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -3980,6 +3980,8 @@ mod tests { let n_keys = app.ui.timeline.reveal_props.len(); assert_eq!(app.ui.timeline.reveal, vec!["props"]); assert!(n_keys >= 1); + let bx = app.session.state.selected_layers[0].0; + app.session.execute("prop.set", json!({"layer": bx, "path": "effects/#1/blurriness", "value": 12})).unwrap(); crate::menus::invoke(&mut app, &ctx, "anim.reveal", json!({"kind": "keyframes"})).unwrap(); assert!(app.ui.timeline.reveal_props.len() > n_keys, "UU reveals modified properties too"); } @@ -4007,9 +4009,14 @@ mod tests { let at = |n: &str| rows.iter().position(|r| r == n).unwrap(); assert_eq!(rows[at("Plate") + 1].trim(), "Scale", "{rows:?}"); assert_eq!(rows[at("Box") + 1].trim(), "Position", "{rows:?}"); - // UU (modified), then U later: Box shows its keyframed properties only again. + // UU (modified): the effect's changed parameter only, under the effect twirled open. + // Then U later: Box shows its keyframed properties only again. + app.session.execute("prop.set", json!({"layer": bx, "path": "effects/#1/blurriness", "value": 12})).unwrap(); crate::menus::invoke(&mut app, &ctx, "anim.reveal", json!({"kind": "keyframes"})).unwrap(); - assert!(labels(&app).iter().any(|r| r.trim() == ">Gaussian Blur"), "UU shows the effect"); + let rows = labels(&app); + let fx = rows.iter().position(|r| r.trim().ends_with("Gaussian Blur")).unwrap_or_else(|| panic!("UU shows the effect: {rows:?}")); + assert_eq!(rows[fx + 1].trim(), "Blurriness", "{rows:?}"); + assert!(!rows.iter().any(|r| r.trim() == "Blur Dimensions"), "unchanged parameters stay hidden: {rows:?}"); app.last_reveal = Some(("keyframes".into(), -10.0)); crate::menus::invoke(&mut app, &ctx, "anim.reveal", json!({"kind": "keyframes"})).unwrap(); app.last_reveal = Some(("keyframes".into(), -10.0));