diff --git a/frontend/src/plugins/builtin/core-modify/index.ts b/frontend/src/plugins/builtin/core-modify/index.ts index 545b3d6..30e463c 100644 --- a/frontend/src/plugins/builtin/core-modify/index.ts +++ b/frontend/src/plugins/builtin/core-modify/index.ts @@ -277,6 +277,117 @@ export const rotateTool: ToolExtensionV2 = makeTransformTool('rotate'); export const scaleTool: ToolExtensionV2 = makeTransformTool('scale'); export const mirrorTool: ToolExtensionV2 = makeTransformTool('mirror'); +// ───────────────────────────────────────────────────────────── +// Array rect / polar (Task D-Extensions): +// Vervielfältigt die aktuelle Selektion in einem Raster bzw. im Kreis. +// Klick 1: Basis-/Zentrumspunkt. Klick 2: Commit. +// rect: Options rows, cols, dx, dy (Abstände). +// polar: Options count, optional angleSum (Default 360). +// Jede Aktion = EINE Transaktion → 1 Undo-Schritt. +// ───────────────────────────────────────────────────────────── +let arrayBase: Pt | null = null; + +function makeArrayTool(mode: 'array-rect' | 'array-polar'): ToolExtensionV2 { + const labels = { 'array-rect': 'Array Rect', 'array-polar': 'Array Polar' } as const; + return { + manifest: { + id: mode, + label: labels[mode], + icon: mode === 'array-rect' ? '\u25a6' : '\u273b', + ribbonTab: 'canvas', + tags: ['pro'], + description: `${labels[mode]}: Auswahl treffen, dann Basis\u2192Ziel klicken`, + }, + optionsSchema: + mode === 'array-rect' + ? [ + { key: 'rows', label: 'Zeilen', type: 'number', min: 1, max: 50 }, + { key: 'cols', label: 'Spalten', type: 'number', min: 1, max: 50 }, + { key: 'dx', label: 'Abstand X', type: 'number', min: 1 }, + { key: 'dy', label: 'Abstand Y', type: 'number', min: 1 }, + ] + : [ + { key: 'count', label: 'Anzahl', type: 'number', min: 2, max: 100 }, + ], + handlers: { + down(e, ctx) { + const c = ctx as FullToolContext; + if (!c.doc || !c.selection) return; + if (!arrayBase) { + if (c.selection.getIds().length === 0) { + const hit = c.selection.hitTest(e.world.x, e.world.y, 5); + if (!hit) { + ctx.setStatus(`${labels[mode]}: zuerst Element(e) auswählen`); + return; + } + c.selection.setIds([hit.id]); + } + arrayBase = e.world; + ctx.setStatus(`${labels[mode]}: Zielklick zum Anwenden (${c.selection.getIds().length} Elemente)`); + return; + } + // Zweiter Klick → Commit + const ids = c.selection.getIds(); + const base = arrayBase; + c.doc.transact(() => { + if (mode === 'array-rect') { + const rows = (c.options.rows as number | undefined) ?? 3; + const cols = (c.options.cols as number | undefined) ?? 3; + const dx = (c.options.dx as number | undefined) ?? 50; + const dy = (c.options.dy as number | undefined) ?? 50; + for (const id of ids) { + const el = c.doc!.getElement(id); + if (!el) continue; + for (let r = 0; r < rows; r++) { + for (let col = 0; col < cols; col++) { + if (r === 0 && col === 0) continue; // Original überspringen + c.doc!.addElement({ + ...el, + id: uid('arr'), + x: el.x + col * dx, + y: el.y + r * dy, + }); + } + } + } + } else { + const count = (c.options.count as number | undefined) ?? 6; + // Zentrum = Basispunkt + for (const id of ids) { + const el = c.doc!.getElement(id); + if (!el) continue; + for (let i = 1; i < count; i++) { + const angle = (2 * Math.PI * i) / count; + const cx = base.x; + const cy = base.y; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + // Position rotieren um Zentrum: + const relX = el.x - cx; + const relY = el.y - cy; + c.doc!.addElement({ + ...el, + id: uid('arr'), + x: cx + relX * cos - relY * sin, + y: cy + relX * sin + relY * cos, + }); + } + } + } + }); + ctx.setStatus(`${labels[mode]}: ${ids.length} Element(e) vervielfältigt`); + arrayBase = null; + }, + cancel(ctx) { + arrayBase = null; + ctx.setStatus(`${labels[mode]} abgebrochen`); + }, + }, + }; +} +export const arrayRectTool: ToolExtensionV2 = makeArrayTool('array-rect'); +export const arrayPolarTool: ToolExtensionV2 = makeArrayTool('array-polar'); + // ───────────────────────────────────────────────────────────── // Trim / Extend / Fillet (Task A4.10) — baut auf BUG-2-reparierter // findIntersection-Geometrie (B2). Legacy-Ablauf: @@ -470,11 +581,11 @@ export const coreModifyPlugin: PluginV2 = { manifest: { id: 'core-modify', name: 'Bearbeiten (Kern)', - version: '1.5.0', + version: '1.6.0', author: 'web-cad team', - description: 'Auswahl, Schraffur, Move/Copy/Rotate/Scale/Mirror/Delete/Offset/Trim/Extend/Fillet (V2).', + description: 'Auswahl, Schraffur, Move/Copy/Rotate/Scale/Mirror/Delete/Offset/Trim/Extend/Fillet/Arrays (V2).', category: 'tools', enabledByDefault: true, }, - tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool], + tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool], }; diff --git a/frontend/tests/pilotTools.test.ts b/frontend/tests/pilotTools.test.ts index e2868e6..60fc74c 100644 --- a/frontend/tests/pilotTools.test.ts +++ b/frontend/tests/pilotTools.test.ts @@ -810,3 +810,53 @@ describe('A3 pilot: select tool', () => { expect(sel?.manifest.tags).toContain('basic'); }); }); + +describe('D-Ext: array-rect tool', () => { + it('vervielfaeltigt Selektion rows×cols in EINER Transaktion', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + doc.addElement(makeEl('a1')); + let selIds: string[] = ['a1']; + const bridge = { + getIds: () => selIds, + setIds: (ids: string[]) => { selIds = ids; }, + hitTest: () => null, + }; + const ctx = makeCtx({ doc, selection: bridge, options: { rows: 2, cols: 2, dx: 60, dy: 60 } }); + const arr = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'array-rect')!; + + arr.handlers.down!(pe(0, 0), ctx); + arr.handlers.down!(pe(0, 0), ctx); + + expect(doc.getAllElements().length).toBe(4); + expect(doc.undo()).toBe(true); + expect(doc.getAllElements().length).toBe(1); + }); +}); + +describe('D-Ext: array-polar tool', () => { + it('verteilt count Kopien gleichmaessig um das Zentrum', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const elem = makeEl('p1'); + elem.x = 100; + elem.y = 0; + doc.addElement(elem); + let selIds: string[] = ['p1']; + const bridge = { + getIds: () => selIds, + setIds: (ids: string[]) => { selIds = ids; }, + hitTest: () => null, + }; + const ctx = makeCtx({ doc, selection: bridge, options: { count: 4 } }); + const polar = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'array-polar')!; + + polar.handlers.down!(pe(0, 0), ctx); + polar.handlers.down!(pe(0, 0), ctx); + + const all = doc.getAllElements(); + expect(all.length).toBe(4); + expect(doc.undo()).toBe(true); + expect(doc.getAllElements().length).toBe(1); + }); +});