48 KiB
web-cad v2.0 — ROADMAP & ARBEITSANWEISUNGEN
Dies ist die Steuerdatei für den Umbau. Sie enthält Status, Zielarchitektur (Frontend + Backend), Regeln und exakte Arbeitsaufträge (Tasks). Bei jedem Fortschritt: Task-Status hier aktualisieren + Eintrag in Teil 7 (Fortschritts-Log).
Stand: 2026-08-26 · Basis: main @
126277a· Planer: Agent Zero · Umsetzung: Agent(en) beliebiger Stärke
TEIL 0 — Wie du als Agent dieses Dokument abarbeitest
Du (Agent) arbeitest IMMER GENAU EINEN Task gleichzeitig ab. Verfahre so:
- Nächsten Task finden: Gehe zu Teil 5. Nimm den ERSTEN Task mit
- [ ](offen), dessen „Voraussetzungen“ alle- [x]sind. Bearbeite Tasks niemals parallel oder in falscher Reihenfolge. - Task lesen: Lies den kompletten Task. Verstanden? Wenn nein: Frage den Nutzer, rate nicht.
- Ist-Zustand prüfen: Öffne alle unter „Dateien“ genannten bestehenden Dateien und lies sie (zumindest die relevanten Abschnitte), BEVOR du etwas änderst.
- Schritte ausführen: Folge „Schritte“ von oben nach unten. Führe nach jedem Schritt das dort genannte Verify-Kommando aus (falls angegeben).
- Fehlerfall: Wenn ein Schritt fehlschlägt: maximal 3 gezielte Fix-Versuche. Danach: Änderungen des Schritts zurücksetzen (
git checkout -- <datei>bzw.git restore), Task-Status aufBLOCKIERTsetzen und Blockgrund in Teil 7 loggen. Weiter mit einem anderen unabhängigen Task ist erlaubt, wenn dessen Voraussetzungen erfüllt sind. - Scope-Disziplin: Ändere NIEMALS Dateien oder Verhalten, die der Task nicht nennt. Keine „Verbesserungen am Rande“. Wenn dir während der Arbeit ein anderes Problem auffällt: notiere es in Teil 7 unter „Gefundene Nebenprobleme“, behebe es NICHT jetzt.
- Abschluss eines Tasks:
a. Führe ALLE Kommandos unter „Verify“ aus. Alles muss grün sein.
b. Commit:
git add -A && git commit -m "task(<TASK-ID>): <kurze beschreibung>". c. Setze in diesem Dokument- [ ]→- [x]mit Datum und Commit-Hash. d. Ergänze Teil 7 (Log): Datum, Task-ID, was getan wurde, Testergebnis, Commit-Hash, offene Punkte. - Commit-Disziplin: Ein Task = mindestens ein Commit. Nie halbfertigen Code committen, der
tscoder Tests bricht. - Nie tun: Dependencies entfernen, die du nicht selbst hinzugefügt hast · Datenbankschema ändern ohne Migrationsscript · Tests löschen oder überspringen (
skip,todo) um grün zu werden · Secrets committen.
TEIL 1 — Projektstatus & Baseline (Ist-Stand)
1.1 Was existiert (verifiziert am 2026-08-26)
| Bereich | Fakten |
|---|---|
| Repo | Forgejo Leopoldadmin/web-cad, Branch main, ~35.000 LOC |
| Frontend | React 18 + TS + Vite, Canvas 2D. src/ ~16.480 LOC. tsc --noEmit = Exit 0 |
| Tests FE | 16 Dateien, 380 Tests, alle grün (npx vitest run) |
| Backend | Fastify + SQLite (better-sqlite3), 13 Route-Module, Auth (bcrypt, 4 Rollen), y-leveldb |
| Tests BE | 17 Dateien, 255 Tests, alle grün |
| Kollaboration | Yjs CRDT + WebSocket + y-indexeddb (Offline) |
| Deployment | Docker Compose, Coolify, Forgejo CI vorhanden |
1.2 Bekannte Bugs (müssen in Phase B gefixt werden)
| ID | Ort | Bug |
|---|---|---|
| BUG-1 | frontend/src/services/seatingService.ts (~L220) |
rowId: row_${Date.now()} → ID-Kollisionen bei schneller Folge. Blöcke setzen gar keine rowId pro Reihe. countSeats(byRow) dadurch falsch. |
| BUG-2 | frontend/src/tools/modification/geometry.ts (~L303) |
findIntersection() kann nur Gerade×Gerade → Trim/Extend/Fillet scheitern still auf Kreis/Bogen/Polyline. |
| BUG-3 | frontend/src/interaction/index.ts (handleModifyDown, offset-Zweig) |
Offset nutzt nur Distanz, ignoriert Richtung des zweiten Klicks (Seite fehlt). |
| BUG-4 | frontend/src/canvas/RenderEngine.ts (drawBackground) |
Hintergrundbild wird bei jedem Frame neu geladen (img.onload = () => this.render()). Muss gecacht werden. |
1.3 Architektur-Schwächen (Motivation für Umbau)
| Schwäche | Ort | Folge |
|---|---|---|
| God-Component | App.tsx (1803 LOC, 40+ useState) |
Unwartbar, jede Mausbewegung (cursorPos in State) rendert GANZE App neu |
| Hart codierte Tools | interaction/index.ts (1157 LOC switch-case) |
Werkzeuge nicht als Plugins möglich, Ribbon statisch |
| Snapshot-History | history/HistoryManager.ts |
Ganzzustand-Undo, kollisionsanfällig in Kollaboration, keine Tool-Transaktionen |
| Direkte Engine-Kopplung | components/CanvasArea.tsx (477 LOC) |
Renderer nicht austauschbar |
| Renderer Canvas2D only | canvas/RenderEngine.ts (1196 LOC) |
50k+ Elemente unrealistisch |
| DXF mini | services/dxfParser.ts (186 LOC), dxfWriter.ts (214 LOC) |
Nur 7 Entity-Typen; ELLIPSE/SPLINE/MTEXT/HATCH/POINT fehlen |
1.4 Zielbild v2.0 (kurz)
Web-2D-CAD in Standard-CAD-Breite mit Event-Fokus. Alle Werkzeuge = Plugins. WebGL-Rendering. Library first-class, durch Plugin-Kataloge erweiterbar. Branchenneutral einsetzbar (Grundrisse, Gelände). Bedienmodi „Einfach“ und „Pro“. Frontend und Backend werden beide umgebaut.
TEIL 2 — ZIELARCHITEKTUR FRONTEND (ausgearbeitet)
2.1 Neue Ordnerstruktur (Ziel)
frontend/src/
├── kernel/ # Framework-frei, kein React-Import erlaubt!
│ ├── model/ # Typen + Fabriken: CADElement, CADLayer, BlockDef, ids.ts
│ ├── geometry/ # intersects.ts (Line/Circle/Arc/Poly), transform.ts, bbox.ts
│ ├── document/ # CADDocument: CRUD + Queries über Yjs-Daten
│ └── history/ # Transaktionen (Wrapper um Yjs UndoManager)
├── render/
│ ├── types.ts # RenderAdapter-Interface + Viewport/Style-Typen
│ ├── pixi/ # PixiJS-Implementierung
│ │ ├── PixiRenderer.ts # implements RenderAdapter
│ │ ├── ElementLayer.ts # Graphics/Sprites pro CAD-Layer
│ │ ├── OverlayLayer.ts # Selection, Snaps, Previews, Tool-Overlays
│ │ ├── GridRulerLayer.ts
│ │ └── atlas.ts # Texturatlas für chair/table/stage
│ └── mock/MockAdapter.ts # Headless-Adapter für Unit-Tests (zeichnet nichts, protokolliert Aufrufe)
├── interaction/
│ ├── dispatcher.ts # InteractionDispatcher: DOM-Events → aktives Tool
│ ├── session.ts # ToolSession-Zustandsmaschine (idle/drawing/modifying)
│ ├── pointer.ts # Event-Normalisierung (screen→world, Buttons, Modifier)
│ └── shortcuts.ts # Tastatur-Mapping
├── plugins/
│ ├── types.ts # API v2 (siehe 2.4)
│ ├── registry.ts # Registry v2
│ ├── context.ts # PluginContext-Fabrik (Permissions geprüft)
│ ├── builtin/
│ │ ├── core-drawing/index.ts
│ │ ├── core-modify/index.ts
│ │ ├── core-annotate/index.ts
│ │ ├── core-measure/index.ts
│ │ └── event-tools/index.ts
│ └── library/ # LibraryProviderExtension-Verwaltung
├── stores/ # zustand Stores (NEU, ersetzt großen Teil des App-State)
│ ├── docStore.ts # Bindung an Yjs: elements/layers/selection (Selektion NICHT in Yjs, pro User lokal)
│ ├── toolStore.ts # activeToolId, toolOptions-Werte
│ ├── viewStore.ts # zoom, pan, grid, ortho, snap, unit, scaleFactor
│ └── uiStore.ts # panels, modals, ribbonTab, mode: 'simple'|'pro'
├── ui/
│ ├── AppShell.tsx # NUR Layout (Grid aus Slots)
│ ├── canvas/CanvasHost.tsx # Mountet Kernel+Renderer+Dispatcher, sonst nichts
│ ├── ribbon/RibbonBar.tsx # dynamisch aus pluginRegistry.getToolExtensions()
│ ├── panels/… # LeftSidebar, RightSidebar, PropertiesPanel (liest toolOptions-Schema)
│ ├── dialogs/…
│ └── simple/… # Einfach-Modus Varianten
├── services/ # bleiben (api.ts, import/export, background, seating → wandert teils in plugins/builtin/event-tools/domain)
├── crdt/ # bleibt (YjsDocument etc.)
└── types/ # cad.types.ts bleibt Quelle der Wahrheit für Persistenz-Typen
Regel: kernel/** darf React nirgends importieren. render/pixi/** kennt keine React-Components. React kennt Renderer nur über CanvasHost.
2.2 State-Management (Entscheidung + Begründung)
Neu: zustand (klein, kein Provider-Boilerplate, selektierbar).
Gründe gegen Ist-Zustand:
cursorPosliegt heute in App-State → jede Mausbewegung re-rendert 1803-LOC-Tree. Neu: Cursor-Anzeige liest Position aus einem throttled store-Slice ODER direkt DOM-Update im CanvasHost (bevorzugt: StatusBar subscribed aufviewStore.cursormit Selektor; Update max 60/s).- Elemente liegen doppelt (React-State UND Yjs). Neu: Yjs ist alleinige Quelle der Wahrheit.
docStorespiegelt Yjs-Arrays in einen zustand-Slice (viayjs.observe), Components lesen daraus. Kein zweitessetElements.
Store-Verträge (exakt):
// stores/docStore.ts
interface DocSlice {
elements: Record<string, CADElement>; // id → element (Spiegel von Yjs Map)
elementOrder: string[]; // Render-/Auswahlreihenfolge
layers: CADLayer[];
selection: string[]; // lokale Selektion (nicht synchronisiert)
setSelection(ids: string[]): void;
}
// stores/toolStore.ts
interface ToolSlice {
activeToolId: string; // default 'select'
optionValues: Record<string, Record<string, unknown>>; // toolId → optionKey → value
setTool(id: string): void; // validiert gegen Registry
setOption(toolId: string, key: string, value: unknown): void;
}
2.3 Rendering-Pipeline (Ziel)
Frame-Zyklus (außerhalb React):
InteractionDispatchererhält DOM-Event → normalisiert (pointer.ts) → ruft Handler des aktiven Tools mit Weltkoordinaten.- Tool ändert Entweder: a)
document.transact(fn)(committed, erzeugt Undo-Eintrag) oder b)renderer.setPreview(el)(nur Optik). PixiRendererhält pro CAD-Layer einen Container. Bei Yjs-Transaktion: diff-basiertes Update (added/removed/changed Keys aus Yjs-Events), KEIN Full-Redraw.- Overlays (Selection/Snap/Preview) leben in eigenem Layer, immer neu zeichnbar ohne Element-Layer zu berühren.
- requestAnimationFrame-Loop rendert nur bei
dirty-Flag (kein Dauerloop).
Performance-Ziele (messbar in tests/PerformanceBenchmark.test.ts weiterführen):
- 10k Elemente: Interaktion < 50ms Latenz
- 50k Chairs (Atlas-instanziiert): ≥ 55fps bei Pan/Zoom auf Referenzhardware
- Erstzeichnung Dashboard→Canvas sichtbar < 2s
2.4 Plugin-API v2 (Vertrag, exakte Interfaces)
// plugins/types.ts (NEU, ersetzt Teile der alten API)
export interface ToolManifestPart {
id: string; label: string; icon: string; ribbonTab: string;
shortcut?: string; tags?: ('basic'|'pro')[]; // 'basic' = im Einfach-Modus sichtbar
}
export interface ToolPointerEvent { world: Pt; screen: Pt; button: number; shift: boolean; alt: boolean; ctrl: boolean; }
export interface ToolContext {
doc: CADDocument; // CRUD + transact()
renderer: RenderAdapter; // für Preview/Overlay
snap: SnapEngine; selection: SelectionEngine;
options: Record<string, unknown>; // aktuelle OptionValues
setStatus(msg: string): void; // Statuszeile
}
export interface ToolExtensionV2 {
manifest: ToolManifestPart & { description?: string };
optionsSchema: PropertyField[]; // existierender Typ wiederverwendet
handlers: {
down?(e: ToolPointerEvent, ctx: ToolContext): void;
move?(e: ToolPointerEvent, ctx: ToolContext): void;
up?(e: ToolPointerEvent, ctx: ToolContext): void;
key?(e: KeyboardEvent, ctx: ToolContext): boolean; // true = konsumiert
cancel?(ctx: ToolContext): void; // ESC
};
}
export interface PanelExtension { id: string; title: string; slot: 'left'|'right'; render(): HTMLElement; }
export interface LibraryProviderExtension {
id: string; label: string;
listFolders(): Promise<LibFolder[]>;
listBlocks(folderId: string): Promise<LibBlock[]>;
getBlock(id: string): Promise<LibBlock>;
}
export interface PluginV2 {
manifest: { id; name; version; author; description; category: 'tools'|'elements'|'library'|'import-export'|'theme'|'other';
enabledByDefault?: boolean; minAppVersion?: string; permissions?: Array<'canvas'|'network'|'storage'> };
tools?: ToolExtensionV2[];
commands?: CommandExtension[]; // existiert bereits, unverändert
elementTypes?: ElementTypeExtension[]; // bleibt, render optional + renderPixi später
panels?: PanelExtension[];
libraryProviders?: LibraryProviderExtension[];
onInit?(ctx: PluginContext): void; onActivate?(); onDeactivate?(); onDestroy?();
}
Migration Alt→Neu (wichtig!):
- Alte
ToolExtension {onActivate}bleibt ÜBERGANGSWEISE bestehen (Phase A kompatibel), wird in A3 migriert und DANACH entfernt. InteractionEngine.setTool(id)delegiert ab A2 ausschließlich an Registry-Tools.
2.5 Undo/Redo (Entscheidung)
Yjs Y.UndoManager verwenden, eigenes Snapshot-History entfernen (Phase A4).
- Eine Tool-Aktion = eine
doc.transact(...)= ein Undo-Schritt. Kollaborativ korrekt (undoed nur eigene Ops). HistoryPanelliestundoManager.undoStack.lengthetc.- Alte
HistoryManager-Tests (32) werden auf neue Transaktions-API umgeschrieben (Aufwand ~0,5 Tag, in A4 eingeplant).
2.6 Bedienmodi
uiStore.mode: 'simple'|'pro'. Default für neue Nutzer:simple.- Filter: Ribbon zeigt Tool nur, wenn
mode==='pro' || manifest.tags.includes('basic'). CommandLine/Layer-Tiefe nur pro. - Umschalter: Topbar-Menü + SettingsModal. Persistenz in usersettings (Backend, existierende settings-Route nutzen).
TEIL 3 — ZIELARCHITEKTUR BACKEND (Kurzfassung)
Bleibt Fastify + SQLite. Neuerungen:
- Tabellen:
plugins(id TEXT PK, version TEXT, enabled_default INT, installed_at TEXT);plugin_settings(plugin_id TEXT, scope TEXT, scope_id TEXT, json TEXT, PRIMARY KEY(plugin_id,scope,scope_id));library_folders(id TEXT PK, parent_id TEXT, name TEXT, owner_scope TEXT);library_blocks(id TEXT PK, folder_id TEXT, name TEXT, tags TEXT, thumbnail TEXT, attr_defs TEXT, payload TEXT, source TEXT);guests(id TEXT PK, project_id TEXT, name TEXT, seat_id TEXT NULL)(Phase E6, optional 2.1). - Routen neu:
/api/pluginsGET ·/api/plugins/:id/settingsGET/PUT ·/api/plugins/installPOST (Admin, ZIP) ·/api/library/folders|blocksCRUD ·/api/library/import|exportPOST. - WebSocket-Notify:
pluginStateChanged,libraryChangedBroadcast. - Migrationsmechanismus:
backend/src/database/migrations/NNNN_name.sql, Runner beim Boot (Schema-Versionstabelle). JEDE Schemaänderung mit Migrationsfile + Test.
TEIL 4 — REGELN (Definition of Done)
Ein Task ist nur dann fertig, wenn ALLSES gilt:
cd frontend && npx tsc --noEmit→ Exit 0.cd frontend && npx vitest run→ alle Tests grün (Anzahl darf nie sinken; neue Features bringen Tests).cd backend && npx vitest run→ alle grün (bei Backend-Tasks).- Keine neuen
console.log(Logger-Utilutils/logger.tsnutzen, in A0 anlegen). - Neue öffentliche Funktionen/Interfaces haben TSDoc-Kommentar (1–3 Zeilen).
- Commit-Format:
task(<ID>): <beschreibung>(Conventional reicht so). - Dieser File: Status gesetzt + Log-Eintrag.
Testkonventionen:
- Neue Kernel-Module: Vitest Unit-Tests OHNE DOM (
environment: nodegeht nicht global — lege pro Testfile// @vitest-environment nodefalls kein DOM nötig). - Renderer: Tests laufen gegen
MockAdapter(protokolliert draw*-Aufrufe). WebGL selbst wird nur in Playwright-Smoke getestet. - Tools: je Tool ein Workflow-Test (down/move/up-Sequenz gegen MockAdapter + InMemory-Doc).
TEIL 5 — PHASEN & TASKS
Legende: - [ ] offen · - [~] in Arbeit (Datum) · - [x] fertig (Datum, Hash) · [BLOCKIERT: grund]
ERWEITERUNGSREGEL (verpflichtend): Die Tasks in Phase C (ab C2) und den Phasen D–I sind bewusst als Kurz-Tasks gehalten. SOFORT VOR Beginn eines solchen Tasks wird er auf das volle Schema erweitert (Voraussetzungen · Ziel · Dateien · nummerierte Schritte mit Code-Ankerpunkten · Verify-Kommandos · Commit-Text) und die Erweiterung in diesem Dokument gespeichert. Ein Kurz-Task darf NIE direkt bearbeitet werden. Grund: Nur vollständig ausgeformte Tasks sind von einem schwachen Modell sicher ausführbar.
COPY-PASTE-REGEL: Wo ein Task kompletten Code enthält (z.B. B0-Testdatei): exakt übernehmen, NICHT „verbessern“. Eigene Abweichungen nur, wenn ein Schritt nachweislich am Ist-Code scheitert – dann Blockierung gemäß Teil 0 Punkt 5 loggen.
Phase B — Bugfixes (zuerst! klein & isoliert)
Task B0 — Testgerüst für Bugs (ROT-Tests) ✅ [2026-08-26 · b9eb69f]
- Voraussetzungen: keine
- Ziel: Repro-Tests für BUG-1, BUG-1b und BUG-2, die HEUTE rot sind und nach den Fixes grün. (Regressionstests für BUG-3 liegen in Task B3, für BUG-4 in Task B4 — dort jeweils im selben Commit wie der Fix, weil sie vor dem Fix gegen die bestehende API nicht kompilierbar sind.)
- Dateien: NEU
frontend/tests/bugs.regress.test.ts
WICHTIG (verifiziert): findIntersection ist in geometry.ts NICHT exportiert (nur intern genutzt). Der BUG-2-Test darf es deshalb NICHT importieren — er prüft über das exportierte trimElement. SeatingService ist exportiert (export class SeatingService, L162).
Erzeuge die Datei frontend/tests/bugs.regress.test.ts mit EXAKT diesem Inhalt:
/**
* Regressions-Tests für bekannte Bugs BUG-1, BUG-1b, BUG-2.
* Diese Tests sind VOR dem jeweiligen Fix ROT (dokumentiertes Fehlverhalten)
* und müssen danach GRÜN sein.
*/
import { describe, it, expect } from 'vitest';
import { SeatingService } from '../src/services/seatingService';
import { trimElement } from '../src/tools/modification/geometry';
import type { CADElement } from '../src/types/cad.types';
// ── Helfer (Muster aus tests/geometry.test.ts) ─────────────
function makeLine(id: string, x1: number, y1: number, x2: number, y2: number): CADElement {
return {
id,
type: 'line',
layerId: 'layer-1',
x: (x1 + x2) / 2,
y: (y1 + y2) / 2,
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
properties: { x1, y1, x2, y2 },
} as unknown as CADElement;
}
function makeCircle(id: string, cx: number, cy: number, r: number): CADElement {
return {
id,
type: 'circle',
layerId: 'layer-1',
x: cx,
y: cy,
width: r * 2,
height: r * 2,
properties: { radius: r },
} as unknown as CADElement;
}
// ── BUG-1: rowId-Kollision bei schnellem Erzeugen ───────────
describe('BUG-1: seating row ids', () => {
it('zwei direkt hintereinander erzeugte Reihen haben unterschiedliche rowIds', () => {
const svc = new SeatingService();
const a = svc.createSeatingRow(0, 0, 'layer-1');
const b = svc.createSeatingRow(0, 100, 'layer-1');
const ids = new Set([a[0].properties.rowId, b[0].properties.rowId]);
expect(ids.size).toBe(2);
});
});
// ── BUG-1b: Block-Reihen ohne eigene rowId ──────────────────
describe('BUG-1b: seating block row ids', () => {
it('jede Reihe eines Blocks hat eine eigene, definierte rowId', () => {
const svc = new SeatingService();
const els = svc.createSeatingBlock(0, 0, 'layer-1', { rows: 3, cols: 2 });
expect(els.length).toBe(6);
const rowIds = new Set(els.map((e) => e.properties.rowId));
expect(rowIds.size).toBe(3);
for (const rid of rowIds) expect(rid).toBeDefined();
});
});
// ── BUG-2: Trim an Kreis scheitert (findIntersection nur Gerade×Gerade) ──
describe('BUG-2: trim line at circle', () => {
it('trimElement(Linie, Kreis) liefert ein Ergebnis statt null', () => {
// Linie von (0,0) nach (200,0) schneidet Kreis (Zentrum 100,0, Radius 40)
// an x=60 und x=140 → Schnitt existiert eindeutig.
const line = makeLine('l1', 0, 0, 200, 0);
const circle = makeCircle('c1', 100, 0, 40);
const result = trimElement(line, circle);
expect(result).not.toBeNull();
});
});
Hinweis zu makeLine/makeCircle: Die properties-Namen (x1,y1,x2,y2 bzw. radius) sind die vom Produktivcode genutzten Konventionen (findIntersection liest x1..y2; RenderEngine.drawCircle liest properties.radius || width/2). Falls tests/geometry.test.ts abweichende Helfer hat, gleiche sie daran an — die Struktur oben bleibt.
- Schritte:
- Datei exakt wie oben anlegen.
- Ausführen:
cd frontend && npx vitest run tests/bugs.regress.test.ts→ ERWARTET: alle 3 Tests FAIL (das ist der Beweis, dass die Bugs real sind; dokumentiere die Fehlermeldungen kurz in Teil 7). - Gesamt-Suite:
npx vitest run→ alles andere muss grün bleiben, nur die 3 neuen Tests rot.
- Verify:
npx vitest run tests/bugs.regress.test.ts 2>&1 | tail -25(3 fail erwartet) ·npx vitest run 2>&1 | tail -5(nur diese 3 rot) ·npx tsc --noEmit(Exit 0 — Tests müssen kompilieren!) - Commit:
task(B0): regression tests for known bugs (red) - Nicht tun: Fixes schon einbauen · Tests mit
.skipversehen · die roten Tests „reparieren"
Task B1 — Seating-IDs fixen (BUG-1) ✅ [2026-08-26 · 1bbd653]
- Voraussetzungen: B0
- Dateien:
frontend/src/services/seatingService.ts - Schritte:
- Direkt nach den Interface-/Default-Konstanten (vor
export class SeatingService) diesen Helper ergänzen:(Prüfe zuerst mit grep, ob es bereits einelet seqCounter = 0; function nextUid(prefix: string): string { seqCounter += 1; return `${prefix}_${Date.now().toString(36)}_${seqCounter.toString(36)}_${Math.random().toString(36).slice(2, 8)}`; }generateId()-Methode in der Klasse gibt — wenn ja, nutze diese für Element-IDs weiter, aber NICHT für rowId, da rowId pro Reihe gleich sein muss.) - In
createSeatingRow(ca. L189): VOR der for-Schleifeconst rowId = nextUid('row');einfügen; innerhalb properties:rowId: row_${Date.now()}ERSETZEN durchrowId,(undrowIndex: ibleibt). - In
createSeatingBlock(ca. L227): VOR der äußeren for-Schleifeconst blockId = nextUid('block');(ersetzt das bestehendeconst blockId = \block_${Date.now()}). INNERHALB der äußeren Schleife (pro Reihe)const rowId = nextUid('row');; in properties ergänzen:rowId,(neu) neben bestehendemblockId,rowIndex,colIndex`. - Ausführen:
npx vitest run tests/bugs.regress.test.ts→ BUG-1 und BUG-1b jetzt GRÜN; BUG-2 weiterhin rot (kommt in B2).
- Direkt nach den Interface-/Default-Konstanten (vor
- Verify:
npx vitest run tests/bugs.regress.test.ts 2>&1 | tail -15(2 von 3 grün) ·npx tsc --noEmitExit 0 ·npx vitest run 2>&1 | tail -5 - Commit:
task(B1): unique row/block ids for seating (fixes BUG-1)
Task B2 — findIntersection erweitern (BUG-2)
- Voraussetzungen: B0
- Dateien:
frontend/src/tools/modification/geometry.ts - Schritte:
- LESE ZUERST
RenderEngine.drawArc(L524) und notiere dir die Arc-Property-Konvention (Felder + Winkeleinheit/Richtung). Nutze dieselbe Konvention in den neuen Funktionen. - Neue exportierte Funktionen in geometry.ts ergänzen:
export interface Pt { x: number; y: number; } /** Alle Schnittpunkte Strecke p1→p2 × Kreis(c,r); NUR t∈[0,1] (echte Strecke). */ export function lineCircleIntersections(p1: Pt, p2: Pt, c: Pt, r: number): Pt[] { // Standardformel: a=d·d, b=2·f·d, cc=f·f−r² mit d=p2−p1, f=p1−c // t=(−b±√(b²−4ac))/(2a); nur t∈[0,1]; Diskriminante<0 → [] } /** Schnittpunkte zweier Kreise; [] wenn getrennt oder identisch. */ export function circleCircleIntersections(c1: Pt, r1: number, c2: Pt, r2: number): Pt[] { // Standardformel: d=|c2−c1|, Grenzfälle dokumentieren } /** Strecke × Bogen; Konvention wie RenderEngine.drawArc. */ export function lineArcIntersection(p1: Pt, p2: Pt, el: CADElement): Pt[] { // 1) Strecke×Kreis via lineCircleIntersections // 2) Treffer auf Winkel im [startAngle,endAngle]-Bereich filtern (Grad→Rad konvertieren) } - findIntersection (intern, L303): Dispatch je Typkombination: line×line (bestehend), line×circle, line×arc, circle×circle; Reihenfolge egal → intern normalisieren (z.B. immer kleinerer Typname zuerst). Bei mehreren Punkten: der NAHERE zu p1 von Element 1.
- trimElement/extendElement unverändert lassen.
- Unit-Tests ERGÄNZEN in tests/geometry.test.ts (Datei NICHT ersetzen): je neuer Funktion ≥2 positiv, ≥1 negativ, Randfälle tangential (genau 1 Punkt) + sekant (2 Punkte). BUG-2-Test aus bugs.regress.test.ts muss grün werden.
- LESE ZUERST
- Verify:
npx vitest run tests/geometry.test.ts tests/bugs.regress.test.ts 2>&1 | tail -15(alles grün inkl. BUG-2) ·npx vitest run 2>&1 | tail -5·npx tsc --noEmit - Commit:
task(B2): line/circle/arc intersections (fixes BUG-2)
Task B3 — Offset-Richtung (BUG-3)
- Voraussetzungen: B2 (nutzt Pt/Normalen-Helfer aus geometry.ts)
- Dateien:
frontend/src/tools/modification/geometry.ts,frontend/src/interaction/index.ts, ERGÄNZENfrontend/tests/bugs.regress.test.ts - Schritte:
- geometry.ts: Signatur ändern zu
export function offsetElement(el: CADElement, dist: number, side?: 1 | -1): CADElement—distwird mit(side ?? 1)multipliziert. Prüfe ALLE bestehenden Aufrufer (grep -n 'offsetElement(' -r src tests): Bestehende Aufrufe ohne drittes Argument müssen unverändert weiterlaufen (Default side=1). Normale für line: (-dy, dx)/len; für rect/circle: Vorzeichen relativ zum Zentrum vs. Klickpunkt (wird vom Aufrufer übergeben). - interaction/index.ts offset-Zweig (handleModifyDown): berechne
sideper Kreuzprodukt:cross = (p2.x-p1.x)*(pt.y-p1.y) - (p2.y-p1.y)*(pt.x-p1.x);const side = cross >= 0 ? 1 : -1; übergebeoffsetElement(this.modifySelected[0], dist, side). p1/p2 = Endpunkte der Basislinie aus properties (x1,y1,x2,y2); bei rect/circle: Klickpunkt vs. Elementzentrum. - Regressionstest in bugs.regress.test.ts ERGÄNZEN:
(Konkret ausarbeiten: offsetElement(line, 20, -1) muss y um −20 verschieben, +1 um +20 — exakt eine Assertion auf resultierende properties.x1/y1.)
describe('BUG-3: offset respects click side', () => { it('Klick links der Linie versetzt nach links', () => { // Linie (100,100)→(300,100), Klick bei (200,50) = oberhalb → Ergebnis-Mitte.y < 100 }); }); - Suite grün inkl. neuem Test.
- geometry.ts: Signatur ändern zu
- Verify:
npx vitest run tests/bugs.regress.test.ts 2>&1 | tail -15·npx vitest run 2>&1 | tail -5·npx tsc --noEmit - Commit:
task(B3): offset respects click side (fixes BUG-3)
Task B4 — Hintergrundbild cachen (BUG-4)
- Voraussetzungen: B0
- Dateien:
frontend/src/canvas/RenderEngine.ts, ERGÄNZENfrontend/tests/bugs.regress.test.ts - Schritte:
- LESE ZUERST die aktuelle
drawBackground()(L381) komplett und verstehe den Transform-Kontext (ZoomPan-Transform ist beim Aufruf aktiv). - Zwei private Felder ergänzen:
private bgImage: HTMLImageElement | null = null; private bgSrcLoaded = ''; - drawBackground umschreiben: wenn
this.options.backgroundSrc && this.options.backgroundSrc !== this.bgSrcLoaded→ NEUES Image laden; onload: Felder setzen + EINMALIGthis.render(); wennbackgroundSrc === bgSrcLoaded && bgImage→ direktctx.drawImage(...)mit dem bestehenden Transform (kein Laden). Wenn backgroundSrc null/leer → Felder zurücksetzen. - Regressionstest in bugs.regress.test.ts ERGÄNZEN (jsdom liefert Mock-Context aus tests/setup.ts):
Ausarbeiten mit vi.spyOn(window, 'Image') oder global.Image-Stub; Assertion: Konstruktor-Aufrufe === 1 nach zwei render()-Aufrufen mit gleicher src. Falls jsdom das nicht hergibt: Test auf privates Feld via
describe('BUG-4: background image cached', () => { it('zweites render laedt das Bild nicht erneut', () => { // RenderEngine instanziieren wie in tests/RenderEngine.test.ts (createMockCanvas-Muster) // engine.setOptions({ backgroundSrc: 'data:image/png;base64,iVBORw0KGgo=' }) // engine.render(); await tick; engine.render(); // assert: nur 1× new Image() bzw. img.src-Zuweisung (spy via vi.spyOn(global, 'Image')) }); });(engine as any).bgSrcLoadedzulassen. - Suite grün inkl. neuem Test.
- LESE ZUERST die aktuelle
- Verify:
npx vitest run tests/bugs.regress.test.ts 2>&1 | tail -15·npx vitest run 2>&1 | tail -5·npx tsc --noEmit - Commit:
task(B4): cache background image (fixes BUG-4)
Task B5 — countSeats robust (folgt aus B1)
- Voraussetzungen: B1
- Dateien:
frontend/src/services/seatingService.ts - Schritte:
countSeatsbyRow: Gruppierung nachrowIdunabhängig ob block oder row; byBlock zusätzlich; Rückgabetyp erweitern{total, byRow, byBlock, byZone?}(byZone optional leer lassen). Test intests/ergänzen (neues Fileseating.count.test.ts). - Verify: volle Suite grün. Commit
task(B5): seat counting by row and block
Phase A — Plugin-first Kernel (nach B; A und C können ab A2 parallel)
Task A0 — Logger + Testhelfer
- Voraussetzungen: B5
- Dateien: NEU
frontend/src/utils/logger.ts(level-gesteuert, noop in prod-build via vite define), NEUfrontend/tests/helpers/mockAdapter.ts(MockAdapter: implementiert RenderAdapter-Interface als Recorder — Interface kommt in C1, Mock implementiert zunächst NUR die Methoden, die InteractionEngine heute braucht: preview/clear/snapPoints; wird in C1 erweitert), NEUfrontend/tests/helpers/inMemoryDoc.ts(dünner Wrapper um YjsDocument ohne Netzwerk). - Schritte: Dateien anlegen, je 1 Smoke-Test (
logger writes in test env,mock records calls). - Verify:
npx vitest run tests/helpersgrün. Committask(A0): logger and test helpers
Task A1 — Plugin-Typen v2 + Registry-Erweiterung ( additive )
- Voraussetzungen: A0
- Dateien:
frontend/src/plugins/types.ts(ERWEITERN, nichts Entfernen),frontend/src/plugins/PluginRegistry.ts(neue Getter), NEUfrontend/plugins-v2.test.ts→ besserfrontend/tests/pluginApiV2.test.ts - Schritte:
- Types aus TEIL 2.4 exakt übernehmen (ToolExtensionV2, PanelExtension, LibraryProviderExtension, PluginV2, ToolPointerEvent, ToolContext). Alte Typen bleiben.
- Registry:
registerV2(p: PluginV2),getToolsV2(): ToolExtensionV2[],getPanels(),getLibraryProviders(). enable/disable analog alt (state map um version erweitern). - Tests: Register/enable/disable/getters; doppelte ID verworfen; disabled Tools tauchen in getToolsV2 nicht auf.
- Verify: suite grün. Commit
task(A1): plugin api v2 types and registry (additive)
Task A2 — InteractionDispatcher (parallel zu Altlaufwerk, hinter Feature-Flag)
- Voraussetzungen: A1
- Dateien: NEU
frontend/src/interaction/dispatcher.ts, NEUfrontend/src/interaction/session.ts, NEUfrontend/src/interaction/pointer.ts, ÄNDERNfrontend/src/components/CanvasArea.tsx(Flag), ÄNDERNfrontend/src/types/ui.types.ts(Flag-Typ) - Schritte:
- pointer.ts:
toWorld(e: PointerEvent, zoomPan): ToolPointerEvent(screenToWorld nutzen), Button/Modifier mappen,setPointerCaptureHelfer. - session.ts:
class ToolSession { phase:'idle'|'active'; begin(); end(); }minimal. - dispatcher.ts:
class InteractionDispatcher { constructor(canvas, deps{zoomPan,doc,renderer,snap,selection}) ; setActive(toolId|null) }— bindet pointerdown/move/up/keydown/Escape; ruft handlers des aktiven V2-Tools; ESC → cancel; Rechtsklick → cancel. - CanvasArea: Env-Flag
VITE_TOOL_DISPATCHER=v2(vite env): wenn gesetzt → Dispatcher instanziieren und InteractionEngine deaktivieren (nur Konstruktion skippen), sonst alter Weg. Standard in .env.example dokumentieren. - Test:
dispatcher.test.ts— synthetische PointerEvents auf Fake-Canvas (aus tests/setup.ts vorhanden?), assert handler-Reihenfolge down→move→up, ESC ruft cancel.
- pointer.ts:
- Verify: suite grün;
VITE_TOOL_DISPATCHER=v2 npm run devmanuell: select-Tool (noch keins V2 — Dispatcher meldet 'no active v2 tool' und tut nichts, Alter Pfad aus). Committask(A2): tool dispatcher behind flag
Task A2b — CADDocument mit transact + UndoManager (Grundlage für Tools)
- Voraussetzungen: A1
- Ziel: Eine dünne Dokument-Fassade über YjsDocument, die Tools in A3/A4 nutzen: CRUD + Transaktionen + Undo/Redo. KEINE Tool-Logik hier!
- Dateien: NEU
frontend/src/kernel/document/CADDocument.ts, NEUfrontend/tests/cadDocument.test.ts, LESENfrontend/src/crdt/YjsDocument.ts - Schritte:
- LESE YjsDocument.ts komplett: wie sind elements/layers gespeichert (Y.Map? Y.Array?), wie lauten die bestehenden Methodennamen?
- CADDocument-Klasse anlegen:
/** * Fassade über YjsDocument: elementar-CRUD + transaktionale Änderungen * mit kollaborativ korrektem Undo via Y.UndoManager. */ export class CADDocument { constructor(ydoc: YjsDocument) { ... } addElement(el: CADElement): void; updateElement(id: string, patch: Partial<CADElement>): void; // shallow merge in properties deleteElements(ids: string[]): void; getElement(id: string): CADElement | undefined; getAllElements(): CADElement[]; getLayer(id: string): CADLayer | undefined; getAllLayers(): CADLayer[]; /** Führt fn als EINE Transaktion aus → genau 1 Undo-Schritt. */ transact(fn: () => void): void; // ydoc.doc.transact(fn) undo(): boolean; redo(): boolean; canUndo(): boolean; canRedo(): boolean; onElementsChanged(cb: () => void): () => void; // subscribe, returns unsubscribe } - UndoManager im Konstruktor:
new Y.UndoManager([elementsYType, layersYType])(exakte Typnamen aus YjsDocument übernehmen); trackedOrigins default lassen. - onElementsChanged: via
ydoc.doc.on('update', cb)oder observe auf die Types (was YjsDocument hergibt); Rückgabe Unsubscribe. - Tests (cadDocument.test.ts, In-Memory-Yjs, kein Netz): a. addElement→getAllElements enthält es · b. transact(()=>{add a; add b}) → undo() entfernt BEIDE (ein Schritt!) · c. redo stellt beide wieder her · d. updateElement merged properties shallow · e. deleteElements entfernt nur genannte ids.
- Verify:
npx vitest run tests/cadDocument.test.ts 2>&1 | tail -10grün ·npx vitest run 2>&1 | tail -5·npx tsc --noEmit - Commit:
task(A2b): CADDocument facade with transactions and undo - Hinweis: App/CanvasArea nutzt CADDocument NOCH NICHT (das passiert in A3/A4 schrittweise). Diese Task ist rein additiv.
Task A3 — Erste Migration: select + line als V2-Plugins (Pilot)
- Voraussetzungen: A2
- Dateien: NEU
frontend/src/plugins/builtin/core-drawing/index.ts(enthält line; select kommt hierher: NEUcore-modify/selectTool.tsund von core-drawing registriert? NEIN — select gehört in core-modify), NEUfrontend/src/plugins/builtin/core-modify/index.ts - Schritte:
- select als V2-Tool: down → hitTest/Window-Select via bestehender SelectionEngine-Logik (Code aus interaction/index.ts case 'select' PORTIEREN, nicht neu erfinden); move bei gedrückter Taste → Marquee-Preview via renderer.setPreview; up → commit selection (docStore.setSelection). Undo: Selektion ist NICHT historisiert (wie bisher).
- line als V2-Tool: down setzt Startpunkt; move Preview-Linie; up:
doc.transact(()=>doc.addElement(lineEl))(eine Transaktion!). Ortho/Snap über bestehende SnapEngine im ctx. - Beide mit
tags:['basic'](select) bzw.['basic'](line). - Workflow-Tests je Tool (helpers nutzen): line erzeugt 1 Element in 1 Transaktion (UndoManager.undo() entfernt es wieder!), select wählt per hit.
- Flag an: manuell beide Tools im Browser testen (Zeichnen, Undo, Redo).
- Verify: suite grün inkl. neuer Tests. Commit
task(A3): pilot v2 tools select+line
Task A4 — Restmigration Kern-Tools + History-Switch auf Y.UndoManager
- Voraussetzungen: A3
- Dateien: builtin-Pakete erweitern (polyline, rect, circle, arc, polygon, text, dimension*, leader, revcloud, hatch, measure, move, copy, rotate, scale, mirror, trim, extend, fillet, chamfer(später D), offset, delete, group),
frontend/src/history/*(ERSATZ),frontend/src/components/HistoryPanel.tsx,frontend/tests/HistoryManager.test.ts(umschreiben) - Schritte (STRENG sequentiell, je Teilcommit):
- CADDocument um
transact(fn)+undo()/redo()/canUndo()wrappen (YjsDocument: new Y.UndoManager([elementsMap, layersMap], {trackedOrigins: 'self'})`). Alte HistoryManager-Klasse PARALLEL lassen bis 4. - Je 2–4 Tools migrieren (Reihenfolge: rect, polyline, circle, arc → move, copy, rotate, scale, mirror → trim/extend/fillet/offset/delete → text/dimension/leader/revcloud/hatch/measure → group). JEDES Mal: Workflowtest mit, Suite grün, Teilcommit
task(A4.x): migrate <tools> to v2. - Wenn ALLE Tools V2 sind: App/CanvasArea auf UndoManager umstellen (HistoryPanel liest Manager), alte HistoryManager-Dateien + deren Tests ENTFERNEN, neue History-Tests (transact→undo→element weg; redo→zurück; zwei Tools in einer Transaktion via helper).
- interaction/index.ts: Rest-Switch löschen. Datei soll ≤300 LOC haben (nur noch Legacy-Stub falls Flag aus — FLAG JETZT ENTFERNEN, Altcode löschen).
- CADDocument um
- Verify:
wc -l src/interaction/index.ts≤300 · suite grün · Playwright-Smoke falls vorhanden. Commits je Schritt. - Nicht tun: neue Tool-Funktionen (Ellipse etc.) — das ist Phase D!
Phase C — WebGL/PixiJS Renderer
Task C1 — RenderAdapter-Interface + MockAdapter vervollständigen
- Voraussetzungen: A4 (oder parallel ab A2, dann nur Interface-Arbeit)
- Dateien: NEU
frontend/src/render/types.ts, ERWEITERNfrontend/tests/helpers/mockAdapter.ts - Interface exakt:
export interface Viewport { x:number; y:number; scale:number; width:number; height:number; }
export interface RenderAdapter {
init(canvas: HTMLCanvasElement): void;
resize(w:number,h:number,dpr:number): void;
setViewport(vp: Viewport): void;
destroy(): void;
clear(color: string): void;
drawGrid(cfg: GridCfg): void; drawBackground(img: HTMLImageElement, cfg: BackgroundCfg): void; drawRuler(cfg: RulerCfg): void;
addOrUpdate(el: CADElement, style: LayerStyle): void; // inkrementell!
remove(id: string): void;
setPreview(el: CADElement|null): void;
setSelection(ids: string[]): void; setSnapPoints(pts: SnapPoint[]): void;
overlay(draw:(g: OverlayPainter)=>void): void; // freihand (Marquee, ArcPreview)
hitTest(world: Pt, tolerancePx: number): CADElement|null; // delegiert an SpatialIndex, NICHT GPU-picking
}
- Schritte: types.ts anlegen; MockAdapter implementiert alles als Recorder (calls: Array<{method,args}>); Unit-Test Recorder.
- Verify: suite grün. Commit
task(C1): render adapter interface
Task C2 — PixiJS Grundgerüst (Linie/Rect/Circle/Arc/Polyline/Text)
- Voraussetzungen: C1
- Schritte:
npm i pixi.js@^8im frontend (--legacy-peer-deps beachten).- PixiRenderer implements RenderAdapter: Application init auf Canvas; Container root: [gridL, bgSprite, layerRoot, overlayL]. Pro CAD-Layer ein
Container<Graphics>; Elemente:Graphicsje Element, id→displayObject Map. - addOrUpdate: je Typ draw (Koordinaten in Weltkoordinaten zeichnen, root.scale/position = viewport → KEINE Neuberechnung pro Element beim Zoom).
- Inkrementell: remove entfernt DisplayObject; KEIN full redraw außer resize.
- Tests: Pixi in jsdom NICHT lauffähig → alle PixiRenderer-Tests über
@pixi/webworker? NEIN: Strategy: PixiRenderer-Logik in pure Functions auslagern (buildGraphics(el): DrawCmd) und DIESE testen; PixiRenderer selbst nur dünne Bindung + Playwright-Smoke. - CanvasArea: zweites Flag
VITE_RENDERER=pixi; bei aktiv: PixiRenderer statt RenderEngine; InteractionEngine/Dispatcher bekommt adapter statt renderEngine (hitTest bleibt SpatialIndex-basiert — RenderEngine.hitTest heute? prüfen und Pfad auf SpatialIndex legen).
- Verify: build läuft; Playwright smoke
render.pixi.spec.ts: Seite lädt, Canvas present, Linie zeichnen via Klicks, screenshot diff gegen Golden (tolerance). Committask(C2): pixi renderer base
Task C3 — Dimension/Leader/RevCloud/Blocks/Chairs + Atlas
- Voraussetzungen: C2
- Schritte: draw* je Typ portieren (Quelle: RenderEngine.drawDimension u.a. LESEN und 1:1 Geometrie übernehmen). Chairs/Table/Stage: atlas.ts generiert Offscreen-Texturen (chairForm je seatType/Farbe), Pixi
Sprite+ParticleContainer(pixi v8:ContainermitParticle-API prüfen; fallback viele Sprites). Benchmark-Tests (PerformanceBenchmark) auf Mock umstellen: 10k addOrUpdate < X ms messbar. - Verify: Playwright: 5000-Chairs-Seite interaktiv; vitest Benchmarks. Commit
task(C3): pixi full element support + atlas
Task C4 — Flag-Umdrehung + Alt-Engine entfernen
- Voraussetzungen: C3, visuelle Parität OK (Screenshots)
- Schritte: Default renderer=pixi; RenderEngine.ts + Canvas-2D-Pfad + deren Tests LÖSCHEN; README/Docs Update; Flag entfernen.
- Verify: suite grün; Playwright-Suite grün. Commit
task(C4): make pixi the renderer
Phase D — Werkzeugpalette komplett (alles V2-Plugins)
Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Workflowtest + Commit task(D*)):
- D1 Ellipse+Bogen (core-drawing) — param: cx,cy,rx,ry,rotation; Interaktion: Center→RadiusX→RadiusY (3 Klicks oder Optionsfelder)
- D2 Spline (CV-basierte Bézier, Speicherung Kontrollpunkte, Render als interpolierte Polyline mit Glättung)
- D3 Point, XLine, Ray (construction-Layer-Tag
properties.construction:true, Standard-Layer 'defpoints' auto-anlegen) - D4 MTEXT (Mehrzeilig, , fontSize/bold/italic Properties; Editor = InlineTextEditor erweitern)
- D5 Stretch (Fenster selektiert Endpunkte; verschiebt nur getroffene Vertices)
- D6 Break/Join (Break: Element an Punkt splitten; Join: kollineare lines→polyline)
- D7 Chamfer, Explode, Align, Lengthen
- D8 PolylineEditor (Vertex add/del, segment→arc glätten)
- D9 GripEditing (select-Tool erweitern: Handles an Endpunkten/Centern, drag=modify in Transaktion)
- D10 Arrays: array-rect (rows,cols,dx,dy), array-polar (cx,count,angleSum,rotate), array-path (entlang polyline/arc) — JEDES als Dialog+Vorschau; Tests mit Zähl-Assertions
- D11 Snaps erweitern (SnapEngine + Optionen): tangent, perpendicular, quadrant, center, from-offset (Basispunkt+Eingabefeld dx/dy)
- D12 BlockAttribute: BlockDef.attrDefs[], Instanz attrValues; PropertiesPanel rendert Formular; RenderAdapter zeichnet Attr-Texte; (DXF ATTDEF/ATTR erst F)
Phase E — Event-Domain (plugin: event-tools)
- E1 seating-row/-block V2 mit Drag-Interaktion + Live-Preview + Optionsleiste (count/spacing/curve)
- E2 ArcRows: Radius+Spread+Center(Bühne snap); Stühle rotieren tangential
- E3 countSeats in StatusBar (Panel rechts), Klick highlight
- E4 Templates: kino, bankett-round, stehtisch, podest, parlamentarisch, u-form (JSON in plugin)
- E5 Export Sitzliste CSV/XLSX (xlsx lib oder csv-manuell)
- E6 Gäste (optional, 2.1): Backend-Tabelle guests + Zuordnung + CSV-Import
- E7 Event-Elemente polish: stage(height attr), curtain, spotlight(angle/color), barrier chain
Phase F — Library & Import/Export
- F1 Backend library_folders/library_blocks + Migrationsfile + Tests (CRUD, scopes)
- F2 UI LibraryPanel (Baum, Suche, Favoriten, Thumbnail grid), DragDrop→Canvas insert instance
- F3 „Als Block speichern“ (Auswahl→Block, Thumbnail client-seitig canvas.toDataURL klein)
- F4 LibraryProviderExtension anbinden: builtin-catalog JSONs (event/architecture/landscape Startersets je ~15-30 Blöcke), user-library, global-library
- F5 DXF Parser erweitern: ELLIPSE, SPLINE(grad≤3→Polyline approx), MTEXT, POINT, HATCH(basic solid/lines), ATTDEF/ATTR, INSERT→Blockinstanz; Fehlerreport (Liste skipped entities)
- F6 DXF Writer erweitern: BLOCKS+INSERTS, ATTR; Version AC1009 (R12) + AC1015 Ziel
- F7 .wcadlib Paket (zip: manifest.json, blocks.json, thumbs/) Import+Export (Backend endpoint + UI Dialog)
- F8 SVG-Import als Block (svg→Path→Polyline approximation, simple subset: path/rect/circle/ellipse/line/polygon)
Phase G — Layouts & Plot
- G1 Datenmodell layouts (Yjs Map: layoutId→{name, viewport{center,scale}, frameBlockRef}), Backend persistence folgt automatisch via drawing json
- 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)
- G5 Browser-Print stylesheet (@media print: nur Layout)
Phase H — Modi & Branchenneutralität
- H1 simple/pro Mode (uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist)
- H2 Projekt-Templates (Halle/OpenAir/Saal/Messe/Buero/Garten): JSON Presets (grid unit, layers, enabled plugins, library folders seed)
- H3 Rollen: library-admin (global write) vs user (own folders) — an bestehendes auth anbinden, Backend guard
- H4 i18n Grundlage: strings.ts zentrale Maps de/en, UI-Texte ersetzen (mechanisch, Teilautomatisierbar)
- H5 Flächenberechnung Polygon (m²) Info im PropertiesPanel + measure-area Tool
Phase I — Qualität & Release
- I1 CI: forgejo workflow erweitern um performance gate (playwright bench: 10k interaktionsbudget), typecheck, beide Suites
- I2 Docs: PLUGIN_GUIDE.md (API v2 + Beispielplugin), LIBRARY_SPEC.md (.wcadlib), CHANGELOG 2.0, README rewrite
- I3 Release 2.0 tag + docker images + coolify deploy verify
TEIL 6 — Offene Entscheidungen (vom Nutzer/Team zu klären, blockieren NICHT außer markiert)
| Nr | Entscheidung | Empfehlung | Blockiert |
|---|---|---|---|
| Q1 | PixiJS v8 bestätigen | ja | C2 |
| Q2 | zustand als State-Lib | ja (alternativ jotai) | A2 (sonst eigener Mini-Store) |
| Q3 | Alte Canvas2D-Engine nach C4 komplett löschen? | ja | C4 |
| Q4 | Gästemodul E6 in 2.0 oder 2.1 | 2.1 | E6 |
| Q5 | Fremd-Plugins (ZIP-Install) Freigabe wann | nach M5 | F/A4 Backend-Install |
TEIL 7 — Fortschritts-Log (chronologisch, neueste oben)
Format: YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/offen
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|---|---|---|---|---|---|
| 2026-08-26 | PLAN | ROADMAP.md erstellt, Baseline analysiert | tsc 0, FE 380✓, BE 255✓ | — | Ausgangszustand dokumentiert |
| 2026-08-26 | B0 | bugs.regress.test.ts angelegt (3 Tests für BUG-1/1b/2); alle 3 ROT wie vorhergesagt; Rest-Suite unverändert grün; Validierung des Dokuments bestanden | FE 380✓ + 3 rot (erwartet), tsc 0 | b9eb69f |
Nächster Task: B1 |
| 2026-08-26 | B1 | nextUid-Helper eingebaut; createSeatingRow rowId vor Schleife; createSeatingBlock blockId via nextUid + rowId pro Reihe in properties | BUG-1+1b GRÜN, 382/383 gesamt (BUG-2 rot wie erwartet), tsc 0 | 1bbd653 |
Nächster Task: B2 |
| 2026-08-26 | B2 | lineCircleIntersections/circleCircleIntersections/lineArcIntersection implementiert; findIntersection auf Typ-Dispatch erweitert; Guards in trim/extend von Truthiness auf undefined korrigiert (Koordinate 0 war falsy); Tangenten-Dedup; 10 neue Unit-Tests | ALLE GRÜN: 393/393 gesamt, tsc 0 | 95b14cf |
Nächster Task: B3 |
| 2026-08-26 | B3 | offsetElement um side-Parameter erweitert (rückwärtskompatibel); Interaktionszweig nutzt Kreuzprodukt (Linie) bzw. Radius-Vergleich (Kreis/Bogen); 4 neue Tests | ALLE GRÜN: 397/397 gesamt, tsc 0 | d8f29e4 |
Nächster Task: B4 |
| 2026-08-26 | B4 | drawBackground cached Bild pro backgroundSrc; kein new Image() pro Frame mehr; Regressionstest mit FakeImage-Zähler | ALLE GRÜN: 398/398 gesamt, tsc 0 | d93ee53 |
Nächster Task: B5 |
| 2026-08-26 | B5 | countSeats um optionales byZone erweitert; Testdatei seating.count.test.ts (5 Tests inkl. BUG-1-Verifikation im Block) | PHASE B ABGESCHLOSSEN — ALLE GRÜN: 403/403 gesamt, tsc 0 | 7f4768c |
Nächster Task: A0 |
Gefundene Nebenprobleme (noch nicht zugewiesen)
webhook-receiver.pyim Root: Zweck unklar, ggf. entfernen/dokumentieren (Frage an Nutzer)phase2..4-task-spec.mdveraltet gegenüber BAUPLAN — nach v2.0 archivierenfrontend/src/utils/__tests__/format.test.tsliegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
Nächster offener Task laut Reihenfolge: A0