diff --git a/frontend/src/plugins/builtin/core-annotate/index.ts b/frontend/src/plugins/builtin/core-annotate/index.ts new file mode 100644 index 0000000..dcd84ad --- /dev/null +++ b/frontend/src/plugins/builtin/core-annotate/index.ts @@ -0,0 +1,141 @@ +/** + * core-annotate – Built-in V2-Plugin (Task A4.4). + * + * Werkzeuge: text (Platzierung — Inhalt via InlineTextEditor/PropertiesPanel, + * Bestandskanal onTextEdit bleibt), dimension (2-Punkt linear), leader + * (2-Punkt Pfeil+Label). + */ +import type { PluginV2, ToolExtensionV2, FullToolContext } from '../../types'; +import type { CADElement } from '../../../types/cad.types'; +import type { Pt } from '../../../tools/modification/geometry'; + +const uid = (prefix: string): string => + `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`; + +/** Gemeinsamer 2-Punkt-Sitzungsstart für dimension/leader. */ +let twoPointStart: Pt | null = null; + +function optsOf(ctx: FullToolContext): { stroke: string; layerId: string } { + return { + stroke: (ctx.options.strokeColor as string | undefined) ?? '#e0e0e0', + layerId: (ctx.options.layerId as string | undefined) ?? 'layer-0', + }; +} + +function buildDimension(start: Pt, end: Pt, ctx: FullToolContext): CADElement { + const o = optsOf(ctx); + return { + id: uid('dim'), + type: 'dimension', + layerId: o.layerId, + x: (start.x + end.x) / 2, + y: (start.y + end.y) / 2, + width: Math.hypot(end.x - start.x, end.y - start.y), // Legacy: dist als Breite + height: 20, + properties: { x1: start.x, y1: start.y, x2: end.x, y2: end.y }, + } as unknown as CADElement; +} + +function buildLeader(start: Pt, end: Pt, ctx: FullToolContext): CADElement { + const o = optsOf(ctx); + return { + id: uid('leader'), + type: 'leader', + layerId: o.layerId, + x: end.x, + y: end.y, + width: Math.abs(end.x - start.x), + height: Math.abs(end.y - start.y), + properties: { x1: start.x, y1: start.y, x2: end.x, y2: end.y, text: '', fontSize: 12, stroke: o.stroke, strokeWidth: 1 }, + } as unknown as CADElement; +} + +/** Generischer 2-Punkt-Annotate-Fluss (down→Preview→up commit). */ +function make2PointAnnotate( + id: 'dimension' | 'leader', + label: string, + icon: string, + build: (a: Pt, b: Pt, ctx: FullToolContext) => CADElement, +): ToolExtensionV2 { + return { + manifest: { id, label, icon, ribbonTab: 'format', tags: ['basic'] }, + optionsSchema: [{ key: 'strokeColor', label: 'Farbe', type: 'color' }], + handlers: { + down(e, ctx) { + twoPointStart = e.world; + ctx.setStatus(`${label}: Startpunkt gesetzt – Ende wählen`); + }, + move(e, ctx) { + if (!twoPointStart) return; + ctx.setPreview?.(build(twoPointStart, e.world, ctx as FullToolContext)); + }, + up(e, ctx) { + const c = ctx as FullToolContext; + if (!twoPointStart || !c.doc) { twoPointStart = null; return; } + const el = build(twoPointStart, e.world, c); + c.doc.transact(() => c.doc!.addElement(el)); + twoPointStart = null; + ctx.setPreview?.(null); + ctx.setStatus(`${label} erstellt (${el.id})`); + }, + cancel(ctx) { + twoPointStart = null; + ctx.setPreview?.(null); + ctx.setStatus(`${label} abgebrochen`); + }, + }, + }; +} + +/** + * text-Werkzeug (A4.4): Klick platziert Textelement mit leerem Inhalt; + * Bearbeitung läuft über den bestehenden InlineTextEditor/onTextEdit-Kanal. + */ +export const textTool: ToolExtensionV2 = { + manifest: { + id: 'text', + label: 'Text', + icon: 'T', + ribbonTab: 'format', + tags: ['basic'], + description: 'Text platzieren und über Editor beschriften', + }, + optionsSchema: [{ key: 'fontSize', label: 'Größe', type: 'number', min: 6, max: 72 }], + handlers: { + down(e, ctx) { + const c = ctx as FullToolContext; + if (!c.doc) return; + const layerId = (c.options.layerId as string | undefined) ?? 'layer-0'; + const fontSize = (c.options.fontSize as number | undefined) ?? 12; + const el = { + id: uid('text'), + type: 'text', + layerId, + x: e.world.x, + y: e.world.y, + width: 100, + height: 20, + properties: { text: '', fontSize }, + } as unknown as CADElement; + c.doc.transact(() => c.doc!.addElement(el)); + ctx.setStatus(`Text platziert (${el.id}) – now via Editor`); + }, + }, +}; + +export const dimensionTool: ToolExtensionV2 = make2PointAnnotate('dimension', 'Bemaßung', '\u2194', buildDimension); +export const leaderTool: ToolExtensionV2 = make2PointAnnotate('leader', 'Leader', '\u21b1', buildLeader); + +/** V2-Plugin: Beschriftung/Bemaßung (A4.4). */ +export const coreAnnotatePlugin: PluginV2 = { + manifest: { + id: 'core-annotate', + name: 'Beschriftung (Kern)', + version: '1.0.0', + author: 'web-cad team', + description: 'Text, Bemaßung, Leader (V2).', + category: 'tools', + enabledByDefault: true, + }, + tools: [textTool, dimensionTool, leaderTool], +}; diff --git a/frontend/src/plugins/index.ts b/frontend/src/plugins/index.ts index f5787b8..410de97 100644 --- a/frontend/src/plugins/index.ts +++ b/frontend/src/plugins/index.ts @@ -29,16 +29,19 @@ export type { export { eventToolsPlugin } from './builtin/eventTools'; export { coreDrawingPlugin } from './builtin/core-drawing'; export { coreModifyPlugin } from './builtin/core-modify'; +export { coreAnnotatePlugin } from './builtin/core-annotate'; import { pluginRegistry } from './PluginRegistry'; import { eventToolsPlugin } from './builtin/eventTools'; import { coreDrawingPlugin } from './builtin/core-drawing'; import { coreModifyPlugin } from './builtin/core-modify'; +import { coreAnnotatePlugin } from './builtin/core-annotate'; /** Register all built-in plugins */ export function registerBuiltinPlugins() { pluginRegistry.register(eventToolsPlugin); - // API v2 (Task A3): + // API v2 (Task A3+): pluginRegistry.registerV2(coreDrawingPlugin); pluginRegistry.registerV2(coreModifyPlugin); + pluginRegistry.registerV2(coreAnnotatePlugin); } diff --git a/frontend/tests/pilotTools.test.ts b/frontend/tests/pilotTools.test.ts index a54a088..b76a1bf 100644 --- a/frontend/tests/pilotTools.test.ts +++ b/frontend/tests/pilotTools.test.ts @@ -6,6 +6,7 @@ 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 { coreAnnotatePlugin } from '../src/plugins/builtin/core-annotate'; import { CADDocument } from '../src/kernel/document/CADDocument'; import { createInMemoryDoc } from './helpers/inMemoryDoc'; import type { ToolPointerEvent, FullToolContext } from '../src/plugins/types'; @@ -199,6 +200,66 @@ describe('A4.3 pilot: polyline/polygon tools', () => { }); }); +describe('A4.4 pilot: annotate tools', () => { + it('dimension: down→up committet mit x1..y2-Properties; undo entfernt', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const ctx = makeCtx({ doc }); + const dim = coreAnnotatePlugin.tools!.find((t) => t.manifest.id === 'dimension')!; + + dim.handlers.down!(pe(0, 0), ctx); + dim.handlers.move!(pe(60, 0), ctx); // Preview war da + expect(ctx.previews.length).toBe(1); + dim.handlers.up!(pe(60, 0), ctx); + + const all = doc.getAllElements(); + expect(all.length).toBe(1); + expect(all[0].type).toBe('dimension'); + expect(all[0].x).toBe(30); // Mitte + expect(all[0].properties.x2).toBe(60); + expect(all[0].width).toBeCloseTo(60); // dist als Breite wie Legacy + + expect(doc.undo()).toBe(true); + expect(doc.getAllElements().length).toBe(0); + }); + + it('leader: übernimmt Text/fontSize-Konvention aus Legacy', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const ctx = makeCtx({ doc }); + const leader = coreAnnotatePlugin.tools!.find((t) => t.manifest.id === 'leader')!; + + leader.handlers.down!(pe(10, 20), ctx); + leader.handlers.up!(pe(80, 90), ctx); + + const all = doc.getAllElements(); + expect(all.length).toBe(1); + expect(all[0].type).toBe('leader'); + expect(all[0].properties.x1).toBe(10); + expect(all[0].properties.y2).toBe(90); + expect(all[0].properties.text).toBe(''); + expect(all[0].properties.fontSize).toBe(12); + }); + + it('text: Einzelklick platziert leeres Textelement mit fontSize-Option', () => { + const { ydoc } = createInMemoryDoc(); + const doc = new CADDocument(ydoc); + const ctx = makeCtx({ doc, options: { fontSize: 18 } }); + const txt = coreAnnotatePlugin.tools!.find((t) => t.manifest.id === 'text')!; + + txt.handlers.down!(pe(40, 70), ctx); // kein up nötig — single-point tool + + const all = doc.getAllElements(); + expect(all.length).toBe(1); + expect(all[0].type).toBe('text'); + expect(all[0].x).toBe(40); + expect(all[0].y).toBe(70); + expect(all[0].width).toBe(100); // Legacy-Default-BBox + expect(all[0].properties.fontSize).toBe(18); // aus options statt Default 12 + expect(all[0].properties.text).toBe(''); + }); +}); + describe('A3 pilot: select tool', () => { function makeBridge(elements: CADElement[]) { let ids: string[] = [];