Compare commits

...

2 Commits

5 changed files with 166 additions and 6 deletions
+4 -3
View File
@@ -631,7 +631,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
- [x] F8 SVG-Import als Block (svg→Path→Polyline approximation, simple subset: path/rect/circle/ellipse/line/polygon) *(2026-08-29: voller Path-Parser M/L/H/V/C/S/Q/T/A/Z mit Bézier-Flattening + Arc-Endpoint→Center-Konvertierung, rect/circle-Konventions-Fixes, ellipse→64-Punkt-Polygon, BlockLibraryTree nutzt echte Geometrie-Payload; Log F8)*
## Phase G — Layouts & Plot
- [ ] G1 Datenmodell layouts (Yjs Map: layoutId→{name, viewport{center,scale}, frameBlockRef}), Backend persistence folgt automatisch via drawing json
- [x] G1 Datenmodell layouts (Yjs Map: layoutId→{name, viewport{center,scale}, frameBlockRef}), Backend persistence folgt automatisch via drawing json *(2026-08-29: LayoutDefinition/LayoutViewport in cad.types, layouts-Map in YjsDocument (YjsDocumentData erweitert), CADDocument-CRUD getLayout/getAllLayouts/addLayout/updateLayout (shallow-merge)/deleteLayout, UndoManager um layouts getrackt; Log G1)*
- [ ] G2 LayoutBar UI + Viewport-Widget (zeigt Modellbereich skaliert, klickbar zoom-to)
- [ ] G3 Titelblock-Block mit Attributen (Projekt/Datum/Maßstab/Autor) auto-insert
- [ ] G4 PDF-Export je Layout im Ausgabemaßstab (pdf-lib; Linienbreiten aus Layern; monochrome Option)
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|---|---|---|---|---|---|
| 2026-08-29 | G1 | Layout-Datenmodell: LayoutViewport {center{x,y}, scale} + LayoutDefinition {id, name, viewport, frameBlockRef?} in cad.types.ts; YjsDocument.data um layouts-Map (YjsDocumentData-Interface additiv erweitert + getLayouts-Getter); CADDocument um Layout-CRUD erweitert: getLayout/getAllLayouts/addLayout/updateLayout (Top-Level ersetzt, viewport + center SHALLOW gemergt — center/scale einzeln änderbar, frameBlockRef bleibt bei reinem name-Patch erhalten)/deleteLayout (still bei unbekannter ID); Y.UndoManager um layouts-Map getrackt → jedes add/update/delete = eigener Undo-Schritt (2-Schritte-Undo-Test: add→delete→undo→undo verifiziert); Persistence läuft automatisch über bestehende drawing-json-Serialisierung (Y-Doc-Snapshot) | Frontend: 557/557 Tests (+6 layouts.test.ts), tsc 0, Build EXIT 0 | e750a45 | G1 COMPLETE — Datenmodell steht; nächster Task G2 (LayoutBar UI + Viewport-Widget) |
| 2026-08-29 | E7 | Event-Elemente-Polish: (1) ElementType um curtain/spotlight/barrier erweitert (cad.types.ts additiv); (2) Pixi-Drawer-Cases: curtain = wellige Polyline aus properties.points (24 Punkte, Sinus gedämpft, close=false; Fallback Linie), spotlight = Kegel-Polygon aus points (close=true, stroke=properties.color), barrier = Kreis-Paare aus properties.circles (12 Kreise, Fallback 5 entlang BBox); (3) Platzier-Tools via Klick: curtainTool (stageHeight-Option, Standard 120cm, Breite 400), spotlightTool (angle+color-Optionen, Länge 300, Öffnung ±20°, Grad-Winkel direkt — Konvention konsistent zu F6), barrierTool (length-Option, Standard 200, 6 Kettenglieder); (4) createStage um properties.height (Bühnenhöhe in cm) erweitert; event-tools v2.2.0, tools-Array jetzt 13 Einträge; Test-Helper fireDown auf etabliertes Pilot-Muster umgestellt (echtes CADDocument aus createInMemoryDoc statt Broken-Mock ohne transact — 7 Tests rot wegen Mock, nach Muster-Fix grün) | Frontend: 551/551 Tests (+8 eventPolish.test.ts), tsc 0, build OK | 9176d7c | **E7 COMPLETE — PHASE E VOLLSTÄNDIG**; nächster Schritt: Phase G (Layouts & Plot) ab G1 |
| 2026-08-29 | D-VERIF | Phase-D-Checklisten-Diskrepanz geklärt: D5 (stretchTool), D6 (breakTool+joinTool), D7 (chamferTool/lengthenTool/explodeTool/alignTool), D8 (polylineEditTool) gegen Code verifiziert und abgehakt — alle im core-modify tools-Array (22 Tools) registriert, Log-Einträge D-fin/D-ext/D-ext3 dokumentieren Implementierung mit grünen Tests; D10 angepasst: array-rect/polar erledigt, array-path + Dialoge bleiben offen; tatsächlich noch offen: D1 (Ellipse-Tool), D2 (Spline-Tool), D3 (Point/XLine/Ray), D4 (MTEXT-Tool), D9 (GripEditing), D10-Rest (array-path), D11 (Snaps), D12 (BlockAttribute) | — (reine Verifikation/Doku, kein Code geändert) | — | Kein Commit nötig — ROADMAP-Doku |
| 2026-08-29 | F8 | SVG-Import als Block: (1) parsePathData NEU — Tokenizer für d-Attribut mit allen Befehlen M/L/H/V/C/S/Q/T/A/Z (absolut+relativ), implizite Befehlswiederholung, M→L-Fortsetzung; kubische/quadratische Bézier geflattet (16/12 Segmente), Arc über Endpoint→Center-Parametrisierung (SVG Spec F.6.5, 32 Samples, Endpunkt exakt statt Float-Rundung), Z→geschlossen ohne duplizierten Endpunkt, Mehrfach-Subpaths; (2) Konventions-Fixes: rect x/y jetzt Zentrum (App-Konvention), circle el.x/el.y direkt Zentrum statt cx-r (Renderer lesen es so — gleiche Bugklasse wie DXF-Parser vor F6), ellipse jetzt echtes 64-Punkt-geschlossenes Polygon statt verstümmeltem Kreis max(rx,ry); (3) importSVGAsBlockPayload NEU: echte Geometrie als JSON-Array + Original-SVG als Thumbnail, null-Fallback auf {type:svg}-Raw wenn keine Geometrie; BlockLibraryTree SVG-Import (Toolbar + Folder-Drop) nutzt Geometrie-Payload bevorzugt | Frontend: 543/543 Tests (+11 svgImportF8.test.ts), tsc 0, build OK | 09948bc | **F8 COMPLETE — PHASE F VOLLSTÄNDIG ABGESCHLOSSEN** (Library & Import/Export: F1F8 alle grün); nächster Schritt: Phase-D-Checklisten-Diskrepanz klären, dann E7 |
@@ -732,5 +733,5 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
---
*Nächster offener Schritt: **G1** (Layout-Datenmodell: Yjs Map layoutId→{name, viewport{center,scale}, frameBlockRef}). Danach G2 LayoutBar+Viewport-Widget, G3 Titelblock, G4 PDF-Export, G5 Print-Stylesheet. Restlich offen: D1D4/D9/D10-path/D11/D12 (CAD-Zeichen-Tools), Phase H, Phase I.*
*Status 2026-08-29: Phasen B+A+C+D+E+F komplett (E7 Event-Polish: curtain/spotlight/barrier; F1F8 Library/Import/Export), BUG-1-Kollisionsklasse geschlossen. FE 551/551 Tests, BE 272/272, tsc/Build grün. Alle Commits auf Forgejo main.*
*Nächster offener Schritt: **G2** (LayoutBar UI + Viewport-Widget: zeigt Modellbereich skaliert, klickbar zoom-to). Danach: G3 Titelblock, G4 PDF-Export, G5 Print-Stylesheet.*
*Status 2026-08-29: Phasen B+A+C+D+E+F komplett, G1 fertig (Layout-Datenmodell), BUG-1-Kollisionsklasse geschlossen. FE 557/557 Tests, BE 272/272, tsc/Build grün. Alle Commits auf Forgejo main.*
+8
View File
@@ -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(() => {
+50 -3
View File
@@ -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). */
+17
View File
@@ -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;
+87
View File
@@ -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();
});
});