mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 02:27:00 -06:00
150 lines
8.5 KiB
Rust
150 lines
8.5 KiB
Rust
//! Agent keyframe times on offset, stretched and reversed layers (#257).
|
|
|
|
use effectcraft_project::LayerId;
|
|
use effectcraft_time::Tick;
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::{Session, tests_timeline::prop};
|
|
|
|
fn offset_layer(start: f64, stretch: f64) -> (Session, u64) {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "Offset", "duration": 20, "frameRate": 30})).unwrap();
|
|
let layer = s.execute("layer.newSolid", json!({"name": "A", "color": "#ffffff"})).unwrap()["layer"].as_u64().unwrap();
|
|
s.execute("layer.timing", json!({"layer": layer, "start": start, "in": 10.5, "out": 20})).unwrap();
|
|
let cid = s.active_comp_id().unwrap();
|
|
std::sync::Arc::make_mut(&mut s.project).comp_mut(cid).unwrap().layer_mut(LayerId(layer)).unwrap().stretch = stretch;
|
|
(s, layer)
|
|
}
|
|
|
|
fn assert_seconds(value: &Value, seconds: f64) {
|
|
assert!((value.as_f64().unwrap() - seconds).abs() < 1e-8, "{value} != {seconds}");
|
|
}
|
|
|
|
#[test]
|
|
fn comp_key_times_on_an_offset_layer_read_at_the_intended_time_and_undo() {
|
|
let (mut s, layer) = offset_layer(10.5, 100.0);
|
|
for (time, value) in [(15.3, 0), (16.0, 100)] {
|
|
let reply = s.execute("prop.addKey", json!({"layer": layer, "path": "transform/opacity", "time": time, "timeBase": "comp", "value": value})).unwrap();
|
|
assert_seconds(&reply["compTime"], time);
|
|
assert_seconds(&reply["layerTime"], time - 10.5);
|
|
assert!(reply.get("warning").is_none());
|
|
}
|
|
let read = s.execute("prop.get", json!({"layer": layer, "path": "transform/opacity", "time": 15.65})).unwrap();
|
|
assert_seconds(&read["value"], 50.0);
|
|
assert_seconds(&read["keys"][0]["compTime"], 15.3);
|
|
let reply = s.execute("prop.set", json!({"layer": layer, "path": "transform/opacity", "time": 15.5, "timeBase": "comp", "value": 45})).unwrap();
|
|
assert_seconds(&reply["compTime"], 15.5);
|
|
assert_eq!(prop(&s, layer, "transform/opacity").keys.len(), 3);
|
|
s.execute("edit.undo", json!({})).unwrap();
|
|
assert_eq!(prop(&s, layer, "transform/opacity").keys.len(), 2);
|
|
s.execute("edit.redo", json!({})).unwrap();
|
|
assert_eq!(prop(&s, layer, "transform/opacity").keys.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn default_layer_times_are_preserved_and_outside_keys_warn() {
|
|
let (mut s, layer) = offset_layer(10.5, 100.0);
|
|
let legacy = s.execute("prop.addKey", json!({"layer": layer, "path": "transform/opacity", "time": 4.8, "value": 0})).unwrap();
|
|
assert_eq!(legacy, json!(1), "ordinary legacy replies stay scalar");
|
|
let outside = s.execute("prop.addKey", json!({"layer": layer, "path": "transform/opacity", "time": 15.3, "value": 100})).unwrap();
|
|
assert_eq!(outside, json!(2), "legacy replies keep their raw count outside the active range");
|
|
let read = s.execute("prop.get", json!({"layer": layer, "path": "transform/opacity"})).unwrap();
|
|
assert_seconds(&read["keys"][1]["time"], 15.3);
|
|
assert_seconds(&read["keys"][1]["compTime"], 25.8);
|
|
assert!(read["keys"][1].get("warning").is_some());
|
|
let selected = s.execute("keys.select", json!({"keys": [{"layer": layer, "path": "transform/opacity", "time": 25.8}], "timeBase": "comp"})).unwrap();
|
|
assert_seconds(&selected["keys"][0]["compTime"], 25.8);
|
|
assert!(selected.get("warning").is_some());
|
|
let moved = s.execute("keys.set", json!({"layer": layer, "path": "transform/opacity", "time": 15.3, "newTime": 9.5, "timeBase": "layer"})).unwrap();
|
|
assert_seconds(&moved["compTime"], 20.0);
|
|
assert!(moved.get("warning").is_some(), "out point is exclusive");
|
|
}
|
|
|
|
#[test]
|
|
fn stretched_and_reversed_keys_use_the_comp_frame_grid_for_both_bases() {
|
|
for (start, stretch) in [(10.5, 200.0), (18.0, -200.0)] {
|
|
let (mut s, layer) = offset_layer(start, stretch);
|
|
for time in [15.3, 16.0] {
|
|
let layer_time = (time - start) * 100.0 / stretch;
|
|
let comp_reply =
|
|
s.execute("prop.addKey", json!({"layer": layer, "path": "transform/opacity", "time": time, "timeBase": "comp", "value": 20})).unwrap();
|
|
assert_seconds(&comp_reply["layerTime"], layer_time);
|
|
let layer_reply =
|
|
s.execute("prop.addKey", json!({"layer": layer, "path": "transform/rotation", "time": layer_time, "timeBase": "layer", "value": 20})).unwrap();
|
|
assert_seconds(&layer_reply["compTime"], time);
|
|
}
|
|
let selected = s.execute("keys.select", json!({"keys": [{"layer": layer, "path": "transform/opacity", "time": 15.3}], "timeBase": "comp"})).unwrap();
|
|
assert_seconds(&selected["keys"][0]["compTime"], 15.3);
|
|
let moved = s
|
|
.execute("keys.set", json!({"layer": layer, "path": "transform/opacity", "time": 15.3, "newTime": 15.41, "timeBase": "comp", "value": 60}))
|
|
.unwrap();
|
|
assert_seconds(&moved["compTime"], 15.4);
|
|
let ly = s.active_comp().unwrap().layer(LayerId(layer)).unwrap();
|
|
assert_eq!(ly.comp_time(s.state.selected_keys[0].time), Tick::from_seconds_f64(15.4));
|
|
let layer_reply = s.execute("keys.set", json!({"layer": layer, "path": "transform/rotation", "time": (15.3 - start) * 100.0 / stretch, "newTime": (15.41 - start) * 100.0 / stretch, "timeBase": "layer"})).unwrap();
|
|
assert_seconds(&layer_reply["compTime"], 15.4);
|
|
s.execute("edit.undo", json!({})).unwrap();
|
|
s.execute("edit.undo", json!({})).unwrap();
|
|
let ly = s.active_comp().unwrap().layer(LayerId(layer)).unwrap();
|
|
let restored = prop(&s, layer, "transform/opacity");
|
|
assert!(restored.keys.iter().any(|k| ly.comp_time(k.time) == Tick::from_seconds_f64(15.3)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_key_time_arguments_leave_properties_and_history_unchanged() {
|
|
let (mut s, layer) = offset_layer(10.5, 100.0);
|
|
let before = prop(&s, layer, "transform/opacity");
|
|
let history_len = s.history.undo.len();
|
|
for cmd in ["prop.addKey", "prop.set", "keys.set"] {
|
|
for extra in [json!({"time": "oops"}), json!({"time": 1e300}), json!({"timeBase": "seconds", "time": 0})] {
|
|
let mut p = json!({"layer": layer, "path": "transform/opacity", "value": 10});
|
|
p.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
|
|
assert!(s.execute(cmd, p).is_err(), "{cmd}: {extra}");
|
|
assert_eq!(prop(&s, layer, "transform/opacity"), before);
|
|
assert_eq!(s.history.undo.len(), history_len);
|
|
}
|
|
}
|
|
assert!(s.execute("keys.select", json!({"keys": [], "timeBase": null})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn omitted_key_times_and_reads_use_the_target_comps_cti() {
|
|
let (mut s, layer) = offset_layer(10.5, 100.0);
|
|
let target = s.active_comp_id().unwrap().0;
|
|
s.execute("time.set", json!({"time": 15.3})).unwrap();
|
|
s.execute("comp.new", json!({"name": "Other", "duration": 20})).unwrap();
|
|
s.execute("time.set", json!({"time": 1})).unwrap();
|
|
let reply = s.execute("prop.addKey", json!({"comp": target, "layer": layer, "path": "transform/opacity", "timeBase": "comp", "value": 25})).unwrap();
|
|
assert_seconds(&reply["compTime"], 15.3);
|
|
let read = s.execute("prop.get", json!({"comp": target, "layer": layer, "path": "transform/opacity"})).unwrap();
|
|
assert_seconds(&read["time"], 15.3);
|
|
assert_seconds(&read["value"], 25.0);
|
|
}
|
|
|
|
#[test]
|
|
fn excessive_stretch_or_start_conversions_fail_without_clamping_keys() {
|
|
for (start, stretch, base, time) in [(10.5, 1e300, "layer", 1.0), (10.5, 1e-300, "comp", 15.3), (Tick::MAX.seconds(), 100.0, "layer", 1.0)] {
|
|
let (mut s, layer) = offset_layer(10.5, 100.0);
|
|
let cid = s.active_comp_id().unwrap();
|
|
let ly = std::sync::Arc::make_mut(&mut s.project).comp_mut(cid).unwrap().layer_mut(LayerId(layer)).unwrap();
|
|
ly.start_time = Tick::from_seconds_f64(start);
|
|
ly.stretch = stretch;
|
|
let before = s.history.undo.len();
|
|
let result = s.execute("prop.addKey", json!({"layer": layer, "path": "transform/opacity", "time": time, "timeBase": base, "value": 10}));
|
|
assert!(result.is_err(), "start {start}, stretch {stretch}: {result:?}");
|
|
assert_eq!(s.history.undo.len(), before);
|
|
assert!(prop(&s, layer, "transform/opacity").keys.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_base_on_static_property_reports_timing_without_enabling_animation() {
|
|
let (mut s, layer) = offset_layer(10.5, 100.0);
|
|
let reply = s.execute("prop.set", json!({"layer": layer, "path": "transform/opacity", "time": 15.3, "timeBase": "comp", "value": 25})).unwrap();
|
|
assert_seconds(&reply["compTime"], 15.3);
|
|
assert_eq!(reply["animated"], false);
|
|
assert_eq!(reply["value"], 25.0);
|
|
assert!(prop(&s, layer, "transform/opacity").keys.is_empty());
|
|
}
|