diff --git a/crates/engine/src/commands/prop.rs b/crates/engine/src/commands/prop.rs index 5248949a..8246d441 100644 --- a/crates/engine/src/commands/prop.rs +++ b/crates/engine/src/commands/prop.rs @@ -631,11 +631,12 @@ fn reset(s: &mut Session, p: &Value) -> Result { let (cid, lid, uid) = prop_ref(s, p, "prop.reset")?; let def = p.get("default").cloned(); let own = default_value(s, cid, lid, uid); - with_prop(s, "Reset Property", None, cid, lid, uid, |pr, _| { - pr.keys.clear(); + with_prop(s, "Reset Property", None, cid, lid, uid, |pr, t| { pr.expr = None; + // An animated property keeps its keyframes and gets the default as a key at the + // current time, as in After Effects (#1078). if let Some(d) = def.as_ref().and_then(|d| pr.value.coerce_json(d)).or(own) { - pr.value = d; + pr.set_value_at(t, d); } Ok(Value::Null) }) @@ -1238,7 +1239,7 @@ pub fn specs() -> Vec { "Reset Property", [], None, - "{layer?, path|prop, default?} → removes keyframes and expression; the value becomes `default`, else the property's own (an effect parameter's, a Transform property's)", + "{layer?, path|prop, default?} → removes the expression; the value becomes `default`, else the property's own (an effect parameter's, a Transform property's), as a keyframe at the current time when the property is animated", has_layers, reset ), diff --git a/crates/engine/src/tests_timeline.rs b/crates/engine/src/tests_timeline.rs index 9014faeb..938056dd 100644 --- a/crates/engine/src/tests_timeline.rs +++ b/crates/engine/src/tests_timeline.rs @@ -597,8 +597,10 @@ fn reset_gives_a_property_its_default() { assert_eq!(v(&s, "transform/anchor"), KV::Vec3([50.0, 50.0, 0.0]), "the solid's centre"); assert_eq!(v(&s, "transform/position"), KV::Vec3([200.0, 150.0, 0.0]), "the comp's centre"); assert_eq!(v(&s, "transform/scale"), KV::Vec3([100.0; 3])); - assert_eq!(v(&s, "transform/rotation"), KV::Scalar(0.0)); - assert!(!prop(&s, l, "transform/rotation").is_animated()); + // Animated: the default becomes the key at the current time (0 s), the 1 s key stays (#1078). + let rot = prop(&s, l, "transform/rotation"); + assert_eq!(rot.keys.len(), 2); + assert_eq!(rot.value_at(effectcraft_time::Tick::ZERO), KV::Scalar(0.0)); assert_eq!(v(&s, "transform/opacity"), KV::Scalar(100.0)); assert!(prop(&s, l, "transform/opacity").expr.is_none()); assert_eq!(v(&s, "effects/#1/blurriness"), blur); @@ -607,6 +609,31 @@ fn reset_gives_a_property_its_default() { assert_eq!(v(&s, "transform/opacity"), KV::Scalar(30.0)); } +/// #1078: resetting an animated property keeps its keyframes and keys the default at the +/// current time, as in After Effects; undo brings the old keys back. +#[test] +fn reset_keys_the_default_on_an_animated_property() { + let (mut s, l) = setup(); + animate(&mut s, l, "transform/scale", &[(0.0, json!([50, 50, 100])), (2.0, json!([150, 150, 100]))]); + s.execute("time.set", json!({"time": 1})).unwrap(); + s.execute("prop.reset", json!({"layer": l, "path": "transform/scale"})).unwrap(); + let pr = prop(&s, l, "transform/scale"); + let at = |sec: f64| pr.value_at(effectcraft_time::Tick::from_seconds_f64(sec)); + assert_eq!(pr.keys.len(), 3, "the 0 s and 2 s keys stay, one is added at 1 s"); + assert_eq!(at(0.0), KV::Vec3([50.0, 50.0, 100.0])); + assert_eq!(at(1.0), KV::Vec3([100.0; 3])); + assert_eq!(at(2.0), KV::Vec3([150.0, 150.0, 100.0])); + // On a key, the key takes the default. + s.execute("time.set", json!({"time": 2})).unwrap(); + s.execute("prop.reset", json!({"layer": l, "path": "transform/scale"})).unwrap(); + let pr = prop(&s, l, "transform/scale"); + assert_eq!(pr.keys.len(), 3); + assert_eq!(pr.value_at(effectcraft_time::Tick::from_seconds_f64(2.0)), KV::Vec3([100.0; 3])); + s.execute("edit.undo", json!({})).unwrap(); + s.execute("edit.undo", json!({})).unwrap(); + assert_eq!(prop(&s, l, "transform/scale").keys.len(), 2); +} + /// #363: dropped on one of the target's values the pick whip picks that dimension /// (`position[0]`), as in After Effects; a multi-dimension property follows it on every axis. #[test] diff --git a/docs/agents.md b/docs/agents.md index 3b1116a7..d36a766a 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -573,8 +573,9 @@ Extension points and the bundled extensions: [plugins.md](plugins.md). (`effectControls.prop..value[.]`), it runs `prop.pickWhip`: the reference replaces the property's expression, or is added as one, and the expression is enabled. * A property's menu (right-click its name; entries `timeline.prop..menu.`): Reset - (`prop.reset`: no keyframes or expression, and the property's default value, an effect - parameter's or a Transform property's), Edit Value… (`prop.set {layer, prop}` without `value` + (`prop.reset`: no expression, and the property's default value, an effect parameter's or a + Transform property's; an animated property keeps its keyframes and gets the default as a + keyframe at the current time), Edit Value… (`prop.set {layer, prop}` without `value` opens the value dialog), Separate Dimensions (Position: `prop.separateDimensions {layer, value}`), Add / Remove Expression (`prop.setExpression {layer, prop}`) and Add Property to Essential Graphics (`essential.addProperty`).