231 lines
6.3 KiB
TypeScript
231 lines
6.3 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|