Files
web-cad/frontend/src/history/HistoryManager.ts
T

231 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* HistoryManager Undo/Redo Stack mit beliebig vielen Schritten.
* Speichert Snapshots des kompletten CAD-Zustands (elements, layers, blocks, groups, bgConfig).
* F-CAD-09: Undo/Redo mit History
*/
import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types';
import type { ElementGroup } from '../tools/modification/GroupTool';
import type { BackgroundConfig } from '../services/backgroundService';
/** Vollständiger CAD-Zustand für einen History-Snapshot */
export interface CADStateSnapshot {
elements: CADElement[];
layers: CADLayer[];
blocks: BlockDefinition[];
groups: ElementGroup[];
bgConfig: BackgroundConfig | null;
timestamp: number;
label: string;
}
/** History-Eintrag für die Historie-Anzeige */
export interface HistoryEntry {
id: string;
label: string;
timestamp: number;
isCurrent: boolean;
}
export interface HistoryManagerOptions {
maxStackSize?: number;
}
const DEFAULT_MAX_SIZE = 100;
export class HistoryManager {
private undoStack: CADStateSnapshot[] = [];
private redoStack: CADStateSnapshot[] = [];
private currentState: CADStateSnapshot | null = null;
private maxStackSize: number;
private listeners: Set<() => void> = new Set();
private idCounter = 0;
constructor(options: HistoryManagerOptions = {}) {
this.maxStackSize = options.maxStackSize ?? DEFAULT_MAX_SIZE;
}
/**
* Initialen Zustand setzen (ohne Undo-Eintrag zu erzeugen).
* Wird beim Laden eines Projekts aufgerufen.
*/
initialize(snapshot: Omit<CADStateSnapshot, 'timestamp' | 'label'>): void {
this.currentState = {
...snapshot,
timestamp: Date.now(),
label: 'Initial',
};
this.undoStack = [];
this.redoStack = [];
this.notifyListeners();
}
/**
* Eine neue Operation aufzeichnen.
* Der aktuelle Zustand wird auf den Undo-Stack geschoben,
* der neue Zustand wird zum aktuellen.
* Der Redo-Stack wird geleert.
*/
pushSnapshot(snapshot: Omit<CADStateSnapshot, 'timestamp' | 'label'>, label: string): void {
if (this.currentState) {
this.undoStack.push(this.currentState);
if (this.undoStack.length > this.maxStackSize) {
this.undoStack.shift();
}
}
this.currentState = {
...snapshot,
timestamp: Date.now(),
label,
};
this.redoStack = [];
this.notifyListeners();
}
/**
* Undo: aktuellen Zustand auf Redo-Stack, letzten Undo-Eintrag holen.
* Gibt den vorherigen Zustand zurück oder null, wenn kein Undo möglich.
*/
undo(): CADStateSnapshot | null {
if (this.undoStack.length === 0 || !this.currentState) return null;
this.redoStack.push(this.currentState);
const previous = this.undoStack.pop()!;
this.currentState = previous;
this.notifyListeners();
return previous;
}
/**
* Redo: aktuellen Zustand auf Undo-Stack, nächsten Redo-Eintrag holen.
* Gibt den nächsten Zustand zurück oder null, wenn kein Redo möglich.
*/
redo(): CADStateSnapshot | null {
if (this.redoStack.length === 0 || !this.currentState) return null;
this.undoStack.push(this.currentState);
const next = this.redoStack.pop()!;
this.currentState = next;
this.notifyListeners();
return next;
}
/** Kann Undo ausgeführt werden? */
canUndo(): boolean {
return this.undoStack.length > 0;
}
/** Kann Redo ausgeführt werden? */
canRedo(): boolean {
return this.redoStack.length > 0;
}
/** Aktuellen Zustand zurückgeben */
getCurrentState(): CADStateSnapshot | null {
return this.currentState;
}
/**
* Historie als Liste zurückgeben (für UI-Anzeige).
* Neueste zuerst, mit isCurrent-Markierung.
*/
getHistory(): HistoryEntry[] {
const entries: HistoryEntry[] = [];
// Undo-Stack (älteste zuerst)
for (let i = 0; i < this.undoStack.length; i++) {
entries.push({
id: `undo-${i}`,
label: this.undoStack[i].label,
timestamp: this.undoStack[i].timestamp,
isCurrent: false,
});
}
// Aktueller Zustand
if (this.currentState) {
entries.push({
id: 'current',
label: this.currentState.label,
timestamp: this.currentState.timestamp,
isCurrent: true,
});
}
// Redo-Stack (neueste zuerst, also umgekehrt)
for (let i = this.redoStack.length - 1; i >= 0; i--) {
entries.push({
id: `redo-${i}`,
label: this.redoStack[i].label,
timestamp: this.redoStack[i].timestamp,
isCurrent: false,
});
}
return entries;
}
/** Anzahl der Undo-Schritte */
getUndoCount(): number {
return this.undoStack.length;
}
/** Anzahl der Redo-Schritte */
getRedoCount(): number {
return this.redoStack.length;
}
/**
* Zu einem bestimmten History-Eintrag springen.
* Macht mehrere Undo- oder Redo-Schritte auf einmal.
*/
jumpTo(entryId: string): CADStateSnapshot | null {
if (entryId === 'current') return this.currentState;
if (entryId.startsWith('undo-')) {
const idx = parseInt(entryId.substring(5), 10);
// Undo bis zu diesem Eintrag
let result: CADStateSnapshot | null = null;
for (let i = this.undoStack.length - 1; i >= idx; i--) {
result = this.undo();
}
return result;
}
if (entryId.startsWith('redo-')) {
const idx = parseInt(entryId.substring(5), 10);
let result: CADStateSnapshot | null = null;
for (let i = 0; i <= idx; i++) {
result = this.redo();
}
return result;
}
return null;
}
/** Alle History löschen und neu initialisieren */
clear(): void {
this.undoStack = [];
this.redoStack = [];
this.currentState = null;
this.notifyListeners();
}
/** Listener registrieren (für React-Updates) */
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notifyListeners(): void {
this.listeners.forEach((l) => l());
}
/** Eindeutige ID generieren */
private generateId(): string {
return `hist-${++this.idCounter}-${Date.now()}`;
}
}
/** Default HistoryManager-Instanz (Singleton) */
let defaultManager: HistoryManager | null = null;
export function getDefaultHistoryManager(): HistoryManager {
if (!defaultManager) {
defaultManager = new HistoryManager();
}
return defaultManager;
}