task(A4.4): migrate text/dimension/leader tools to v2 annotate plugin

This commit is contained in:
Agent Zero
2026-08-27 00:43:20 +02:00
parent 4850f0584b
commit 239257c8b0
3 changed files with 206 additions and 1 deletions
@@ -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],
};
+4 -1
View File
@@ -29,16 +29,19 @@ export type {
export { eventToolsPlugin } from './builtin/eventTools'; export { eventToolsPlugin } from './builtin/eventTools';
export { coreDrawingPlugin } from './builtin/core-drawing'; export { coreDrawingPlugin } from './builtin/core-drawing';
export { coreModifyPlugin } from './builtin/core-modify'; export { coreModifyPlugin } from './builtin/core-modify';
export { coreAnnotatePlugin } from './builtin/core-annotate';
import { pluginRegistry } from './PluginRegistry'; import { pluginRegistry } from './PluginRegistry';
import { eventToolsPlugin } from './builtin/eventTools'; import { eventToolsPlugin } from './builtin/eventTools';
import { coreDrawingPlugin } from './builtin/core-drawing'; import { coreDrawingPlugin } from './builtin/core-drawing';
import { coreModifyPlugin } from './builtin/core-modify'; import { coreModifyPlugin } from './builtin/core-modify';
import { coreAnnotatePlugin } from './builtin/core-annotate';
/** Register all built-in plugins */ /** Register all built-in plugins */
export function registerBuiltinPlugins() { export function registerBuiltinPlugins() {
pluginRegistry.register(eventToolsPlugin); pluginRegistry.register(eventToolsPlugin);
// API v2 (Task A3): // API v2 (Task A3+):
pluginRegistry.registerV2(coreDrawingPlugin); pluginRegistry.registerV2(coreDrawingPlugin);
pluginRegistry.registerV2(coreModifyPlugin); pluginRegistry.registerV2(coreModifyPlugin);
pluginRegistry.registerV2(coreAnnotatePlugin);
} }
+61
View File
@@ -6,6 +6,7 @@
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { lineTool, coreDrawingPlugin } from '../src/plugins/builtin/core-drawing'; import { lineTool, coreDrawingPlugin } from '../src/plugins/builtin/core-drawing';
import { selectTool, coreModifyPlugin } from '../src/plugins/builtin/core-modify'; 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 { CADDocument } from '../src/kernel/document/CADDocument';
import { createInMemoryDoc } from './helpers/inMemoryDoc'; import { createInMemoryDoc } from './helpers/inMemoryDoc';
import type { ToolPointerEvent, FullToolContext } from '../src/plugins/types'; 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', () => { describe('A3 pilot: select tool', () => {
function makeBridge(elements: CADElement[]) { function makeBridge(elements: CADElement[]) {
let ids: string[] = []; let ids: string[] = [];