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;
|
|||
|
|
}
|