effectcraft/crates/engine/src/history.rs

348 lines
14 KiB
Rust

//! Branching undo history (the History panel; better than After Effects, whose undo is a
//! straight line).
//!
//! [`History`] keeps After Effects' linear model as its working line — `undo` (the states
//! before each step, oldest first) and `redo` (undone states, next first) — and adds
//! **branches**: when you undo and then do something new, the undone states are not thrown
//! away but kept as a [`Branch`] hanging off the state they grew from. [`History::tree`] lists
//! every state as a tree and [`Session::goto_history`] jumps to any of them, on any branch: the
//! path to the target becomes the working line, and the line you left becomes a branch.
//!
//! States are whole-project snapshots (`Arc<Project>`), identified by their address while the
//! history holds them ([`HistoryNode::id`]), so ids stay valid across undo, redo and jumps.
use std::collections::HashMap;
use std::sync::Arc;
use effectcraft_project::Project;
use serde::Serialize;
use crate::Session;
/// Label of the oldest state in the history (the project as opened, or the oldest state kept).
pub const ROOT_LABEL: &str = "Original";
/// Undo history of whole-project snapshots.
#[derive(Clone, Default)]
pub struct History {
/// The states before each step on the working line, oldest first: (step label, state).
pub undo: Vec<(String, Arc<Project>)>,
/// Undone steps, the next one last: (step label, the state it leads to).
pub redo: Vec<(String, Arc<Project>)>,
/// Key of the last merged step: a continuous gesture with the same key folds into one step.
pub merge_key: Option<String>,
/// Alternative futures kept when a new step was made after undoing.
pub branches: Vec<Branch>,
}
/// A line of states that grows from another state of the history.
#[derive(Clone)]
pub struct Branch {
/// The state the branch grows from.
pub parent: Arc<Project>,
/// The branch's states in order: (label of the step that made it, state).
pub steps: Vec<(String, Arc<Project>)>,
}
/// One state of the history tree (`edit.history.list`).
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HistoryNode {
/// Position in the listing (depth-first, the working line's continuation first).
pub index: usize,
/// Stable id of the state while the history keeps it.
pub id: String,
/// The step that made this state ([`ROOT_LABEL`] for the oldest).
pub label: String,
/// Index of the state it was made from.
pub parent: Option<usize>,
/// Branch nesting (0 = the working line).
pub depth: usize,
/// The current state.
pub current: bool,
/// On the working line (Undo / Redo walk it).
pub line: bool,
/// On the working line after the current state (Redo reaches it).
pub future: bool,
}
fn key(p: &Arc<Project>) -> usize {
Arc::as_ptr(p) as usize
}
/// Id of a state (its address while the history holds it).
pub fn state_id(p: &Arc<Project>) -> String {
format!("s{:x}", key(p))
}
/// The whole tree as an arena (internal).
struct Arena {
label: Vec<String>,
state: Vec<Arc<Project>>,
parent: Vec<Option<usize>>,
children: Vec<Vec<usize>>,
/// Index of each working-line state, oldest first, and the current state's position in it.
line: Vec<usize>,
current: usize,
}
impl Arena {
fn build(h: &History, current: &Arc<Project>) -> Arena {
let mut a = Arena { label: vec![], state: vec![], parent: vec![], children: vec![], line: vec![], current: 0 };
let add = |a: &mut Arena, label: String, st: Arc<Project>, parent: Option<usize>| {
let i = a.state.len();
a.label.push(label);
a.state.push(st);
a.parent.push(parent);
a.children.push(vec![]);
if let Some(p) = parent {
a.children[p].push(i);
}
i
};
// The working line: undo states, the current state, then the redo states.
let mut prev: Option<usize> = None;
let mut next_label = ROOT_LABEL.to_string();
for (label, st) in &h.undo {
let i = add(&mut a, std::mem::take(&mut next_label), st.clone(), prev);
a.line.push(i);
prev = Some(i);
next_label = label.clone();
}
let cur = add(&mut a, next_label, current.clone(), prev);
a.line.push(cur);
a.current = cur;
prev = Some(cur);
for (label, st) in h.redo.iter().rev() {
let i = add(&mut a, label.clone(), st.clone(), prev);
a.line.push(i);
prev = Some(i);
}
// Branches, attached to whichever state they grow from (branches of branches need the
// parent branch first, hence the passes). Orphans (their state was dropped) are skipped.
let mut index: HashMap<usize, usize> = a.state.iter().enumerate().map(|(i, s)| (key(s), i)).collect();
let mut pending: Vec<&Branch> = h.branches.iter().collect();
loop {
let before = pending.len();
pending.retain(|b| {
let Some(&p) = index.get(&key(&b.parent)) else { return true };
let mut prev = p;
for (label, st) in &b.steps {
if index.contains_key(&key(st)) {
// Already in the tree (shared state): continue from it.
prev = index[&key(st)];
continue;
}
let i = add(&mut a, label.clone(), st.clone(), Some(prev));
index.insert(key(st), i);
prev = i;
}
false
});
if pending.is_empty() || pending.len() == before {
break;
}
}
a
}
fn on_line(&self) -> Vec<bool> {
let mut v = vec![false; self.state.len()];
for &i in &self.line {
v[i] = true;
}
v
}
/// Depth-first listing: the working line's child first, then branches in creation order.
fn listing(&self) -> Vec<HistoryNode> {
let on_line = self.on_line();
let pos_in_line: HashMap<usize, usize> = self.line.iter().enumerate().map(|(k, &i)| (i, k)).collect();
let cur_pos = pos_in_line[&self.current];
let mut out: Vec<HistoryNode> = vec![];
let mut order: HashMap<usize, usize> = HashMap::new();
// (node, depth)
let mut stack: Vec<(usize, usize)> = vec![(self.line[0], 0)];
while let Some((n, depth)) = stack.pop() {
order.insert(n, out.len());
let line = on_line[n];
out.push(HistoryNode {
index: out.len(),
id: state_id(&self.state[n]),
label: self.label[n].clone(),
parent: self.parent[n].and_then(|p| order.get(&p).copied()),
depth,
current: n == self.current,
line,
future: line && pos_in_line[&n] > cur_pos,
});
// Push in reverse so the first child pops first; the line child continues at the
// same depth, branch children one deeper; a branch's own continuation (its first
// child) stays at the branch's depth.
for (k, &c) in self.children[n].iter().enumerate().rev() {
let cont = if line { on_line[c] } else { k == 0 };
stack.push((c, if cont { depth } else { depth + 1 }));
}
}
out
}
}
impl History {
/// Record a new step made from `before`: the redo line (if any) is kept as a branch.
pub fn record(&mut self, label: &str, before: Arc<Project>, levels: usize) {
self.abandon_redo(&before);
self.undo.push((label.to_string(), before.clone()));
self.merge_key = None;
self.trim(levels, &before);
}
/// Keep the undone states as a branch growing from `from` (the state a new step starts from).
pub fn abandon_redo(&mut self, from: &Arc<Project>) {
if self.redo.is_empty() {
return;
}
let steps: Vec<(String, Arc<Project>)> = self.redo.drain(..).rev().collect();
self.branches.push(Branch { parent: from.clone(), steps });
}
/// Apply the undo-levels limit: drop the oldest working-line states, the branches that grew
/// from them, and the oldest branches when branches hold more states than `levels`.
pub fn trim(&mut self, levels: usize, current: &Arc<Project>) {
let levels = levels.max(1);
if self.undo.len() > levels {
let extra = self.undo.len() - levels;
self.undo.drain(..extra);
}
let mut total: usize = self.branches.iter().map(|b| b.steps.len()).sum();
while total > levels && !self.branches.is_empty() {
total -= self.branches.remove(0).steps.len();
}
self.prune(current);
}
/// Drop branches whose parent state is no longer in the history (`current` = the current
/// state, which the history doesn't store).
pub fn prune(&mut self, current: &Arc<Project>) {
if self.branches.is_empty() {
return;
}
let mut live: std::collections::HashSet<usize> = self.undo.iter().chain(self.redo.iter()).map(|(_, p)| key(p)).collect();
live.insert(key(current));
let mut keep = vec![false; self.branches.len()];
loop {
let mut changed = false;
for (i, b) in self.branches.iter().enumerate() {
if !keep[i] && live.contains(&key(&b.parent)) {
keep[i] = true;
live.extend(b.steps.iter().map(|(_, p)| key(p)));
changed = true;
}
}
if !changed {
break;
}
}
let mut k = keep.into_iter();
self.branches.retain(|_| k.next().unwrap_or(false));
}
/// Drop everything (Edit ▸ Purge ▸ Undo, New / Open Project).
pub fn clear(&mut self) {
*self = History::default();
}
/// The history as a tree, given the current state.
pub fn tree(&self, current: &Arc<Project>) -> Vec<HistoryNode> {
Arena::build(self, current).listing()
}
/// Number of states on branches (not on the working line).
pub fn branch_states(&self) -> usize {
self.branches.iter().map(|b| b.steps.len()).sum()
}
}
impl Session {
/// The undo history as a tree (the History panel, `edit.history.list`).
pub fn history_tree(&self) -> Vec<HistoryNode> {
self.history.tree(&self.project)
}
/// Jump to any state of the history (`id` from [`HistoryNode::id`]), including states on
/// other branches: the path to it becomes the working line (its undone continuation the
/// redo line), and the line left behind is kept as a branch. Returns false for an unknown
/// id.
pub fn goto_history(&mut self, id: &str) -> bool {
let a = Arena::build(&self.history, &self.project);
let Some(target) = a.state.iter().position(|s| state_id(s) == id) else { return false };
if target == a.current {
return true;
}
let on_line = a.on_line();
// Path root → target.
let mut path = vec![target];
while let Some(p) = a.parent[*path.last().unwrap_or(&target)] {
path.push(p);
}
path.reverse();
// Continue past the target: the working line where it runs on, else each branch's own
// continuation (its first child).
let mut new_line = path.clone();
let mut n = target;
loop {
let kids = &a.children[n];
let next = if on_line[n] { kids.iter().copied().find(|&c| on_line[c]) } else { kids.first().copied() };
match next {
Some(c) => {
new_line.push(c);
n = c;
}
None => break,
}
}
let mut in_new = vec![false; a.state.len()];
for &i in &new_line {
in_new[i] = true;
}
// Every state off the new line goes into branches: a chain per first child, the other
// children starting branches of their own.
fn chain(a: &Arena, start: usize, parent: usize, in_new: &[bool], out: &mut Vec<Branch>) {
let mut steps = vec![];
let mut n = start;
let mut extra: Vec<(usize, usize)> = vec![];
loop {
steps.push((a.label[n].clone(), a.state[n].clone()));
let kids: Vec<usize> = a.children[n].iter().copied().filter(|&c| !in_new[c]).collect();
let Some((&first, rest)) = kids.split_first() else { break };
extra.extend(rest.iter().map(|&c| (c, n)));
n = first;
}
out.push(Branch { parent: a.state[parent].clone(), steps });
for (c, p) in extra {
chain(a, c, p, in_new, out);
}
}
let mut branches = vec![];
for &n in &new_line {
for &c in &a.children[n] {
if !in_new[c] {
chain(&a, c, n, &in_new, &mut branches);
}
}
}
let k = new_line.iter().position(|&i| i == target).unwrap_or(0);
let undo: Vec<(String, Arc<Project>)> = (0..k).map(|i| (a.label[new_line[i + 1]].clone(), a.state[new_line[i]].clone())).collect();
let redo: Vec<(String, Arc<Project>)> = new_line[k + 1..].iter().rev().map(|&i| (a.label[i].clone(), a.state[i].clone())).collect();
let cur = std::mem::replace(&mut self.project, a.state[target].clone());
self.keep_view_settings(&cur);
self.history.undo = undo;
self.history.redo = redo;
self.history.branches = branches;
self.history.merge_key = None;
self.sanitize_state();
self.bump();
true
}
}