diff --git a/crates/automation/src/tests.rs b/crates/automation/src/tests.rs index 4cea8b819..6624b4859 100644 --- a/crates/automation/src/tests.rs +++ b/crates/automation/src/tests.rs @@ -153,6 +153,20 @@ fn headless_workflow() { let mid = call_json(&mut s, "get_property", json!({"layer": l, "path": "transform/position", "time": 1.0})); assert!((mid["value"][0].as_f64().unwrap() - 160.0).abs() < 1.0, "{mid}"); + // Comp-time keys on an offset layer (#257): the key lands where the Timeline shows it. + call_json(&mut s, "execute_command", json!({"command": "layer.timing", "params": {"layer": l, "start": 1.0}})); + let p = call_json( + &mut s, + "add_keyframe", + json!({"layer": l, "path": "transform/scale", "keys": [{"time": 1.5, "value": [50, 50]}, {"time": 2.5, "value": [100, 100]}], "timeBase": "comp", "interpolation": "hold"}), + ); + let times: Vec = p["keys"].as_array().unwrap().iter().map(|k| k["time"].as_f64().unwrap()).collect(); + assert!((times[0] - 0.5).abs() < 1e-6 && (times[1] - 1.5).abs() < 1e-6, "layer-time keys: {times:?}"); + assert_eq!(p["keys"][0]["out"], "Hold", "the comp-time keys were selected for the interpolation"); + call_json(&mut s, "set_property", json!({"layer": l, "path": "transform/scale", "value": [75, 75], "time": 3.0, "timeBase": "comp"})); + let p = call_json(&mut s, "get_property", json!({"layer": l, "path": "transform/scale"})); + assert!(p["keys"].as_array().unwrap().iter().any(|k| (k["time"].as_f64().unwrap() - 2.0).abs() < 1e-6), "{p}"); + // Expression via set_property. let p = call_json(&mut s, "set_property", json!({"layer": l, "path": "transform/rotation", "expression": "time * 90"})); assert_eq!(p["expression"], "time * 90"); diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index bf24afbc3..12ea31675 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -319,6 +319,9 @@ fn set_property(b: &mut Backend, a: &Value) -> Result { match time { Some(t) => { p["time"] = t.clone(); + if let Some(tb) = get(a, "timeBase") { + p["timeBase"] = tb.clone(); + } b.exec("prop.addKey", p)?; } None => { @@ -351,6 +354,9 @@ fn add_keyframe(b: &mut Backend, a: &Value) -> Result { if let Some(v) = get(k, "value") { p["value"] = v.clone(); } + if let Some(tb) = get(a, "timeBase") { + p["timeBase"] = tb.clone(); + } b.exec("prop.addKey", p)?; } let info = b.exec("prop.get", obj(&base))?; @@ -360,7 +366,11 @@ fn add_keyframe(b: &mut Backend, a: &Value) -> Result { b.exec("comp.open", json!({"comp": c}))?; } let sel: Vec = keys.iter().map(|k| json!({"layer": info["layer"], "prop": info["uid"], "time": k["time"]})).collect(); - b.exec("keys.select", json!({"keys": sel}))?; + let mut select = json!({"keys": sel}); + if let Some(tb) = get(a, "timeBase") { + select["timeBase"] = tb.clone(); + } + b.exec("keys.select", select)?; match interp.to_ascii_lowercase().as_str() { "easyease" | "ease" => b.exec("keys.easyEase", json!({}))?, "easyeasein" => b.exec("keys.easyEase", json!({"which": "in"}))?, @@ -628,6 +638,7 @@ static TOOLS: &[ToolDef] = &[ json!({ "layer": layer_s(), "path": path_s(), "value": value_s(), "comp": comp_s(), "time": {"type": "number", "description": "Keyframe time in seconds (layer time; equals comp time unless the layer is offset/stretched)."}, + "timeBase": {"type": "string", "enum": ["layer", "comp"], "description": "What `time` counts: layer time (the default) or comp time, as the Timeline shows it."}, "expression": {"type": "string", "description": "Expression text; empty string removes the expression."} }), &["layer", "path"], @@ -644,6 +655,7 @@ static TOOLS: &[ToolDef] = &[ json!({ "layer": layer_s(), "path": path_s(), "comp": comp_s(), "time": {"type": "number", "description": "Key time in seconds (layer time)."}, + "timeBase": {"type": "string", "enum": ["layer", "comp"], "description": "What `time` counts: layer time (the default) or comp time, as the Timeline shows it."}, "value": value_s(), "keys": {"type": "array", "items": {"type": "object", "properties": {"time": {"type": "number"}, "value": value_s()}, "required": ["time"]}, "description": "Several keys at once."}, "interpolation": {"type": "string", "enum": ["linear", "bezier", "hold", "easyEase", "easyEaseIn", "easyEaseOut"]}