task(A4.4): migrate text/dimension/leader tools to v2 annotate plugin
This commit is contained in:
@@ -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],
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user