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 <noreply@anthropic.com>
This commit is contained in:
redonion23
2026-10-12 23:52:44 +02:00
co-authored by Claude Opus 5.5
parent 470ea7a3af
commit a69c7d7cd2
3 changed files with 37 additions and 8 deletions
+5 -4
View File
@@ -631,11 +631,12 @@ fn reset(s: &mut Session, p: &Value) -> Result<Value> {
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<CommandSpec> {
"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
),
+29 -2
View File
@@ -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]
+3 -2
View File
@@ -566,8 +566,9 @@ Extension points and the bundled extensions: [plugins.md](plugins.md).
(`effectControls.prop.<uid>.value[.<d>]`), 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.<uid>.menu.<entry>`): 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`).