Files
effectcraft/crates/engine/src/tests_key_metadata_order.rs
T
tylerjenningsw 7643f803e0 Keyframes: moving keys keeps the selection order
keys.move (and nudging) rebuilt the key selection in layer and property
order, so the order the keys were selected in was lost after a move. A
Quick Offset that follows staggers layers in that order (#1012).

The move now goes through move_key_refs, which maps each selected key to
its new time and returns the selection in its original order, with the
same collisions (a key landing on another replaces it), roving retiming
and locked-layer handling as before. key_gesture is the drag-step logic
(start again from the keys and project as they were before the drag)
split out of shift_keys so other key drags can share it.

Test: moving_keys_keeps_the_selection_order (fails before: the keys come
back sorted by layer).
2026-10-12 17:37:29 -04:00

127 lines
5.5 KiB
Rust

//! Key attributes preserve the order used by the next stagger gesture.
use std::sync::Arc;
use effectcraft_keyframe::Interp;
use effectcraft_project::LayerId;
use serde_json::json;
use crate::{KeyRef, Session, time::FrameRate};
fn setup() -> (Session, u64) {
let mut s = Session::default();
s.execute_checked("comp.new", json!({"width": 64, "height": 64, "frameRate": 30, "duration": 4})).unwrap();
let layer = s.execute_checked("layer.newSolid", json!({"name": "Box", "color": "#fff"})).unwrap()["layer"].as_u64().unwrap();
(s, layer)
}
fn add(s: &mut Session, layer: u64, path: &str, time: i64, value: serde_json::Value) {
s.execute_checked("prop.addKey", json!({"layer": layer, "path": path, "time": time, "value": value})).unwrap();
}
fn uid(s: &Session, layer: u64, path: &str) -> u64 {
s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.prop(path).unwrap().uid
}
fn key(layer: u64, prop: u64, frame: i64) -> KeyRef {
KeyRef { layer: LayerId(layer), prop, time: FrameRate::FPS_30.tick_of(frame) }
}
#[test]
fn easing_preserves_interleaved_layer_and_property_order_with_one_undo() {
let (mut s, first) = setup();
let second = s.execute_checked("layer.newSolid", json!({"name": "Other", "color": "#fff"})).unwrap()["layer"].as_u64().unwrap();
for layer in [first, second] {
for time in [0, 1] {
add(&mut s, layer, "transform/opacity", time, json!(time * 25));
}
}
add(&mut s, first, "transform/position", 1, json!([10, 20]));
let a = uid(&s, first, "transform/opacity");
let b = uid(&s, second, "transform/opacity");
let position = uid(&s, first, "transform/position");
s.execute_checked(
"keys.select",
json!({"keys": [
{"layer": second, "prop": b, "time": 1},
{"layer": first, "prop": position, "time": 1},
{"layer": first, "prop": a, "time": 0},
{"layer": second, "prop": b, "time": 0}
]}),
)
.unwrap();
let order = s.state.selected_keys.clone();
let before = s.project.clone();
s.history.clear();
s.execute_checked("keys.easyEase", json!({})).unwrap();
assert_eq!(s.state.selected_keys, order);
assert_eq!(s.history.undo.len(), 1);
for selected in &order {
let keys = &s.active_comp().unwrap().layer(selected.layer).unwrap().props.find(selected.prop).unwrap().keys;
assert_eq!(keys.iter().find(|k| k.time == selected.time).unwrap().out_interp, Interp::Bezier);
}
s.execute_checked("edit.undo", json!({})).unwrap();
assert!(Arc::ptr_eq(&s.project, &before));
assert_eq!(s.state.selected_keys, order);
}
#[test]
fn roving_keeps_the_selected_key_identity_and_arbitrary_order() {
let (mut s, layer) = setup();
for (time, x) in [(0, 0), (1, 25), (2, 100)] {
add(&mut s, layer, "transform/position", time, json!([x, 0]));
}
let prop = uid(&s, layer, "transform/position");
s.execute_checked(
"keys.select",
json!({"keys": [
{"layer": layer, "prop": prop, "time": 1},
{"layer": layer, "prop": prop, "time": 2},
{"layer": layer, "prop": prop, "time": 0}
]}),
)
.unwrap();
s.execute_checked("keys.interpolation", json!({"roving": true})).unwrap();
assert_eq!(s.state.selected_keys, vec![key(layer, prop, 15), key(layer, prop, 60), key(layer, prop, 0)]);
let position = s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.find(prop).unwrap();
assert_eq!(position.keys[1].value.as_vec2(), [25.0, 0.0]);
assert_eq!(position.keys[1].time, FrameRate::FPS_30.tick_of(15));
assert!(position.keys[1].roving);
assert!(!position.keys[0].roving && !position.keys[2].roving);
}
#[test]
fn attribute_edit_deduplicates_legacy_refs_and_drops_stale_refs_without_snapping() {
let (mut s, layer) = setup();
for time in [0, 1] {
add(&mut s, layer, "transform/opacity", time, json!(20));
}
let prop = uid(&s, layer, "transform/opacity");
let first = key(layer, prop, 0);
let second = key(layer, prop, 30);
s.state.selected_keys = vec![second, key(layer, prop, 10), first, second, key(layer, u64::MAX, 0), first, key(u64::MAX, prop, 0)];
s.history.clear();
let result = s.execute_checked("keys.toggleHold", json!({})).unwrap();
assert_eq!(result, json!(2));
assert_eq!(s.state.selected_keys, vec![second, first]);
assert_eq!(s.history.undo.len(), 1);
let opacity = s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.find(prop).unwrap();
assert!(opacity.keys.iter().all(|k| k.out_interp == Interp::Hold));
s.execute_checked("edit.undo", json!({})).unwrap();
let opacity = s.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.find(prop).unwrap();
assert!(opacity.keys.iter().all(|k| k.out_interp == Interp::Linear));
}
#[test]
fn moving_keys_keeps_the_selection_order() {
let (mut s, first) = setup();
let second = s.execute_checked("layer.newSolid", json!({"name": "Other", "color": "#fff"})).unwrap()["layer"].as_u64().unwrap();
for layer in [first, second] {
add(&mut s, layer, "transform/opacity", 0, json!(0));
}
let (a, b) = (uid(&s, first, "transform/opacity"), uid(&s, second, "transform/opacity"));
s.execute_checked("keys.select", json!({"keys": [{"layer": second, "prop": b, "time": 0}, {"layer": first, "prop": a, "time": 0}]})).unwrap();
s.execute_checked("keys.move", json!({"delta": 2.0 / 30.0})).unwrap();
assert_eq!(s.state.selected_keys, vec![key(second, b, 2), key(first, a, 2)], "not sorted by layer");
}