task(A2b): CADDocument facade with transactions and undo

This commit is contained in:
Agent Zero
2026-08-26 21:23:22 +02:00
parent 4a7a8f912f
commit ddfeadac58
2 changed files with 258 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
/**
* 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 } 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: Werkzeuge ändern primär Elemente/Layer.
// 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], {
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());
}
// ── 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<CADElement>): 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);
}
}