mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 00:40:57 -06:00
#1244: undo history bounded by bytes, without lookup tables or repeated selections in its steps
History was limited by step count only; a whole-selection move of 50k paths keeps ~40 MB per step. Steps now carry a size estimate (only unshared subtrees are visited) and the oldest are dropped beyond a byte budget, keeping the last 8. Documents entering history drop their id index, selections are shared between steps, the command journal is bounded (held while an Action records), and document resources are copy-on-write. Saved files are byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
fe8a6983c5
commit
bbbafdef6b
@@ -142,8 +142,8 @@ impl Clipboard {
|
||||
d.patterns = self.patterns.clone();
|
||||
d.swatches.extend(self.swatches.iter().cloned());
|
||||
d.graphic_styles.extend(self.graphic_styles.iter().cloned());
|
||||
d.char_styles = self.char_styles.clone();
|
||||
d.para_styles = self.para_styles.clone();
|
||||
d.char_styles = self.char_styles.clone().into();
|
||||
d.para_styles = self.para_styles.clone().into();
|
||||
if !self.brushes.is_empty() {
|
||||
let mut lib = vectorcraft_brush::library(&d);
|
||||
for b in &self.brushes {
|
||||
|
||||
@@ -1198,6 +1198,7 @@ fn persp_draw(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(e) = st.history.undo.last_mut() {
|
||||
e.label = "Draw in Perspective".into();
|
||||
}
|
||||
st.history.recost_last(&st.doc);
|
||||
}
|
||||
res?;
|
||||
Ok(json!({ "id": id.0, "plane": plane.id() }))
|
||||
@@ -1268,7 +1269,9 @@ fn edit_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// in every undo state (the flat view is never an undo step of its own).
|
||||
pub(crate) fn finish_edit_text(st: &mut crate::DocState, id: NodeId) {
|
||||
let clear = |doc: &mut Arc<Document>| {
|
||||
if doc.node(id).and_then(|n| n.perspective.as_deref()).is_some_and(|p| p.editing)
|
||||
// Undo steps keep no lookup cache: look without building one, and drop the one an edit
|
||||
// builds (the document now keeps its own).
|
||||
if doc.node_uncached(id).and_then(|n| n.perspective.as_deref()).is_some_and(|p| p.editing)
|
||||
&& let Some(p) = Arc::make_mut(doc).node_mut(id).and_then(|n| n.perspective.as_deref_mut())
|
||||
{
|
||||
p.editing = false;
|
||||
@@ -1281,6 +1284,7 @@ pub(crate) fn finish_edit_text(st: &mut crate::DocState, id: NodeId) {
|
||||
}
|
||||
for e in st.history.undo.iter_mut().chain(st.history.redo.iter_mut()) {
|
||||
clear(&mut e.doc);
|
||||
e.doc.drop_lookup_cache();
|
||||
}
|
||||
if let Some(it) = &mut st.interaction {
|
||||
clear(&mut it.doc);
|
||||
|
||||
@@ -402,9 +402,9 @@ fn edit_saved(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(C, format!("two saved selections would be named `{}`", x.name)));
|
||||
}
|
||||
}
|
||||
if after != before {
|
||||
if *before != after {
|
||||
s.edit("Edit Selection", |d, _| {
|
||||
d.saved_selections = after;
|
||||
d.saved_selections = after.into();
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -215,11 +215,11 @@ fn undo(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let e = st.history.undo.pop().ok_or_else(|| EngineError::Other("nothing to undo".into()))?;
|
||||
let label = e.label.clone();
|
||||
let editing = st.doc.mask_edit.map(|m| m.layer);
|
||||
st.history.redo.push(crate::HistoryEntry {
|
||||
label: e.label,
|
||||
doc: std::mem::replace(&mut st.doc, e.doc),
|
||||
selection: std::mem::replace(&mut st.selection, e.selection),
|
||||
});
|
||||
let doc = std::mem::replace(&mut st.doc, e.doc);
|
||||
let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection));
|
||||
let selection = shared_selection(selection, st.history.redo.last());
|
||||
st.history.redo.push(crate::HistoryEntry::kept(e.label, doc, selection, &st.doc));
|
||||
st.history.trim();
|
||||
st.revision += 1;
|
||||
st.selection.prune(&st.doc);
|
||||
super::maskedit::follow_history(st, editing);
|
||||
@@ -231,17 +231,25 @@ fn redo(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let e = st.history.redo.pop().ok_or_else(|| EngineError::Other("nothing to redo".into()))?;
|
||||
let label = e.label.clone();
|
||||
let editing = st.doc.mask_edit.map(|m| m.layer);
|
||||
st.history.undo.push(crate::HistoryEntry {
|
||||
label: e.label,
|
||||
doc: std::mem::replace(&mut st.doc, e.doc),
|
||||
selection: std::mem::replace(&mut st.selection, e.selection),
|
||||
});
|
||||
let doc = std::mem::replace(&mut st.doc, e.doc);
|
||||
let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection));
|
||||
let selection = shared_selection(selection, st.history.undo.last());
|
||||
st.history.undo.push(crate::HistoryEntry::kept(e.label, doc, selection, &st.doc));
|
||||
st.history.trim();
|
||||
st.revision += 1;
|
||||
st.selection.prune(&st.doc);
|
||||
super::maskedit::follow_history(st, editing);
|
||||
Ok(json!({ "redone": label }))
|
||||
}
|
||||
|
||||
/// `selection` for an undo (redo) step, sharing that of `like`, the step beside it, when the same.
|
||||
fn shared_selection(selection: vectorcraft_doc::Selection, like: Option<&crate::HistoryEntry>) -> Arc<vectorcraft_doc::Selection> {
|
||||
match like {
|
||||
Some(l) if *l.selection == selection => l.selection.clone(),
|
||||
_ => Arc::new(selection),
|
||||
}
|
||||
}
|
||||
|
||||
/// Selected top-level objects in paint order, dropping any whose ancestor is also selected.
|
||||
pub(crate) fn selected_roots(s: &Session) -> Result<Vec<NodeId>> {
|
||||
let st = s.doc()?;
|
||||
|
||||
@@ -180,6 +180,7 @@ pub(crate) fn squash(s: &mut Session, from: usize, label: &str) {
|
||||
if h.len() > from {
|
||||
h.truncate(from + 1);
|
||||
h[from].label = label.to_string();
|
||||
st.history.recost_last(&st.doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,8 +543,7 @@ impl Session {
|
||||
for st in &mut self.docs {
|
||||
if history_changed {
|
||||
st.history.limit = self.prefs.history_states as usize;
|
||||
let over = st.history.undo.len().saturating_sub(st.history.limit);
|
||||
st.history.undo.drain(..over);
|
||||
st.history.trim();
|
||||
}
|
||||
// Documents in pattern editing mode redraw their tile edge.
|
||||
if tile_edge_changed && st.doc.pattern_edit.is_some() {
|
||||
|
||||
@@ -606,7 +606,7 @@ fn reset_defaults(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut added = vec![];
|
||||
if replace {
|
||||
let before: Vec<String> = d.swatches_iter().map(|w| w.name.clone()).collect();
|
||||
(d.swatches, d.swatch_groups) = (defaults, groups);
|
||||
(d.swatches, d.swatch_groups) = (defaults.into(), groups.into());
|
||||
added.extend(d.swatches_iter().filter(|w| !before.contains(&w.name)).map(|w| w.name.clone()));
|
||||
live.extend(d.swatches_iter().filter(|w| w.global).map(|w| w.name.clone()));
|
||||
d.map_solid_paints(&mut |_, l, _| unlink_dead(&live, l));
|
||||
|
||||
@@ -755,7 +755,7 @@ fn discard_empty(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// plus the text (and its id), roll back to it without a trace.
|
||||
let st = s.doc_mut()?;
|
||||
if st.interaction.is_none()
|
||||
&& let Some(k) = st.history.undo.iter().rposition(|e| e.doc.node(id).is_none())
|
||||
&& let Some(k) = st.history.undo.iter().rposition(|e| e.doc.node_uncached(id).is_none())
|
||||
&& let Some(created) = st.history.undo.get(k)
|
||||
{
|
||||
let mut now = (*st.doc).clone();
|
||||
|
||||
Reference in New Issue
Block a user