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 { ElementGroup } from '../tools/modification/GroupTool';
|
||||
import type { BackgroundConfig } from '../services/backgroundService';
|
||||
import type { LayoutDefinition } from '../types/cad.types';
|
||||
|
||||
export interface YjsDocumentData {
|
||||
elements: Y.Map<CADElement>;
|
||||
@@ -14,6 +15,7 @@ export interface YjsDocumentData {
|
||||
meta: Y.Map<unknown>;
|
||||
groups: Y.Map<ElementGroup>;
|
||||
bgConfig: Y.Map<BackgroundConfig>;
|
||||
layouts: Y.Map<LayoutDefinition>; // Task G1
|
||||
}
|
||||
|
||||
export class YjsDocument {
|
||||
@@ -29,6 +31,7 @@ export class YjsDocument {
|
||||
meta: this.doc.getMap<unknown>('meta'),
|
||||
groups: this.doc.getMap<ElementGroup>('groups'),
|
||||
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());
|
||||
}
|
||||
|
||||
/** Get all layouts as a plain array (Task G1) */
|
||||
getLayouts(): LayoutDefinition[] {
|
||||
return Array.from(this.data.layouts.values());
|
||||
}
|
||||
|
||||
/** Add or update a single element */
|
||||
setElement(el: CADElement): void {
|
||||
this.doc.transact(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
import * as Y from 'yjs';
|
||||
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 {
|
||||
private readonly ydoc: YjsDocument;
|
||||
@@ -22,10 +22,10 @@ export class CADDocument {
|
||||
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.
|
||||
// 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.undoManager = new Y.UndoManager([this.ydoc.data.elements, this.ydoc.data.layers, this.ydoc.data.layouts], {
|
||||
captureTimeout: 0,
|
||||
});
|
||||
}
|
||||
@@ -52,6 +52,53 @@ export class CADDocument {
|
||||
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). */
|
||||
|
||||
@@ -72,6 +72,23 @@ export interface BlockDefinition {
|
||||
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 {
|
||||
minX: 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