/** * 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): 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, 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; }