/** * CADDocument – Fassade über YjsDocument (Task A2b). * * - Elementar-CRUD auf CADElement/CADLayer * - `transact(fn)`: Bündelt Änderungen zu EINEM Undo-Schritt * (kollaborativ korrekt via Y.UndoManager) * - `undo()/redo()` wrappen den Manager; HistoryPanel liest später daraus (A4) * - Keine Tool-Logik! Nur Dokument-Operationen. * * Nutzung künftiger V2-Werkzeuge (ab A3): * doc.transact(() => doc.addElement(line)); // 1 Klick = 1 Undo-Schritt */ import * as Y from 'yjs'; import { YjsDocument } from '../../crdt/YjsDocument'; import type { CADElement, CADLayer, LayoutDefinition } from '../../types/cad.types'; export class CADDocument { private readonly ydoc: YjsDocument; private readonly undoManager: Y.UndoManager; /** @param ydoc Netzwerkfrei erstellbar (createInMemoryDoc) oder CRDT-verbunden. */ constructor(ydoc: YjsDocument) { this.ydoc = ydoc; // Trackt alle Operationen auf Elements + Layers → gemeinsame Undo-Historie. // Blocks/Groups/Configs bewusst NICHT tracken; Layouts (G1) dazu seit Task G1. // captureTimeout 0: KEIN automatisches Mergen zeitnaher Transaktionen — // jede explizite transact() bleibt EIGENER Undo-Schritt (deterministisch für Tools). this.undoManager = new Y.UndoManager([this.ydoc.data.elements, this.ydoc.data.layers, this.ydoc.data.layouts], { captureTimeout: 0, }); } // ── Lesen ──────────────────────────────────────────────── /** Ein Element per ID (oder undefined). */ getElement(id: string): CADElement | undefined { return this.ydoc.data.elements.get(id); } /** Alle Elemente als Array. */ getAllElements(): CADElement[] { return Array.from(this.ydoc.data.elements.values()); } /** Ein Layer per ID (oder undefined). */ getLayer(id: string): CADLayer | undefined { return this.ydoc.data.layers.get(id); } /** Alle Layer als Array. */ getAllLayers(): CADLayer[] { return Array.from(this.ydoc.data.layers.values()); } // ── Layouts (Task G1) ──────────────────────────────────── /** Ein Layout per ID (oder undefined). */ getLayout(id: string): LayoutDefinition | undefined { return this.ydoc.data.layouts.get(id); } /** Alle Layouts als Array. */ getAllLayouts(): LayoutDefinition[] { return Array.from(this.ydoc.data.layouts.values()); } /** Fügt ein Layout hinzu (eigener Undo-Schritt). */ addLayout(layout: LayoutDefinition): void { this.transact(() => { this.ydoc.data.layouts.set(layout.id, layout); }); } /** * Aktualisiert ein Layout: Top-Level-Felder ersetzt, viewport SHALLOW gemergt * (center/scale einzeln änderbar). @throws wenn Layout fehlt. */ updateLayout(id: string, patch: Partial>): void { const existing = this.ydoc.data.layouts.get(id); if (!existing) throw new Error(`updateLayout: Layout '${id}' nicht gefunden`); const merged: LayoutDefinition = { ...existing, ...patch, viewport: patch.viewport ? { ...existing.viewport, ...patch.viewport, center: { ...existing.viewport.center, ...patch.viewport.center } } : existing.viewport, }; this.transact(() => { this.ydoc.data.layouts.set(id, merged); }); } /** Entfernt ein Layout (still bei unbekannter ID, eigener Undo-Schritt). */ deleteLayout(id: string): void { this.transact(() => { if (this.ydoc.data.layouts.has(id)) { this.ydoc.data.layouts.delete(id); } }); } // ── Schreiben ──────────────────────────────────────────── /** Fügt ein Element hinzu (einzelner Aufruf = eigener Undo-Schritt). */ addElement(el: CADElement): void { this.transact(() => { this.ydoc.data.elements.set(el.id, el); }); } /** * Aktualisiert ein Element: Top-Level-Felder werden ersetzt, * `patch.properties` wird SHALLOW in bestehende Properties gemergt * (Details wie fill/rotation einzeln änderbar, ohne den Rest zu verlieren). * @throws wenn Element nicht existiert */ updateElement(id: string, patch: Partial): void { const existing = this.ydoc.data.elements.get(id); if (!existing) throw new Error(`updateElement: Element '${id}' nicht gefunden`); const { properties, ...rest } = patch; const merged: CADElement = { ...existing, ...rest, properties: properties ? { ...existing.properties, ...properties } : existing.properties, }; this.transact(() => { this.ydoc.data.elements.set(id, merged); }); } /** Entfernt mehrere Elemente in EINER Transaktion. Ignoriert unbekannte IDs still. */ deleteElements(ids: string[]): void { this.transact(() => { for (const id of ids) { if (this.ydoc.data.elements.has(id)) { this.ydoc.data.elements.delete(id); } } }); } // ── Transaktion & Undo ─────────────────────────────────── /** Führt fn aus; ALLE darin getätigten Y-Änderungen werden EIN Undo-Schritt. */ transact(fn: () => void): void { this.ydoc.doc.transact(fn); } /** Macht den letzten Schritt rückgängig. false wenn nichts da. */ undo(): boolean { if (!this.canUndo()) return false; this.undoManager.undo(); return true; } /** Stellt einen Rückgängig-Schritt wieder her. false wenn nichts da. */ redo(): boolean { if (!this.canRedo()) return false; this.undoManager.redo(); return true; } /** Gibt es etwas zum Rückgängigmachen? */ canUndo(): boolean { return this.undoManager.undoStack.length > 0; } /** Gibt es etwas zum Wiederherstellen? */ canRedo(): boolean { return this.undoManager.redoStack.length > 0; } /** Exponiert den Manager (HistoryPanel liest Stacklängen für die UI, A4). */ getUndoManager(): Y.UndoManager { return this.undoManager; } // ── Subscriptions ──────────────────────────────────────── /** Benachrichtigung bei JEDEM Doc-Update (inkl. Remote über CRDT). */ onChanged(cb: () => void): () => void { return this.ydoc.onChange(cb); } }