184 lines
10 KiB
Rust
184 lines
10 KiB
Rust
//! 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());
|
||
}
|