Files
DOSSIER-STANDALONE/src/state/historySlice.ts
T
karim 44942e6976 Undo/Redo für Projekt-Änderungen
setProject wird jetzt von einer History gewrappt (undoStack/redoStack,
Limit 100). Echte Aenderungen (Referenzvergleich) landen auf dem
Undo-Stack, eine neue Aktion leert den Redo-Stack. Hochfrequente
Drag-Mutatoren (Griffe/Body-Move) bekommen einen coalesceKey, damit
ein Drag nicht hunderte Einzelschritte erzeugt. Tastatur-Bindung
(Ctrl+Z/Ctrl+Shift+Z/Ctrl+Y) folgt im TopBar-Commit (App.tsx).
2026-07-04 05:01:15 +02:00

108 lines
4.1 KiB
TypeScript

// History-Slice: Undo/Redo für das Projekt-Modell. Reine Snapshot-Historie —
// dank immutabler Updates in `projectSlice.ts` (jede echte Änderung liefert
// eine NEUE `Project`-Referenz mit structural sharing) ist ein Stack aus
// `Project`-Werten billig (kein Deep-Clone nötig, unveränderte Teilbäume
// werden zwischen den Snapshots geteilt).
//
// Die eigentliche Buchführung (WANN wird gepusht, Koaleszenz von Drag-Serien)
// lebt bewusst in `projectSlice.ts` bei `setProject` selbst — das ist die
// EINZIGE Stelle, durch die jede Projekt-Mutation läuft. Dieses Modul liefert
// nur den reaktiven Zustand (Stacks + Flags) und die `undo`/`redo`-Aktionen,
// die DIREKT über `set()` schreiben (NICHT über `setProject`), damit sie
// keinen neuen History-Eintrag erzeugen (sonst Endlosschleife/kaputte
// Historie). Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import type { Project } from "../model/types";
import type { StoreApi } from "./store";
/** Obergrenze der Undo-Tiefe — ältere Einträge fallen hinten raus (Speicher/Performance). */
export const HISTORY_LIMIT = 100;
/** Felder, die diese Slice in den RootState beisteuert. */
export interface HistorySlice {
/** Ältere Projekt-Stände (oben = zuletzt verlassener Stand, für `undo`). */
undoStack: Project[];
/** Durch `undo` verlassene Stände (oben = zuletzt verlassener, für `redo`). */
redoStack: Project[];
/** Abgeleitet aus `undoStack.length > 0` — bequem für UI-Bindings (Buttons/Menüs). */
canUndo: boolean;
/** Abgeleitet aus `redoStack.length > 0`. */
canRedo: boolean;
/** Einen Schritt zurück. No-Op (kein Crash), wenn der Undo-Stack leer ist. */
undo: () => void;
/** Einen Schritt vor. No-Op (kein Crash), wenn der Redo-Stack leer ist. */
redo: () => void;
}
/**
* Reines Patch-Berechnen für einen History-Push — von `setProject` (in
* `projectSlice.ts`) bei jeder ECHTEN Projekt-Änderung aufgerufen (kein
* Store-Zugriff nötig, daher hier als freie Funktion statt Store-Aktion: so
* lässt sich der Push in EINEM gemeinsamen `set()`-Aufruf mit der eigentlichen
* `project`-Änderung zusammenfassen, statt einen zweiten, verschachtelten
* `set()`-Aufruf auszulösen).
*
* Eine neue Aktion verwirft immer die Redo-Historie (Standardverhalten jedes
* Undo/Redo-Systems: sobald der Nutzer nach einem `undo()` etwas Neues tut,
* ist der alte "vordere" Zweig weg).
*/
export function pushHistoryPatch(
undoStack: Project[],
prevProject: Project,
): Pick<HistorySlice, "undoStack" | "redoStack" | "canUndo" | "canRedo"> {
const next = [...undoStack, prevProject];
if (next.length > HISTORY_LIMIT) next.shift();
return { undoStack: next, redoStack: [], canUndo: true, canRedo: false };
}
/**
* Baut `undo`/`redo` + initialen Zustand. `onAfterJump` wird nach jedem
* Sprung aufgerufen (projectSlice.ts nutzt das, um die Koaleszenz-Buchführung
* für Drag-Serien zurückzusetzen — sonst könnte eine neue Aktion direkt nach
* einem `undo()` fälschlich mit der VORHERIGEN Serie verschmolzen und dadurch
* NIE auf dem Undo-Stack landen).
*/
export function createHistorySlice(
api: StoreApi<HistorySlice & { project: Project }>,
onAfterJump?: () => void,
): HistorySlice {
const { set, get } = api;
return {
undoStack: [],
redoStack: [],
canUndo: false,
canRedo: false,
undo: () => {
const { undoStack, redoStack, project } = get();
if (undoStack.length === 0) return;
const prev = undoStack[undoStack.length - 1];
const rest = undoStack.slice(0, -1);
set({
project: prev,
undoStack: rest,
redoStack: [...redoStack, project],
canUndo: rest.length > 0,
canRedo: true,
});
onAfterJump?.();
},
redo: () => {
const { undoStack, redoStack, project } = get();
if (redoStack.length === 0) return;
const next = redoStack[redoStack.length - 1];
const rest = redoStack.slice(0, -1);
set({
project: next,
undoStack: [...undoStack, project],
redoStack: rest,
canUndo: true,
canRedo: rest.length > 0,
});
onAfterJump?.();
},
};
}