mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 04:26:55 -06:00
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).
127 lines
5.5 KiB
Rust
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");
|
|
}
|