task(D4): mtext tool - multiline text with line breaks, optional frame rect in same transaction (1 undo), fontSize option
This commit is contained in:
@@ -127,15 +127,81 @@ export const dimensionTool: ToolExtensionV2 = make2PointAnnotate('dimension', 'B
|
|||||||
export const leaderTool: ToolExtensionV2 = make2PointAnnotate('leader', 'Leader', '\u21b1', buildLeader);
|
export const leaderTool: ToolExtensionV2 = make2PointAnnotate('leader', 'Leader', '\u21b1', buildLeader);
|
||||||
|
|
||||||
/** V2-Plugin: Beschriftung/Bemaßung (A4.4). */
|
/** V2-Plugin: Beschriftung/Bemaßung (A4.4). */
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Task D4: MTEXT — Mehrzeiliger Text mit optionalem Rahmen.
|
||||||
|
// Klick platziert Text (Inhalt aus Options, Zeilenumbrüche erhalten),
|
||||||
|
// frame-Option erzeugt ein rect in derselben Transaktion (1 Undo).
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const mtextTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'mtext',
|
||||||
|
label: 'Mehrzeilentext',
|
||||||
|
icon: '¶',
|
||||||
|
ribbonTab: 'format',
|
||||||
|
tags: ['basic'],
|
||||||
|
description: 'Mehrzeiligen Text platzieren (Rahmen optional, Task D4)',
|
||||||
|
},
|
||||||
|
optionsSchema: [
|
||||||
|
{ key: 'text', label: 'Text', type: 'text' },
|
||||||
|
{ key: 'fontSize', label: 'Größe', type: 'number', min: 6, max: 72 },
|
||||||
|
{ key: 'frame', label: 'Rahmen', type: 'checkbox' },
|
||||||
|
],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc) return;
|
||||||
|
const layerId = (c.options.layerId as string | undefined) ?? 'layer-0';
|
||||||
|
const text = (c.options.text as string | undefined) ?? '';
|
||||||
|
const fontSize = (c.options.fontSize as number | undefined) ?? 12;
|
||||||
|
const frame = (c.options.frame as boolean | undefined) ?? false;
|
||||||
|
// Newline ohne Escape-Probleme: charCode 10
|
||||||
|
const NL = String.fromCharCode(10);
|
||||||
|
const lines = text.split(NL);
|
||||||
|
const lineCount = Math.max(1, lines.length);
|
||||||
|
const maxLen = lines.reduce((m, l) => Math.max(m, l.length), 0);
|
||||||
|
const textW = maxLen * fontSize * 0.6;
|
||||||
|
const textH = lineCount * fontSize * 1.4;
|
||||||
|
const els: CADElement[] = [{
|
||||||
|
id: uid('mtext'),
|
||||||
|
type: 'text',
|
||||||
|
layerId,
|
||||||
|
x: e.world.x,
|
||||||
|
y: e.world.y,
|
||||||
|
width: textW,
|
||||||
|
height: textH,
|
||||||
|
properties: { text, fontSize, mtext: true },
|
||||||
|
} as unknown as CADElement];
|
||||||
|
if (frame) {
|
||||||
|
const pad = fontSize * 0.4;
|
||||||
|
els.push({
|
||||||
|
id: uid('mtextframe'),
|
||||||
|
type: 'rect',
|
||||||
|
layerId,
|
||||||
|
x: e.world.x,
|
||||||
|
y: e.world.y,
|
||||||
|
width: textW + pad * 2,
|
||||||
|
height: textH + pad * 2,
|
||||||
|
properties: { stroke: '#9aa0a6', strokeWidth: 1, fill: 'none' },
|
||||||
|
} as unknown as CADElement);
|
||||||
|
}
|
||||||
|
c.doc.transact(() => {
|
||||||
|
for (const el of els) c.doc!.addElement(el);
|
||||||
|
});
|
||||||
|
ctx.setStatus(`Mehrzeilentext platziert (${lineCount} Zeile(n)${frame ? ', mit Rahmen' : ''})`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const coreAnnotatePlugin: PluginV2 = {
|
export const coreAnnotatePlugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'core-annotate',
|
id: 'core-annotate',
|
||||||
name: 'Beschriftung (Kern)',
|
name: 'Beschriftung (Kern)',
|
||||||
version: '1.0.0',
|
version: '1.1.0',
|
||||||
author: 'web-cad team',
|
author: 'web-cad team',
|
||||||
description: 'Text, Bemaßung, Leader (V2).',
|
description: 'Text, Bemaßung, Leader (V2).',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
},
|
},
|
||||||
tools: [textTool, dimensionTool, leaderTool],
|
tools: [textTool, dimensionTool, leaderTool, mtextTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Task D4 – MTEXT-Tool: Mehrzeiliger Text mit/ohne Rahmen.
|
||||||
|
*
|
||||||
|
* Klick platziert mehrzeiligen Text (Inhalt aus Options, Zeilen-
|
||||||
|
* umbrueche), fontSize-Option, optionale Rahmen-Option erzeugt ein
|
||||||
|
* rect-Element in derselben Transaktion (1 Undo-Schritt).
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { mtextTool } from '../src/plugins/builtin/core-annotate';
|
||||||
|
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||||
|
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||||
|
import type { CADElement, Pt } from '../src/types/cad.types';
|
||||||
|
|
||||||
|
function mkCtx(options: Record<string, unknown> = {}) {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const statuses: string[] = [];
|
||||||
|
const ctx: any = {
|
||||||
|
doc,
|
||||||
|
options,
|
||||||
|
setStatus: (m: string) => statuses.push(m),
|
||||||
|
setPreview: (_el: CADElement | null) => {},
|
||||||
|
};
|
||||||
|
return { ctx, doc, statuses };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pe(x: number, y: number): { world: Pt } {
|
||||||
|
return { world: { x, y } } as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('D4: mtextTool', () => {
|
||||||
|
it('platziert mehrzeiligen Text mit Zeilenumbruechen aus den Options', () => {
|
||||||
|
const { ctx, doc } = mkCtx({ text: 'Zeile 1\nZeile 2\nZeile 3', fontSize: 14 });
|
||||||
|
mtextTool.handlers.down!(pe(100, 50), ctx);
|
||||||
|
const els = doc.getAllElements();
|
||||||
|
expect(els).toHaveLength(1);
|
||||||
|
const el = els[0];
|
||||||
|
expect(el.type).toBe('text');
|
||||||
|
expect(el.x).toBe(100);
|
||||||
|
expect(el.y).toBe(50);
|
||||||
|
expect(el.properties.text).toBe('Zeile 1\nZeile 2\nZeile 3');
|
||||||
|
expect(el.properties.fontSize).toBe(14);
|
||||||
|
expect(el.properties.mtext).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Rahmen-Option erzeugt zusaetzliches rect in derselben Transaktion (1 Undo)', () => {
|
||||||
|
const { ctx, doc } = mkCtx({ text: 'Mit Rahmen', frame: true });
|
||||||
|
mtextTool.handlers.down!(pe(0, 0), ctx);
|
||||||
|
const els = doc.getAllElements();
|
||||||
|
expect(els).toHaveLength(2);
|
||||||
|
const texts = els.filter((e) => e.type === 'text');
|
||||||
|
const rects = els.filter((e) => e.type === 'rect');
|
||||||
|
expect(texts).toHaveLength(1);
|
||||||
|
expect(rects).toHaveLength(1);
|
||||||
|
expect(rects[0].x).toBeCloseTo(0, 5);
|
||||||
|
expect(rects[0].y).toBeCloseTo(0, 5);
|
||||||
|
doc.undo();
|
||||||
|
expect(doc.getAllElements()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ohne frame-Option: nur Text, kein rect', () => {
|
||||||
|
const { ctx, doc } = mkCtx({ text: 'Ohne Rahmen' });
|
||||||
|
mtextTool.handlers.down!(pe(0, 0), ctx);
|
||||||
|
expect(doc.getAllElements().filter((e) => e.type === 'rect')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Standard-Text wenn Options leer: MTEXT-Platzhalter leer (Editor bearbeitet spaeter)', () => {
|
||||||
|
const { ctx, doc } = mkCtx({});
|
||||||
|
mtextTool.handlers.down!(pe(10, 10), ctx);
|
||||||
|
const el = doc.getAllElements()[0];
|
||||||
|
expect(el.properties.text).toBe('');
|
||||||
|
expect(el.properties.fontSize).toBe(12); // Default
|
||||||
|
});
|
||||||
|
|
||||||
|
it('OptionsSchema: text/number/checkbox-Felder', () => {
|
||||||
|
const schema = mtextTool.optionsSchema ?? [];
|
||||||
|
const keys = schema.map((f) => f.key);
|
||||||
|
expect(keys).toContain('text');
|
||||||
|
expect(keys).toContain('fontSize');
|
||||||
|
expect(keys).toContain('frame');
|
||||||
|
const textField = schema.find((f) => f.key === 'text');
|
||||||
|
expect(textField!.type).toBe('text');
|
||||||
|
const frameField = schema.find((f) => f.key === 'frame');
|
||||||
|
expect(frameField!.type).toBe('checkbox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Rahmenhoehe waechst mit Zeilenzahl', () => {
|
||||||
|
const { ctx: ctx1, doc: doc1 } = mkCtx({ text: 'eine Zeile', frame: true });
|
||||||
|
mtextTool.handlers.down!(pe(0, 0), ctx1);
|
||||||
|
const { ctx: ctx3, doc: doc3 } = mkCtx({ text: 'eins\nzwei\ndrei', frame: true });
|
||||||
|
mtextTool.handlers.down!(pe(0, 0), ctx3);
|
||||||
|
const rect1 = doc1.getAllElements().find((e) => e.type === 'rect')!;
|
||||||
|
const rect3 = doc3.getAllElements().find((e) => e.type === 'rect')!;
|
||||||
|
expect(rect3.height).toBeGreaterThan(rect1.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Manifest: id=mtext, tags basic', () => {
|
||||||
|
expect(mtextTool.manifest.id).toBe('mtext');
|
||||||
|
expect((mtextTool.manifest.tags ?? [])).toContain('basic');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user