- Radierer-Modus „✂ Teilstrich“: entfernt nur den überstrichenen Teil, der Rest bleibt in Stücken; ein Radierzug ist ein Undo-Schritt, Formen werden ganz radiert - ☰ → „Seite als PNG / PNG transparent / SVG“ nach ~/Documents (über den PDF-Export gerendert: Bilder und importierte PDF-Seiten inklusive) - Strg+C legt die Auswahl zusätzlich als Bild in die Systemzwischenablage (für Chat, Mail, Office); in ink-notes fügt Strg+V weiter die Objekte ein - Seitentitel in der Seitenübersicht, Ansicht „Inhalt“ als Inhaltsverzeichnis, Lesezeichen-Ecke an benannten Seiten, Titel im Fenstertitel und als PDF-Lesezeichen (Umlaute als UTF-16) - Dateiformat-Version 6 (Seitentitel) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
299 lines
10 KiB
Rust
299 lines
10 KiB
Rust
//! Transaktionales Undo / Redo mit History-Stack pro Seite oder Notizbuch.
|
|
|
|
use crate::document::Stroke;
|
|
|
|
/// Eine rückgängig machbare Benutzeraktion auf einer Seite.
|
|
#[derive(Debug, Clone)]
|
|
pub enum HistoryAction {
|
|
/// Ein Strich wurde neu gezeichnet.
|
|
AddStroke(Stroke),
|
|
/// Mehrere Striche wurden auf einmal hinzugefügt (z. B. Einfügen/Duplizieren).
|
|
AddStrokes(Vec<Stroke>),
|
|
/// Ein oder mehrere Striche wurden durch den Radierer oder Selektion gelöscht.
|
|
RemoveStrokes(Vec<Stroke>),
|
|
/// Ein Strich wurde durch eine geometrische Form ersetzt.
|
|
ReplaceStroke { old: Stroke, new: Stroke },
|
|
/// Ein oder mehrere Striche wurden modifiziert (z. B. verschoben, gedreht, skaliert, umgefärbt).
|
|
ModifyStrokes {
|
|
before: Vec<Stroke>,
|
|
after: Vec<Stroke>,
|
|
},
|
|
/// Teilradierer: Striche wurden durch ihre übrig gebliebenen Stücke ersetzt.
|
|
SplitStrokes {
|
|
removed: Vec<Stroke>,
|
|
added: Vec<Stroke>,
|
|
/// Zeitpunkt des letzten Schnitts (zum Zusammenfassen eines Radierzugs)
|
|
at: std::time::Instant,
|
|
},
|
|
}
|
|
|
|
/// History-Stack für Transaktionsverwaltung (Undo / Redo).
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct HistoryStack {
|
|
pub undo_stack: Vec<HistoryAction>,
|
|
pub redo_stack: Vec<HistoryAction>,
|
|
}
|
|
|
|
impl HistoryStack {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
undo_stack: Vec::new(),
|
|
redo_stack: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Registriert eine neue Aktion. Leert den Redo-Stack.
|
|
pub fn push(&mut self, action: HistoryAction) {
|
|
self.undo_stack.push(action);
|
|
self.redo_stack.clear();
|
|
}
|
|
|
|
/// Registriert einen Teilradier-Schnitt. Folgt er innerhalb von
|
|
/// [`crate::document::PARTIAL_ERASE_MERGE_WINDOW`] auf einen vorherigen, werden beide zu
|
|
/// einem Undo-Schritt zusammengefasst: Stücke, die im selben Zug wieder zerschnitten
|
|
/// wurden, verschwinden aus der Bilanz.
|
|
pub fn push_split(&mut self, removed: Vec<Stroke>, added: Vec<Stroke>) {
|
|
let now = std::time::Instant::now();
|
|
if let Some(HistoryAction::SplitStrokes {
|
|
removed: prev_removed,
|
|
added: prev_added,
|
|
at,
|
|
}) = self.undo_stack.last_mut()
|
|
{
|
|
if now.duration_since(*at) <= crate::document::PARTIAL_ERASE_MERGE_WINDOW
|
|
&& self.redo_stack.is_empty()
|
|
{
|
|
for stroke in removed {
|
|
if let Some(pos) = prev_added.iter().position(|s| s.id == stroke.id) {
|
|
prev_added.remove(pos);
|
|
} else {
|
|
prev_removed.push(stroke);
|
|
}
|
|
}
|
|
prev_added.extend(added);
|
|
*at = now;
|
|
return;
|
|
}
|
|
}
|
|
self.push(HistoryAction::SplitStrokes {
|
|
removed,
|
|
added,
|
|
at: now,
|
|
});
|
|
}
|
|
|
|
/// Gibt zurück, ob Undo verfügbar ist.
|
|
pub fn can_undo(&self) -> bool {
|
|
!self.undo_stack.is_empty()
|
|
}
|
|
|
|
/// Gibt zurück, ob Redo verfügbar ist.
|
|
pub fn can_redo(&self) -> bool {
|
|
!self.redo_stack.is_empty()
|
|
}
|
|
|
|
/// Führt Undo auf der Liste der Striche aus.
|
|
///
|
|
/// Gibt `true` zurück, wenn sich die Strichliste geändert hat.
|
|
pub fn undo(&mut self, strokes: &mut Vec<Stroke>) -> bool {
|
|
let Some(action) = self.undo_stack.pop() else {
|
|
return false;
|
|
};
|
|
|
|
match action {
|
|
HistoryAction::AddStroke(stroke) => {
|
|
// Den hinzugefügten Strich aus der Seite entfernen
|
|
if let Some(pos) = strokes.iter().position(|s| s.id == stroke.id) {
|
|
strokes.remove(pos);
|
|
}
|
|
self.redo_stack.push(HistoryAction::AddStroke(stroke));
|
|
}
|
|
HistoryAction::AddStrokes(added) => {
|
|
let ids: Vec<u64> = added.iter().map(|s| s.id).collect();
|
|
strokes.retain(|s| !ids.contains(&s.id));
|
|
self.redo_stack.push(HistoryAction::AddStrokes(added));
|
|
}
|
|
HistoryAction::RemoveStrokes(removed) => {
|
|
// Die gelöschten Striche wiederherstellen
|
|
for s in &removed {
|
|
strokes.push(s.clone());
|
|
}
|
|
self.redo_stack.push(HistoryAction::RemoveStrokes(removed));
|
|
}
|
|
HistoryAction::ReplaceStroke { old, new } => {
|
|
if let Some(pos) = strokes.iter().position(|s| s.id == new.id) {
|
|
strokes[pos] = old.clone();
|
|
}
|
|
self.redo_stack
|
|
.push(HistoryAction::ReplaceStroke { old, new });
|
|
}
|
|
HistoryAction::ModifyStrokes { before, after } => {
|
|
for b in &before {
|
|
if let Some(pos) = strokes.iter().position(|s| s.id == b.id) {
|
|
strokes[pos] = b.clone();
|
|
}
|
|
}
|
|
self.redo_stack
|
|
.push(HistoryAction::ModifyStrokes { before, after });
|
|
}
|
|
HistoryAction::SplitStrokes { removed, added, at } => {
|
|
strokes.retain(|s| !added.iter().any(|a| a.id == s.id));
|
|
strokes.extend(removed.iter().cloned());
|
|
self.redo_stack
|
|
.push(HistoryAction::SplitStrokes { removed, added, at });
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Führt Redo auf der Liste der Striche aus.
|
|
///
|
|
/// Gibt `true` zurück, wenn sich die Strichliste geändert hat.
|
|
pub fn redo(&mut self, strokes: &mut Vec<Stroke>) -> bool {
|
|
let Some(action) = self.redo_stack.pop() else {
|
|
return false;
|
|
};
|
|
|
|
match action {
|
|
HistoryAction::AddStroke(stroke) => {
|
|
// Strich erneut hinzufügen
|
|
strokes.push(stroke.clone());
|
|
self.undo_stack.push(HistoryAction::AddStroke(stroke));
|
|
}
|
|
HistoryAction::AddStrokes(added) => {
|
|
strokes.extend(added.clone());
|
|
self.undo_stack.push(HistoryAction::AddStrokes(added));
|
|
}
|
|
HistoryAction::RemoveStrokes(removed) => {
|
|
// Striche erneut entfernen
|
|
let ids: Vec<u64> = removed.iter().map(|s| s.id).collect();
|
|
strokes.retain(|s| !ids.contains(&s.id));
|
|
self.undo_stack.push(HistoryAction::RemoveStrokes(removed));
|
|
}
|
|
HistoryAction::ReplaceStroke { old, new } => {
|
|
if let Some(pos) = strokes.iter().position(|s| s.id == old.id) {
|
|
strokes[pos] = new.clone();
|
|
}
|
|
self.undo_stack
|
|
.push(HistoryAction::ReplaceStroke { old, new });
|
|
}
|
|
HistoryAction::ModifyStrokes { before, after } => {
|
|
for a in &after {
|
|
if let Some(pos) = strokes.iter().position(|s| s.id == a.id) {
|
|
strokes[pos] = a.clone();
|
|
}
|
|
}
|
|
self.undo_stack
|
|
.push(HistoryAction::ModifyStrokes { before, after });
|
|
}
|
|
HistoryAction::SplitStrokes { removed, added, at } => {
|
|
strokes.retain(|s| !removed.iter().any(|r| r.id == s.id));
|
|
strokes.extend(added.iter().cloned());
|
|
self.undo_stack
|
|
.push(HistoryAction::SplitStrokes { removed, added, at });
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Setzt den History-Stack zurück.
|
|
pub fn clear(&mut self) {
|
|
self.undo_stack.clear();
|
|
self.redo_stack.clear();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::document::StylusSample;
|
|
|
|
fn make_dummy_stroke(id: u64) -> Stroke {
|
|
Stroke::new(
|
|
id,
|
|
vec![StylusSample {
|
|
position: glam::Vec2::new(0.0, 0.0),
|
|
pressure: 0.5,
|
|
timestamp: 0.0,
|
|
}],
|
|
Vec::new(),
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_undo_redo_add_stroke() {
|
|
let mut history = HistoryStack::new();
|
|
let mut strokes = Vec::new();
|
|
|
|
let s1 = make_dummy_stroke(1);
|
|
strokes.push(s1.clone());
|
|
history.push(HistoryAction::AddStroke(s1));
|
|
|
|
assert_eq!(strokes.len(), 1);
|
|
assert!(history.can_undo());
|
|
assert!(!history.can_redo());
|
|
|
|
// Undo
|
|
assert!(history.undo(&mut strokes));
|
|
assert_eq!(strokes.len(), 0);
|
|
assert!(!history.can_undo());
|
|
assert!(history.can_redo());
|
|
|
|
// Redo
|
|
assert!(history.redo(&mut strokes));
|
|
assert_eq!(strokes.len(), 1);
|
|
assert_eq!(strokes[0].id, 1);
|
|
assert!(history.can_undo());
|
|
assert!(!history.can_redo());
|
|
}
|
|
|
|
#[test]
|
|
fn test_undo_redo_remove_strokes() {
|
|
let mut history = HistoryStack::new();
|
|
let mut strokes = vec![make_dummy_stroke(1), make_dummy_stroke(2)];
|
|
|
|
// Löschen von Stroke 2
|
|
let removed = vec![strokes.remove(1)];
|
|
history.push(HistoryAction::RemoveStrokes(removed));
|
|
|
|
assert_eq!(strokes.len(), 1);
|
|
|
|
// Undo: Stroke 2 wird wiederhergestellt
|
|
assert!(history.undo(&mut strokes));
|
|
assert_eq!(strokes.len(), 2);
|
|
|
|
// Redo: Stroke 2 wird wieder entfernt
|
|
assert!(history.redo(&mut strokes));
|
|
assert_eq!(strokes.len(), 1);
|
|
assert_eq!(strokes[0].id, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_undo_redo_replace_stroke() {
|
|
let mut history = HistoryStack::new();
|
|
let s_old = make_dummy_stroke(1);
|
|
let mut s_new = make_dummy_stroke(1);
|
|
s_new.color = [1.0, 0.0, 0.0, 1.0]; // Modifizierter Strich (z. B. erkannte Form)
|
|
|
|
let mut strokes = vec![s_old.clone()];
|
|
strokes[0] = s_new.clone();
|
|
history.push(HistoryAction::ReplaceStroke {
|
|
old: s_old.clone(),
|
|
new: s_new.clone(),
|
|
});
|
|
|
|
assert_eq!(strokes[0].color, [1.0, 0.0, 0.0, 1.0]);
|
|
|
|
// Undo: stellt den alten Strich wieder her
|
|
assert!(history.undo(&mut strokes));
|
|
assert_eq!(strokes[0].color, s_old.color);
|
|
|
|
// Redo: setzt den neuen Strich wieder ein
|
|
assert!(history.redo(&mut strokes));
|
|
assert_eq!(strokes[0].color, [1.0, 0.0, 0.0, 1.0]);
|
|
}
|
|
}
|