mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
136 lines
5.5 KiB
Rust
136 lines
5.5 KiB
Rust
//! Branching history: undo then a new step keeps the undone line as a branch; the tree lists
|
|
//! every state; jumping to any state (on any branch) restores it and keeps everything else.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
|
|
fn names(s: &Session) -> Vec<String> {
|
|
s.project.comps().next().map(|(_, c)| c.layers.iter().map(|l| l.name.clone()).collect()).unwrap_or_default()
|
|
}
|
|
|
|
fn solid(s: &mut Session, name: &str) {
|
|
s.execute("layer.newSolid", json!({"name": name, "color": "#ff0000", "width": 10, "height": 10})).unwrap();
|
|
}
|
|
|
|
fn list(s: &mut Session) -> Vec<Value> {
|
|
s.execute("edit.history.list", json!({})).unwrap()["states"].as_array().cloned().unwrap()
|
|
}
|
|
|
|
fn index_of(states: &[Value], label: &str, nth: usize) -> u64 {
|
|
states.iter().filter(|n| n["label"] == label).nth(nth).unwrap_or_else(|| panic!("no state {label} in {states:?}"))["index"].as_u64().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn undo_then_new_step_keeps_a_branch() {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "H", "width": 64, "height": 64, "duration": 2})).unwrap();
|
|
solid(&mut s, "A");
|
|
solid(&mut s, "B");
|
|
solid(&mut s, "C");
|
|
// Linear so far: Original, New Composition, 3 solids.
|
|
let st = list(&mut s);
|
|
assert_eq!(st.len(), 5, "{st:?}");
|
|
assert!(st.iter().all(|n| n["depth"] == 0));
|
|
assert_eq!(st.last().unwrap()["current"], true);
|
|
// Undo twice (B and C undone), then a new step: AE would forget B and C.
|
|
assert!(s.undo() && s.undo());
|
|
let st = list(&mut s);
|
|
assert_eq!(st.iter().filter(|n| n["future"] == true).count(), 2);
|
|
solid(&mut s, "D");
|
|
assert!(s.history.redo.is_empty(), "the new step clears Redo, as in After Effects");
|
|
assert_eq!(s.history.branches.len(), 1);
|
|
assert_eq!(names(&s), ["D", "A"]);
|
|
let st = list(&mut s);
|
|
assert_eq!(st.len(), 6, "the undone states are still listed: {st:?}");
|
|
let branch: Vec<&Value> = st.iter().filter(|n| n["depth"] == 1).collect();
|
|
assert_eq!(branch.len(), 2, "B and C sit on a branch: {st:?}");
|
|
// Jump to C on the old branch.
|
|
let c = st.iter().rfind(|n| n["depth"] == 1).unwrap()["index"].as_u64().unwrap();
|
|
let r = s.execute("edit.history.goto", json!({"index": c})).unwrap();
|
|
assert_eq!(r["label"], "New Solid", "{r}");
|
|
assert_eq!(names(&s), ["C", "B", "A"]);
|
|
// Now D is the branch, and Undo walks the B/C line.
|
|
assert_eq!(s.history.branches.len(), 1);
|
|
assert_eq!(s.history.branches[0].steps.len(), 1);
|
|
assert!(s.undo());
|
|
assert_eq!(names(&s), ["B", "A"]);
|
|
assert!(s.redo());
|
|
// ...and back to D by id.
|
|
let st = list(&mut s);
|
|
let d = st.iter().find(|n| n["depth"] == 1).unwrap()["id"].as_str().unwrap().to_string();
|
|
s.execute("edit.history.goto", json!({"id": d})).unwrap();
|
|
assert_eq!(names(&s), ["D", "A"]);
|
|
let st = list(&mut s);
|
|
assert_eq!(st.len(), 6);
|
|
assert_eq!(st.iter().filter(|n| n["current"] == true).count(), 1);
|
|
// The original state.
|
|
let root = index_of(&st, "Original", 0);
|
|
s.execute("edit.history.goto", json!({"index": root})).unwrap();
|
|
assert!(s.project.comps().next().is_none());
|
|
assert!(s.history.undo.is_empty());
|
|
assert_eq!(list(&mut s).len(), 6, "nothing is lost by jumping");
|
|
// Relative steps along the line.
|
|
s.execute("edit.history.goto", json!({"steps": 2})).unwrap();
|
|
assert_eq!(names(&s), ["A"]);
|
|
assert!(s.execute("edit.history.goto", json!({"id": "nope"})).is_err());
|
|
assert!(s.execute("edit.history.goto", json!({})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn branches_of_branches_and_purge() {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "H", "width": 64, "height": 64, "duration": 2})).unwrap();
|
|
solid(&mut s, "A");
|
|
solid(&mut s, "B");
|
|
s.undo();
|
|
solid(&mut s, "C"); // branch 1: B
|
|
s.undo();
|
|
solid(&mut s, "D"); // branch 2: C
|
|
let st = list(&mut s);
|
|
assert_eq!(st.len(), 6, "{st:?}");
|
|
// Go to B, undo to A, make E: D's line becomes a branch, and a branch of the new line.
|
|
let b = st.iter().find(|n| n["depth"] == 1 && n["label"] == "New Solid").unwrap()["index"].as_u64().unwrap();
|
|
s.execute("edit.history.goto", json!({"index": b})).unwrap();
|
|
let which = names(&s);
|
|
assert!(which == ["B", "A"] || which == ["C", "A"], "{which:?}");
|
|
s.undo();
|
|
solid(&mut s, "E");
|
|
let st = list(&mut s);
|
|
assert_eq!(st.len(), 7, "{st:?}");
|
|
// Every state is reachable and restores its own layers.
|
|
let mut seen = std::collections::BTreeSet::new();
|
|
for i in 0..st.len() {
|
|
s.execute("edit.history.goto", json!({"index": i})).unwrap();
|
|
seen.insert(names(&s).join(","));
|
|
assert_eq!(list(&mut s).len(), 7);
|
|
}
|
|
for want in ["", "A", "B,A", "C,A", "D,A", "E,A"] {
|
|
assert!(seen.contains(want), "{want:?} not reachable: {seen:?}");
|
|
}
|
|
s.execute("edit.purgeUndo", json!({})).unwrap();
|
|
assert_eq!(list(&mut s).len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn undo_levels_trim_branches() {
|
|
let mut s = Session::default();
|
|
s.prefs.general.undo_levels = 3;
|
|
s.execute("comp.new", json!({"name": "H", "width": 64, "height": 64, "duration": 2})).unwrap();
|
|
for n in ["A", "B", "C", "D", "E"] {
|
|
solid(&mut s, n);
|
|
}
|
|
assert_eq!(s.history.undo.len(), 3);
|
|
s.undo();
|
|
s.undo();
|
|
solid(&mut s, "F");
|
|
// The branch (D, E) grew from a state still kept.
|
|
assert_eq!(s.history.branch_states(), 2);
|
|
for n in ["G", "H", "I"] {
|
|
solid(&mut s, n);
|
|
}
|
|
// Its parent fell off the end of the undo levels: the branch goes too.
|
|
assert_eq!(s.history.branch_states(), 0);
|
|
assert_eq!(list(&mut s).len(), 4);
|
|
}
|