feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+230
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
export { HistoryManager, getDefaultHistoryManager } from './HistoryManager';
export type { CADStateSnapshot, HistoryEntry, HistoryManagerOptions } from './HistoryManager';