185 lines
6.3 KiB
TypeScript
185 lines
6.3 KiB
TypeScript
/**
|
||
* 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<Omit<LayoutDefinition, 'id'>>): 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<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);
|
||
}
|
||
}
|