From 8204de81c40d8a173ba36becf29b93c8e6271167 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Wed, 26 Aug 2026 23:14:26 +0200 Subject: [PATCH] task(A4.2): migrate rect tool to v2 plugin --- .../src/plugins/builtin/core-drawing/index.ts | 77 ++++++++++++++++++- frontend/tests/pilotTools.test.ts | 40 ++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/frontend/src/plugins/builtin/core-drawing/index.ts b/frontend/src/plugins/builtin/core-drawing/index.ts index b94538d..caab46b 100644 --- a/frontend/src/plugins/builtin/core-drawing/index.ts +++ b/frontend/src/plugins/builtin/core-drawing/index.ts @@ -16,6 +16,8 @@ const uid = (prefix: string): string => /** Startpunkt der laufenden Linie (nur innerhalb einer down→up-Sitzung gesetzt). */ let lineStart: Pt | null = null; +/** Startpunkt des laufenden Rechtecks (A4.2). */ +let rectStart: Pt | null = null; function buildLine(start: Pt, end: Pt, ctx: FullToolContext): CADElement { const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0'; @@ -32,6 +34,25 @@ function buildLine(start: Pt, end: Pt, ctx: FullToolContext): CADElement { } as unknown as CADElement; } +function buildRect(cornerA: Pt, cornerB: Pt, ctx: FullToolContext): CADElement { + const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0'; + const layerId = (ctx.options.layerId as string | undefined) ?? 'layer-0'; + const minX = Math.min(cornerA.x, cornerB.x); + const minY = Math.min(cornerA.y, cornerB.y); + const w = Math.abs(cornerB.x - cornerA.x); + const h = Math.abs(cornerB.y - cornerA.y); + return { + id: uid('rect'), + type: 'rect', + layerId, + x: minX + w / 2, // BBox-Zentrum (Legacy-Konvention) + y: minY + h / 2, + width: w, + height: h, + properties: { stroke, strokeWidth: 1 }, + } as unknown as CADElement; +} + /** * line-Werkzeug: down setzt Startpunkt, move zeigt Live-Vorschau, * up committet via doc.transact (1 Linie = 1 Undo-Schritt). @@ -80,16 +101,64 @@ export const lineTool: ToolExtensionV2 = { }, }; -/** V2-Plugin: Kern-Zeichenwerkzeuge (Pilot: nur line). */ +/** + * rect-Werkzeug (A4.2): down setzt Ecke, move zeigt BBox-Vorschau, + * up committet via doc.transact. Portiert aus Legacy case 'rect' + * (BBox-Konvention: x/y = Zentrum, width/height absolut). + */ +export const rectTool: ToolExtensionV2 = { + manifest: { + id: 'rect', + label: 'Rechteck', + icon: '\u25ad', + ribbonTab: 'canvas', + tags: ['basic'], + description: 'Zieht ein Rechteck auf (Klick Ecke, Klick Gegenecke)', + }, + optionsSchema: [ + { key: 'strokeColor', label: 'Farbe', type: 'color' }, + ], + handlers: { + down(e, ctx) { + rectStart = e.world; + ctx.setStatus(`Rechteck ab (${rectStart.x.toFixed(0)}, ${rectStart.y.toFixed(0)}) – Gegenecke wählen`); + }, + move(e, ctx) { + if (!rectStart) return; + ctx.setPreview?.(buildRect(rectStart, e.world, ctx)); + }, + up(e, ctx) { + const c = ctx as FullToolContext; + if (!rectStart || !c.doc) { + rectStart = null; + return; + } + const el = buildRect(rectStart, e.world, c); + c.doc.transact(() => { + c.doc!.addElement(el); + }); + rectStart = null; + ctx.setPreview?.(null); + ctx.setStatus(`Rechteck erstellt (${el.id})`); + }, + cancel(ctx) { + rectStart = null; + ctx.setPreview?.(null); + ctx.setStatus('Rechteck abgebrochen'); + }, + }, +}; + +/** V2-Plugin: Kern-Zeichenwerkzeuge (Pilot: line; A4.2: rect). */ export const coreDrawingPlugin: PluginV2 = { manifest: { id: 'core-drawing', name: 'Zeichnen (Kern)', - version: '1.0.0', + version: '1.1.0', author: 'web-cad team', - description: 'Grundlegende Zeichenwerkzeuge (V2-Pilot: Linie).', + description: 'Grundlegende Zeichenwerkzeuge (V2: Linie, Rechteck).', category: 'tools', enabledByDefault: true, }, - tools: [lineTool], + tools: [lineTool, rectTool], }; diff --git a/frontend/tests/pilotTools.test.ts b/frontend/tests/pilotTools.test.ts index c4d9bb4..4d13936 100644 --- a/frontend/tests/pilotTools.test.ts +++ b/frontend/tests/pilotTools.test.ts @@ -80,6 +80,46 @@ describe('A3 pilot: line tool', () => { }); }); +describe('A4.2 pilot: rect tool', () => { + it('erzeugt Rechteck mit BBox-Konvention; ein undo entfernt es', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const ctx = makeCtx({ doc }); + const rect = coreDrawingPlugin.tools?.find((t) => t.manifest.id === 'rect'); + expect(rect).toBeDefined(); + + rect!.handlers.down!(pe(10, 10), ctx); + rect!.handlers.move!(pe(110, 60), ctx); + expect(ctx.previews.length).toBe(1); // Live-Vorschau + rect!.handlers.up!(pe(110, 60), ctx); + + const all = doc.getAllElements(); + expect(all.length).toBe(1); + expect(all[0].type).toBe('rect'); + // Legacy-BBox-Konvention: Zentrum zwischen beiden Ecken + expect(all[0].x).toBe(60); + expect(all[0].y).toBe(35); + expect(all[0].width).toBe(100); + expect(all[0].height).toBe(50); + + expect(doc.undo()).toBe(true); + expect(doc.getAllElements().length).toBe(0); + }); + + it('cancel verwirft das laufende Rechteck', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const ctx = makeCtx({ doc }); + const rect = coreDrawingPlugin.tools!.find((t) => t.manifest.id === 'rect')!; + + rect.handlers.down!(pe(5, 5), ctx); + rect.handlers.cancel!(ctx); + rect.handlers.up!(pe(50, 50), ctx); + + expect(doc.getAllElements().length).toBe(0); + }); +}); + describe('A3 pilot: select tool', () => { function makeBridge(elements: CADElement[]) { let ids: string[] = [];