From a69c7d7cd21c4775dcc3c9cf59db3d484898637a Mon Sep 17 00:00:00 2001 From: redonion23 <199118131+redonion23@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:52:44 +0200 Subject: [PATCH] Timeline: Reset keeps an animated property's keyframes and keys the default (#1078) prop.reset cleared every keyframe before setting the default value. As in After Effects (and as effect.reset already does for effect parameters), an animated property now keeps its keyframes and gets the default as a key at the current time; a static property just takes the default. The expression is still removed. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/commands/prop.rs | 9 +++++---- crates/engine/src/tests_timeline.rs | 31 +++++++++++++++++++++++++++-- docs/agents.md | 5 +++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/commands/prop.rs b/crates/engine/src/commands/prop.rs index 64c166fd..7ac5f788 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) }) @@ -1207,7 +1208,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 009af9e5..531ddc14 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 01738edd..ffea88de 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -566,8 +566,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`).