task(G1): layout data model - LayoutDefinition type, Yjs layouts map, undo-tracked CRUD in CADDocument
This commit is contained in:
@@ -6,6 +6,7 @@ import * as Y from 'yjs';
|
|||||||
import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types';
|
import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types';
|
||||||
import type { ElementGroup } from '../tools/modification/GroupTool';
|
import type { ElementGroup } from '../tools/modification/GroupTool';
|
||||||
import type { BackgroundConfig } from '../services/backgroundService';
|
import type { BackgroundConfig } from '../services/backgroundService';
|
||||||
|
import type { LayoutDefinition } from '../types/cad.types';
|
||||||
|
|
||||||
export interface YjsDocumentData {
|
export interface YjsDocumentData {
|
||||||
elements: Y.Map<CADElement>;
|
elements: Y.Map<CADElement>;
|
||||||
@@ -14,6 +15,7 @@ export interface YjsDocumentData {
|
|||||||
meta: Y.Map<unknown>;
|
meta: Y.Map<unknown>;
|
||||||
groups: Y.Map<ElementGroup>;
|
groups: Y.Map<ElementGroup>;
|
||||||
bgConfig: Y.Map<BackgroundConfig>;
|
bgConfig: Y.Map<BackgroundConfig>;
|
||||||
|
layouts: Y.Map<LayoutDefinition>; // Task G1
|
||||||
}
|
}
|
||||||
|
|
||||||
export class YjsDocument {
|
export class YjsDocument {
|
||||||
@@ -29,6 +31,7 @@ export class YjsDocument {
|
|||||||
meta: this.doc.getMap<unknown>('meta'),
|
meta: this.doc.getMap<unknown>('meta'),
|
||||||
groups: this.doc.getMap<ElementGroup>('groups'),
|
groups: this.doc.getMap<ElementGroup>('groups'),
|
||||||
bgConfig: this.doc.getMap<BackgroundConfig>('bgConfig'),
|
bgConfig: this.doc.getMap<BackgroundConfig>('bgConfig'),
|
||||||
|
layouts: this.doc.getMap<LayoutDefinition>('layouts'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +60,11 @@ export class YjsDocument {
|
|||||||
return Array.from(this.data.bgConfig.values());
|
return Array.from(this.data.bgConfig.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get all layouts as a plain array (Task G1) */
|
||||||
|
getLayouts(): LayoutDefinition[] {
|
||||||
|
return Array.from(this.data.layouts.values());
|
||||||
|
}
|
||||||
|
|
||||||
/** Add or update a single element */
|
/** Add or update a single element */
|
||||||
setElement(el: CADElement): void {
|
setElement(el: CADElement): void {
|
||||||
this.doc.transact(() => {
|
this.doc.transact(() => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { YjsDocument } from '../../crdt/YjsDocument';
|
import { YjsDocument } from '../../crdt/YjsDocument';
|
||||||
import type { CADElement, CADLayer } from '../../types/cad.types';
|
import type { CADElement, CADLayer, LayoutDefinition } from '../../types/cad.types';
|
||||||
|
|
||||||
export class CADDocument {
|
export class CADDocument {
|
||||||
private readonly ydoc: YjsDocument;
|
private readonly ydoc: YjsDocument;
|
||||||
@@ -22,10 +22,10 @@ export class CADDocument {
|
|||||||
constructor(ydoc: YjsDocument) {
|
constructor(ydoc: YjsDocument) {
|
||||||
this.ydoc = ydoc;
|
this.ydoc = ydoc;
|
||||||
// Trackt alle Operationen auf Elements + Layers → gemeinsame Undo-Historie.
|
// Trackt alle Operationen auf Elements + Layers → gemeinsame Undo-Historie.
|
||||||
// Blocks/Groups/Configs bewusst NICHT tracken: Werkzeuge ändern primär Elemente/Layer.
|
// Blocks/Groups/Configs bewusst NICHT tracken; Layouts (G1) dazu seit Task G1.
|
||||||
// captureTimeout 0: KEIN automatisches Mergen zeitnaher Transaktionen —
|
// captureTimeout 0: KEIN automatisches Mergen zeitnaher Transaktionen —
|
||||||
// jede explizite transact() bleibt EIGENER Undo-Schritt (deterministisch für Tools).
|
// 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.undoManager = new Y.UndoManager([this.ydoc.data.elements, this.ydoc.data.layers, this.ydoc.data.layouts], {
|
||||||
captureTimeout: 0,
|
captureTimeout: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,53 @@ export class CADDocument {
|
|||||||
return Array.from(this.ydoc.data.layers.values());
|
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 ────────────────────────────────────────────
|
// ── Schreiben ────────────────────────────────────────────
|
||||||
|
|
||||||
/** Fügt ein Element hinzu (einzelner Aufruf = eigener Undo-Schritt). */
|
/** Fügt ein Element hinzu (einzelner Aufruf = eigener Undo-Schritt). */
|
||||||
|
|||||||
@@ -72,6 +72,23 @@ export interface BlockDefinition {
|
|||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout (Task G1): Papier-/Plot-Seite mit eigenem Viewport aufs Modell.
|
||||||
|
* Persistiert in der Yjs-Map 'layouts' (key = layoutId).
|
||||||
|
*/
|
||||||
|
export interface LayoutViewport {
|
||||||
|
center: { x: number; y: number };
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LayoutDefinition {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
viewport: LayoutViewport;
|
||||||
|
/** Referenz auf den Titelblock-Block (frame block), optional. */
|
||||||
|
frameBlockRef?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BoundingBox {
|
export interface BoundingBox {
|
||||||
minX: number;
|
minX: number;
|
||||||
minY: number;
|
minY: number;
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Task G1 – Layout-Datenmodell: Yjs Map layoutId → LayoutDefinition.
|
||||||
|
*
|
||||||
|
* LayoutDefinition: { id, name, viewport: { center: {x,y}, scale },
|
||||||
|
* frameBlockRef? }. CRUD über CADDocument, undo-getrackt, Persistence
|
||||||
|
* läuft automatisch über die Yjs-Doc-Serialisierung (drawing json).
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||||
|
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||||
|
|
||||||
|
function mkDoc(): CADDocument {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
return new CADDocument(ydoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAYOUT = {
|
||||||
|
id: 'layout-1',
|
||||||
|
name: 'A4 Plan',
|
||||||
|
viewport: { center: { x: 100, y: 50 }, scale: 0.5 },
|
||||||
|
frameBlockRef: 'frame-a4',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('G1: Layout-CRUD', () => {
|
||||||
|
it('addLayout + getLayout: Layout landet mit allen Feldern in der Map', () => {
|
||||||
|
const doc = mkDoc();
|
||||||
|
doc.addLayout(LAYOUT);
|
||||||
|
const loaded = doc.getLayout('layout-1');
|
||||||
|
expect(loaded).toBeDefined();
|
||||||
|
expect(loaded!.name).toBe('A4 Plan');
|
||||||
|
expect(loaded!.viewport.center.x).toBe(100);
|
||||||
|
expect(loaded!.viewport.center.y).toBe(50);
|
||||||
|
expect(loaded!.viewport.scale).toBe(0.5);
|
||||||
|
expect(loaded!.frameBlockRef).toBe('frame-a4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAllLayouts liefert Array; leer ohne Layouts', () => {
|
||||||
|
const doc = mkDoc();
|
||||||
|
expect(doc.getAllLayouts()).toHaveLength(0);
|
||||||
|
doc.addLayout(LAYOUT);
|
||||||
|
doc.addLayout({ ...LAYOUT, id: 'layout-2', name: 'A3 Plan' });
|
||||||
|
const all = doc.getAllLayouts();
|
||||||
|
expect(all).toHaveLength(2);
|
||||||
|
expect(all.map((l) => l.name)).toContain('A3 Plan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updateLayout ändert name und viewport shallow (id/struktur bleibt)', () => {
|
||||||
|
const doc = mkDoc();
|
||||||
|
doc.addLayout(LAYOUT);
|
||||||
|
doc.updateLayout('layout-1', { name: 'A4 geändert', viewport: { center: { x: 0, y: 0 }, scale: 2 } });
|
||||||
|
const loaded = doc.getLayout('layout-1');
|
||||||
|
expect(loaded!.name).toBe('A4 geändert');
|
||||||
|
expect(loaded!.viewport.scale).toBe(2);
|
||||||
|
expect(loaded!.frameBlockRef).toBe('frame-a4'); // unverändert erhalten
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deleteLayout entfernt Layout still (unbekannte ID kein Fehler)', () => {
|
||||||
|
const doc = mkDoc();
|
||||||
|
doc.addLayout(LAYOUT);
|
||||||
|
doc.deleteLayout('layout-1');
|
||||||
|
expect(doc.getLayout('layout-1')).toBeUndefined();
|
||||||
|
expect(() => doc.deleteLayout('gibts-nicht')).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('G1: Undo-Integration', () => {
|
||||||
|
it('Undo entfernt hinzugefügtes Layout wieder', () => {
|
||||||
|
const doc = mkDoc();
|
||||||
|
doc.addLayout(LAYOUT);
|
||||||
|
expect(doc.getLayout('layout-1')).toBeDefined();
|
||||||
|
doc.undo();
|
||||||
|
expect(doc.getLayout('layout-1')).toBeUndefined();
|
||||||
|
doc.redo();
|
||||||
|
expect(doc.getLayout('layout-1')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Layout-Undo ist EIN Schritt (nicht mit Element-Transaktion vermischt)', () => {
|
||||||
|
const doc = mkDoc();
|
||||||
|
doc.addLayout(LAYOUT);
|
||||||
|
doc.deleteLayout('layout-1');
|
||||||
|
// Zwei Schritte: undo stellt Löschung wieder her
|
||||||
|
doc.undo();
|
||||||
|
expect(doc.getLayout('layout-1')).toBeDefined();
|
||||||
|
doc.undo();
|
||||||
|
expect(doc.getLayout('layout-1')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user