effectcraft/crates/engine/src/tests_markers.rs

184 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Composition / layer markers: dialog fields, move, duration, delete, convert, update from
//! source, all with undo.
use serde_json::json;
use crate::tests_timeline::setup;
#[test]
fn comp_marker_dialog_fields_and_undo() {
let (mut s, _) = setup();
s.execute("time.set", json!({"time": 1.0})).unwrap();
s.execute("comp.addMarker", json!({"comment": "a"})).unwrap();
let r = s
.execute(
"markers.set",
json!({"index": 0, "time": 1.51, "duration": 0.5, "comment": "Intro", "chapter": "One", "url": "https://getartcraft.com", "frameTarget": "_blank",
"cuePoint": {"name": "cue", "navigation": true, "params": [["k", "v"]]}, "protected": true, "label": "Aqua"}),
)
.unwrap();
assert_eq!(r["index"], 0);
let l = s.execute("markers.list", json!({})).unwrap();
let m = &l[0];
// Snapped to whole frames (30 fps).
assert!((m["time"].as_f64().unwrap() - 1.5).abs() < 1e-9, "{m}");
assert!((m["duration"].as_f64().unwrap() - 0.5).abs() < 1e-9);
assert_eq!(m["comment"], "Intro");
assert_eq!(m["chapter"], "One");
assert_eq!(m["url"], "https://getartcraft.com");
assert_eq!(m["frameTarget"], "_blank");
assert_eq!(m["cuePoint"]["name"], "cue");
assert_eq!(m["cuePoint"]["navigation"], true);
assert_eq!(m["cuePoint"]["params"][0][1], "v");
assert_eq!(m["protected"], true);
assert_eq!(m["label"], "Aqua");
s.execute("edit.undo", json!({})).unwrap();
let l = s.execute("markers.list", json!({})).unwrap();
assert_eq!(l[0]["comment"], "a");
assert_eq!(l[0]["time"], 1.0);
s.execute("edit.redo", json!({})).unwrap();
// Find by time, clear the cue point, delete with undo.
s.execute("markers.set", json!({"at": 1.5, "cuePoint": null})).unwrap();
assert!(s.execute("markers.list", json!({})).unwrap()[0]["cuePoint"].is_null());
s.execute("markers.delete", json!({"at": 1.5})).unwrap();
assert_eq!(s.execute("markers.list", json!({})).unwrap().as_array().unwrap().len(), 0);
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(s.execute("markers.list", json!({})).unwrap().as_array().unwrap().len(), 1);
assert!(s.execute("markers.set", json!({"index": 5, "comment": "x"})).is_err());
}
#[test]
fn drag_merges_into_one_undo_step_and_markers_stay_sorted() {
let (mut s, _) = setup();
for t in [0.5, 2.0] {
s.execute("markers.set", json!({"new": true, "time": t})).unwrap();
}
let before = s.history.undo.len();
// A drag: several moves merged under one key; passing the other marker re-sorts.
let mut idx = 0;
for t in [1.0, 1.5, 2.5] {
idx = s.execute("markers.set", json!({"index": idx, "time": t, "merge": "mk-drag"})).unwrap()["index"].as_u64().unwrap();
}
assert_eq!(idx, 1);
assert_eq!(s.history.undo.len(), before + 1);
let l = s.execute("markers.list", json!({})).unwrap();
assert_eq!(l[0]["time"], 2.0);
assert_eq!(l[1]["time"], 2.5);
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(s.execute("markers.list", json!({})).unwrap()[0]["time"], 0.5);
}
#[test]
fn layer_markers_convert_and_update_from_source() {
let (mut s, l) = setup();
// Layer starts at 1 s: layer markers are stored in layer time but addressed in comp time.
s.execute("layer.timing", json!({"layers": [l], "start": 1.0})).unwrap();
s.execute("markers.set", json!({"layer": l, "new": true, "time": 2.0, "comment": "hit"})).unwrap();
let c = s.active_comp().unwrap();
let lay = c.layer(effectcraft_project::LayerId(l)).unwrap();
assert_eq!(lay.markers[0].time.seconds(), 1.0);
assert_eq!(s.execute("markers.list", json!({"layer": l})).unwrap()[0]["time"], 2.0);
// Layer → comp marker keeps the comp time.
s.execute("markers.convert", json!({"layer": l, "index": 0})).unwrap();
assert_eq!(s.execute("markers.list", json!({})).unwrap()[0]["time"], 2.0);
assert_eq!(s.execute("markers.list", json!({"layer": l})).unwrap().as_array().unwrap().len(), 0);
// And back.
s.execute("markers.convert", json!({"index": 0, "toLayer": l})).unwrap();
assert_eq!(s.execute("markers.list", json!({"layer": l})).unwrap()[0]["comment"], "hit");
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(s.execute("markers.list", json!({})).unwrap().as_array().unwrap().len(), 1);
// Locked markers refuse edits.
s.execute("layer.markersLock", json!({"layers": [l], "value": true})).unwrap();
assert!(s.execute("markers.set", json!({"layer": l, "new": true})).is_err());
s.execute("layer.markersLock", json!({"layers": [l], "value": false})).unwrap();
// Update Markers From Source on a precomp layer.
let r = s.execute("layer.precompose", json!({"layers": [l], "name": "Inner"})).unwrap();
let inner = r["comp"].as_u64().unwrap();
let outer_layer = r["layer"].as_u64().unwrap();
s.execute("markers.set", json!({"comp": inner, "new": true, "time": 0.5, "comment": "src"})).unwrap();
s.execute("layer.select", json!({"layers": [outer_layer]})).unwrap();
let r = s.execute("layer.updateMarkersFromSource", json!({})).unwrap();
assert_eq!(r["added"], 1);
let lm = s.execute("markers.list", json!({"layer": outer_layer})).unwrap();
assert!(lm.as_array().unwrap().iter().any(|m| m["comment"] == "src"));
// Idempotent; undoable.
assert_eq!(s.execute("layer.updateMarkersFromSource", json!({})).unwrap()["added"], 0);
s.execute("edit.undo", json!({})).unwrap();
s.execute("edit.undo", json!({})).unwrap();
assert!(!s.execute("markers.list", json!({"layer": outer_layer})).unwrap().as_array().unwrap().iter().any(|m| m["comment"] == "src"));
}
#[test]
fn precompose_leave_and_move_attributes() {
use effectcraft_project::{ItemId, LayerId, LayerSource};
let (mut s, l) = setup();
s.execute("prop.set", json!({"layer": l, "path": "transform/position", "value": [50, 60]})).unwrap();
let outer = s.active_comp_id().unwrap();
// Leave all attributes: same layer, keeps its transform; the new comp is the source size and
// holds a default layer of the original source.
let r = s.execute("layer.precompose", json!({"layers": [l], "name": "Leave", "mode": "leave"})).unwrap();
assert_eq!(r["layer"], l);
let inner = ItemId(r["comp"].as_u64().unwrap());
let lay = s.project.comp(outer).unwrap().layer(LayerId(l)).unwrap().clone();
assert_eq!(lay.source, LayerSource::Comp { item: inner });
assert_eq!(lay.props.prop("transform/position").unwrap().value.as_vec2(), [50.0, 60.0]);
let ic = s.project.comp(inner).unwrap();
assert_eq!((ic.width, ic.height), (100, 100));
assert_eq!(ic.layers.len(), 1);
assert_eq!(ic.layers[0].props.prop("transform/position").unwrap().value.as_vec2(), [50.0, 50.0]);
assert!(matches!(ic.layers[0].source, LayerSource::Solid { .. }));
s.execute("edit.undo", json!({})).unwrap();
assert!(s.project.comp(inner).is_none());
assert!(matches!(s.project.comp(outer).unwrap().layer(LayerId(l)).unwrap().source, LayerSource::Solid { .. }));
// Leave needs one layer with a source item.
let t = s.execute("layer.newText", json!({"text": "Hi"})).unwrap()["layer"].as_u64().unwrap();
assert!(s.execute("layer.precompose", json!({"layers": [t], "mode": "leave"})).is_err());
assert!(s.execute("layer.precompose", json!({"layers": [t, l], "mode": "leave"})).is_err());
// Move all attributes + adjust duration + open: the layer moves inside, trimmed to its span.
s.execute("layer.timing", json!({"layers": [l], "in": 1.0, "out": 3.0})).unwrap();
let r = s.execute("layer.precompose", json!({"layers": [l], "name": "Move", "adjustDuration": true, "open": true})).unwrap();
let inner = ItemId(r["comp"].as_u64().unwrap());
let ic = s.project.comp(inner).unwrap();
assert!((ic.duration.seconds() - 2.0).abs() < 1e-9);
assert_eq!(ic.layers[0].props.prop("transform/position").unwrap().value.as_vec2(), [50.0, 60.0]);
assert_eq!(ic.layers[0].in_point.seconds(), 0.0);
let nl = s.project.comp(outer).unwrap().layer(LayerId(r["layer"].as_u64().unwrap())).unwrap().clone();
assert_eq!((nl.in_point.seconds(), nl.out_point.seconds()), (1.0, 3.0));
assert_eq!(s.active_comp_id(), Some(inner));
}
/// A precomp layer shows its comp's markers on its bar, mapped through its timing.
#[test]
fn nested_comp_markers_follow_the_precomp_layers_timing() {
let mut s = crate::Session::default();
let pre = s.execute("comp.new", json!({"name": "Pre", "width": 32, "height": 32, "frameRate": 10, "duration": 4})).unwrap()["comp"].as_u64().unwrap();
for (t, c) in [(1.0, "beat"), (3.0, "drop")] {
s.execute("markers.set", json!({"new": true, "time": t, "comment": c})).unwrap();
}
s.execute("comp.new", json!({"name": "Main", "width": 32, "height": 32, "frameRate": 10, "duration": 20})).unwrap();
let l = s.execute("layer.addItem", json!({"item": pre})).unwrap()["layer"].as_u64().unwrap();
s.execute("layer.timing", json!({"layers": [l], "start": 2.0})).unwrap();
let times = |s: &mut crate::Session| -> Vec<(f64, String)> {
let v = s.execute_checked("markers.nested", json!({"layer": l})).unwrap();
v.as_array().unwrap().iter().map(|m| (m["time"].as_f64().unwrap(), m["comment"].as_str().unwrap().to_string())).collect()
};
assert_eq!(times(&mut s), vec![(3.0, "beat".to_string()), (5.0, "drop".to_string())]);
// Stretched to 200 %: twice as far from the start.
s.execute("layer.timeStretch", json!({"layers": [l], "percent": 200})).unwrap();
assert_eq!(times(&mut s), vec![(4.0, "beat".to_string()), (8.0, "drop".to_string())]);
s.execute("layer.timeStretch", json!({"layers": [l], "percent": 100})).unwrap();
// Trimmed: markers outside the In–Out range are not on the bar.
s.execute("layer.timing", json!({"layers": [l], "out": 4.0})).unwrap();
assert_eq!(times(&mut s), vec![(3.0, "beat".to_string())]);
s.execute("layer.timing", json!({"layers": [l], "out": 6.0})).unwrap();
// Time remapping at half speed: nested 1 s is reached 2 s into the layer.
s.execute("layer.enableTimeRemap", json!({"layers": [l]})).unwrap();
s.execute("prop.addKey", json!({"layer": l, "path": "timeRemap", "time": 4.0, "value": 2.0})).unwrap();
let t = times(&mut s);
assert_eq!(t.first(), Some(&(4.0, "beat".to_string())), "{t:?}");
// Not a precomp layer.
let solid = s.execute("layer.newSolid", json!({"color": "#ffffff"})).unwrap()["layer"].as_u64().unwrap();
assert!(s.execute("markers.nested", json!({"layer": solid})).is_err());
}