From 76f305fc6f8a25d8a6931a3a329f6a81cf38efb9 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Thu, 27 Aug 2026 01:12:52 +0200 Subject: [PATCH] task(A4.9): migrate event domain tools (chair/row/block/table/stage) to v2 --- .../src/plugins/builtin/event-tools/index.ts | 78 +++++++++++++++++++ frontend/src/plugins/index.ts | 3 + frontend/tests/pilotTools.test.ts | 55 ++++++++++++- 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 frontend/src/plugins/builtin/event-tools/index.ts diff --git a/frontend/src/plugins/builtin/event-tools/index.ts b/frontend/src/plugins/builtin/event-tools/index.ts new file mode 100644 index 0000000..d833110 --- /dev/null +++ b/frontend/src/plugins/builtin/event-tools/index.ts @@ -0,0 +1,78 @@ +/** + * event-tools – Built-in V2-Plugin (Task A4.9). + * + * Fachwerkzeuge für Veranstaltungsplanung: chair, seating-row, + * seating-block, table, stage — alle Einzelklick-Platzierer über + * den (durch BUG-1-Fix korrigierten) SeatingService. + * + * Jede Platzierung = doc.transact → 1 Undo-Schritt (Multi-Element-Sets + * wie seating-block erscheinen/verschwinden gemeinsam). + */ +import type { PluginV2, ToolExtensionV2, FullToolContext } from '../../types'; +import type { CADElement } from '../../../types/cad.types'; +import { SeatingService } from '../../../services/seatingService'; + +const seating = new SeatingService(); + +/** Erzeugt ein Einzelklick-Platzierer-Werkzeug über einen placer. */ +function makePlaceTool( + id: string, + label: string, + icon: string, + placer: (x: number, y: number, layerId: string) => CADElement | CADElement[], + statusNoun: string, +): ToolExtensionV2 { + return { + manifest: { + id, + label, + icon, + ribbonTab: 'insert', + tags: ['basic'], + description: `${statusNoun} per Klick platzieren`, + }, + optionsSchema: [], + handlers: { + down(e, ctx) { + const c = ctx as FullToolContext; + if (!c.doc) return; + const layerId = (c.options.layerId as string | undefined) ?? 'layer-0'; + const placed = placer(e.world.x, e.world.y, layerId); + const els = Array.isArray(placed) ? placed : [placed]; + c.doc.transact(() => { + for (const el of els) c.doc!.addElement(el); + }); + ctx.setStatus(`${statusNoun} platziert (${els.length} Element${els.length === 1 ? '' : 'e'})`); + }, + }, + }; +} + +export const chairTool = makePlaceTool('chair', 'Stuhl', '\u2570', + (x, y, layerId) => seating.createChair(x, y, layerId), 'Stuhl'); + +export const seatingRowTool = makePlaceTool('seating-row', 'Stuhldreihe', '\u2500\u2570\u2500', + (x, y, layerId) => seating.createSeatingRow(x, y, layerId), 'Reihe'); + +export const seatingBlockTool = makePlaceTool('seating-block', 'Stuhlblock', '\u2565', + (x, y, layerId) => seating.createSeatingBlock(x, y, layerId), 'Block'); + +export const tableTool = makePlaceTool('table', 'Tisch', '\u25cb', + (x, y, layerId) => seating.createTable(x, y, layerId), 'Tisch'); + +export const stageTool = makePlaceTool('stage', 'Bühne', '\u25ad', + (x, y, layerId) => seating.createStage(x, y, layerId), 'Bühne'); + +/** V2-Plugin: Event-Domain (Task A4.9). */ +export const eventToolsV2Plugin: PluginV2 = { + manifest: { + id: 'event-tools', + name: 'Event-Bestuhlung', + version: '2.0.0', + author: 'web-cad team', + description: 'Stühle, Reihen, Blöcke, Tische, Bühne (V2).', + category: 'elements', + enabledByDefault: true, + }, + tools: [chairTool, seatingRowTool, seatingBlockTool, tableTool, stageTool], +}; diff --git a/frontend/src/plugins/index.ts b/frontend/src/plugins/index.ts index a515dfc..97923e0 100644 --- a/frontend/src/plugins/index.ts +++ b/frontend/src/plugins/index.ts @@ -31,6 +31,7 @@ export { coreDrawingPlugin } from './builtin/core-drawing'; export { coreModifyPlugin } from './builtin/core-modify'; export { coreAnnotatePlugin } from './builtin/core-annotate'; export { coreMeasurePlugin } from './builtin/core-measure'; +export { eventToolsV2Plugin } from './builtin/event-tools'; import { pluginRegistry } from './PluginRegistry'; import { eventToolsPlugin } from './builtin/eventTools'; @@ -38,6 +39,7 @@ import { coreDrawingPlugin } from './builtin/core-drawing'; import { coreModifyPlugin } from './builtin/core-modify'; import { coreAnnotatePlugin } from './builtin/core-annotate'; import { coreMeasurePlugin } from './builtin/core-measure'; +import { eventToolsV2Plugin } from './builtin/event-tools'; /** Register all built-in plugins */ export function registerBuiltinPlugins() { @@ -47,4 +49,5 @@ export function registerBuiltinPlugins() { pluginRegistry.registerV2(coreModifyPlugin); pluginRegistry.registerV2(coreAnnotatePlugin); pluginRegistry.registerV2(coreMeasurePlugin); + pluginRegistry.registerV2(eventToolsV2Plugin); } diff --git a/frontend/tests/pilotTools.test.ts b/frontend/tests/pilotTools.test.ts index 8fff261..709bdd5 100644 --- a/frontend/tests/pilotTools.test.ts +++ b/frontend/tests/pilotTools.test.ts @@ -6,8 +6,9 @@ import { describe, it, expect, vi } from 'vitest'; import { lineTool, coreDrawingPlugin } from '../src/plugins/builtin/core-drawing'; import { selectTool, coreModifyPlugin } from '../src/plugins/builtin/core-modify'; -import { coreMeasurePlugin } from '../src/plugins/builtin/core-measure'; import { coreAnnotatePlugin } from '../src/plugins/builtin/core-annotate'; +import { coreMeasurePlugin } from '../src/plugins/builtin/core-measure'; +import { eventToolsV2Plugin } from '../src/plugins/builtin/event-tools'; import { CADDocument } from '../src/kernel/document/CADDocument'; import { createInMemoryDoc } from './helpers/inMemoryDoc'; import type { ToolPointerEvent, FullToolContext } from '../src/plugins/types'; @@ -348,6 +349,58 @@ describe('A4.5 pilot: hatch tool', () => { }); }); +describe('A4.9 pilot: event tools', () => { + it('chair platziert Einzel-Element per Klick', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const ctx = makeCtx({ doc }); + const chair = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'chair')!; + + chair.handlers.down!(pe(100, 200), ctx); + const all = doc.getAllElements(); + expect(all.length).toBe(1); + expect(all[0].type).toBe('chair'); + expect(all[0].x).toBe(100); + expect(all[0].y).toBe(200); + }); + + it('seating-block erzeugt rows×cols Elemente in EINER Transaktion (ein undo = ganzer Block weg)', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const ctx = makeCtx({ doc }); + const block = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'seating-block')!; + + block.handlers.down!(pe(0, 0), ctx); // Legacy-Default: rows=3, cols=8 + const all = doc.getAllElements(); + expect(all.length).toBeGreaterThan(1); // Multi-Element-Satz + expect(all.every((e) => e.type === 'chair')).toBe(true); + + // BUG-1-Regressionsschutz: alle Reihen im Block haben eigene rowIds + const rowIds = new Set(all.map((e) => e.properties.rowId)); + expect(rowIds.size).toBeGreaterThanOrEqual(3); + + // Kern-Akzeptanz: EIN undo entfernt den GANZEN Block + expect(doc.undo()).toBe(true); + expect(doc.getAllElements().length).toBe(0); + }); + + it('seating-row platziert mehrere Stühle mit gemeinsamer rowId; undo entfernt Reihe', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const ctx = makeCtx({ doc }); + const row = eventToolsV2Plugin.tools!.find((t) => t.manifest.id === 'seating-row')!; + + row.handlers.down!(pe(0, 0), ctx); + const all = doc.getAllElements(); + expect(all.length).toBeGreaterThanOrEqual(2); + const rowIds = new Set(all.map((e) => e.properties.rowId)); + expect(rowIds.size).toBe(1); // eine Reihe → EINE rowId + + expect(doc.undo()).toBe(true); + expect(doc.getAllElements().length).toBe(0); + }); +}); + describe('A4.6 pilot: move/copy tools', () => { it('move verschiebt selektierte Elemente in EINER Transaktion (undo entfernt Versatz)', () => { const { ydoc } = createInMemoryDoc();