feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { HistoryManager, getDefaultHistoryManager } from './HistoryManager';
|
||||
export type { CADStateSnapshot, HistoryEntry, HistoryManagerOptions } from './HistoryManager';
|
||||
Reference in New Issue
Block a user