Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68b66ba190 | |||
| fbc4978a40 |
+4
-3
@@ -600,7 +600,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
|
||||
- [x] D1 Ellipse+Bogen (core-drawing) — param: cx,cy,rx,ry,rotation; Interaktion: Center→RadiusX→RadiusY (3 Klicks oder Optionsfelder) *(2026-08-29: ellipseTool 3-Klick mit Rotation aus Klick2-Zugrichtung, Voll-Ellipse=64-Punkt-geschlossenes Polygon, Bogen via startAngle/endAngle-Options als offene Polyline; Log D1)*
|
||||
- [x] D2 Spline (CV-basierte Bézier, Speicherung Kontrollpunkte, Render als interpolierte Polyline mit Glättung) *(2026-08-29: splineTool mit Catmull-Rom-Interpolation DURCH alle CVs (echte Glättung mit Schwingung an Ecken), Klick-Sequenz + ENTER ab 3 CVs, controlPoints-Metadaten; Log D2)*
|
||||
- [x] D3 Point, XLine, Ray (construction-Layer-Tag `properties.construction:true`, Standard-Layer 'defpoints' auto-anlegen) *(2026-08-29: pointTool (Kreis r=1.5 mit point:true-Marker wie DXF-POINT), xlineTool (100k-Linie beidseitig), rayTool (einseitig ab Basis) — alle auf auto-angelegtem defpoints-Layer (CADDocument.addLayer additiv) mit construction:true; Log D3)*
|
||||
- [ ] D4 MTEXT (Mehrzeilig,
|
||||
- [x] D4 MTEXT (Mehrzeilig, *(2026-08-29: mtextTool in core-annotate v1.1.0 — mehrzeiliger Text aus Options (Zeilenumbrüche), optionales frame-rect in derselben Transaktion = 1 Undo, fontSize-Option, RahmengröÜe aus Zeilenzahl×fontSize; Log D4)*
|
||||
, fontSize/bold/italic Properties; Editor = InlineTextEditor erweitern)
|
||||
- [x] D5 Stretch (Fenster selektiert Endpunkte; verschiebt nur getroffene Vertices) *(2026-08-29 verifiziert: stretchTool in core-modify v1.9.0 per Log D-fin + Code-Grep)*
|
||||
- [x] D6 Break/Join (Break: Element an Punkt splitten; Join: kollineare lines→polyline) *(2026-08-29 verifiziert: breakTool + joinTool in core-modify per Log D-fin/D-ext3 + Code-Grep)*
|
||||
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
|
||||
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-29 | D4 | MTEXT-Tool: mtextTool NEU in core-annotate v1.1.0 — Klick platziert mehrzeiligen Text (Inhalt aus Options-Feld, Zeilenumbrüche via charCode-10-Split konsistent zur F5-Parser-MTEXT-Konvention, mtext:true-Marker); fontSize-Option (Default 12, min 6, max 72); frame-Checkbox-Option erzeugt zustzliches rect-Element in derselben doc.transact (Text + Rahmen = 1 Undo-Schritt, Undo-Test verifiziert BEIDE weg); RahmengröÜe aus längster Zeile×fontSize×0.6 Breite und Zeilenzahl×fontSize×1.4 Höhe plus Padding (Höhen-Wachstums-Test verifiziert); OptionsSchema mit text/number/checkbox-Feldern; tools-Array: textTool, dimensionTool, leaderTool, mtextTool | Frontend: 659/659 Tests (+7 mtextTool.test.ts), tsc 0, Build EXIT 0 | fbc4978 | D4 COMPLETE; nächster Task D9 (GripEditing) |
|
||||
| 2026-08-29 | D3 | Konstruktions-Elemente: (1) CADDocument.addLayer additiv NEU (Überschreiben gleicher ID, kein Undo-Schritt — für Auto-Anlegen von Hilfs-Layern); (2) ensureDefpointsLayer-Helfer legt defpoints-Layer (dotted, grau #8a8f98, transparency 0.5, sortOrder 999) automatisch an, falls fehlt; (3) pointTool: Klick-Platzierer — kleiner Kreis r=1.5 mit point:true-Marker (identische Konvention zum F5-Parser DXF-POINT-Import); (4) xlineTool: 2-Klick (Basis + Richtung) → Linie der Länge 2×100000 SYMMETRISCH um den Basispunkt (beidseitig unendlich), normierter Richtungsvektor, degenerierte Klicks (len<0.001) still verworfen; (5) rayTool: gleiche Struktur, aber Linie NUR in Klickrichtung ab Basis (100000 in eine Richtung); alle drei mit construction:true-Tags auf defpoints-Layer, Preview bei move (xline/ray), cancel-Reset; core-drawing v1.5.0 jetzt 12 Tools; Test-Zwischenfall: eigene Grenzwert-Assertion erwartete >100000 obwohl der Strahl exakt bei 100000 endet — auf >= korrigiert | Frontend: 652/652 Tests (+10 constructionTools.test.ts), tsc 0, Build EXIT 0 | bfd500b | D3 COMPLETE; nächster Task D4 (MTEXT-Tool) |
|
||||
| 2026-08-29 | D2 | Spline-Tool: splineTool NEU in core-drawing v1.4.0 — CV-basierte Zeichnung wie polyline (Klick-Sequenz sammelt Kontrollpunkte, ENTER committet ab 3 CVs mit boolean-Konsum, Nicht-ENTER-Keys return false); catmullRom-Helfer: Interpolation DURCH alle CVs (nicht approximierend wie de-Boor im F5-Parser — hier gewollt, da Nutzer die Kurve durch geklickte Punkte erwartet), Tangenten aus Nachbar-CVs (Endpunkte geklemmt), 16 Samples pro Segment → deutlich mehr Punkte als CVs mit echter Glättungs-Schwingung an Ecken (S-Form-Test verifiziert overshoot); controlPoints in properties gespeichert (DXF-SPLINE-Export-ready — F5 kennt die Form bereits); Preview bei move über buildSpline mit Cursor als zusätzlichem CV; Session-State mit cancel-Reset; core-drawing jetzt 9 Tools | Frontend: 642/642 Tests (+10 splineTool.test.ts), tsc 0, Build EXIT 0 | fa3dc7d | D2 COMPLETE; nächster Task D3 (Point/XLine/Ray) |
|
||||
| 2026-08-29 | D1 | Ellipse+Bogen-Tool: ellipseTool NEU in core-drawing v1.3.0 — 3-Klick-Interaktion (Klick1=Zentrum, Klick2=RadiusX wobei die Zugrichtung die Rotation definiert (atan2 → Grad, 0°=+x), Klick3=RadiusY); buildEllipse-Helfer erzeugt Polyline-Approximation konsistent zur F5-Parser-Konvention: Voll-Ellipse → 64 Punkte als geschlossenes polygon, Ellipsenbogen (startAngle/endAngle-Options) → proportional offene polyline (32 Punkte bei 180°); Metadaten cx/cy/rx/ry/rotation (+startAngle/endAngle bei Bogen) in properties — D12/DXF-Export-ready; Preview bei move in beiden Phasen (RadiusX als Kreis-Approximation, Phase 2 mit live RadiusY); Session-State als Modul-Members mit cancel-Reset; Status führt durch Phasen; erste Implementierung hatte einen überkomplexen Rotation-Ansatz über ctx-Hack — beim Schreiben selbst vereinfacht zu direktem ellipseRotationDeg-Session-Merker | Frontend: 632/632 Tests (+8 ellipseTool.test.ts), tsc 0, Build EXIT 0 | 5db421d | D1 COMPLETE; nächster Task D2 (Spline-Tool) |
|
||||
@@ -745,5 +746,5 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
|
||||
|
||||
---
|
||||
*Nächster offener Schritt: **D4** (MTEXT-Tool: Mehrzeilig, Zeilenumbrüche, mit/ohne Rahmen). Danach: D9 GripEditing, D10 array-path, D11 Snaps, D12 BlockAttribute, Phase I.*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G+H komplett, D1+D2+D3 fertig (Ellipse-, Spline- und Konstruktions-Tools), BUG-1-Kollisionsklasse geschlossen. FE 652/652 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: D4/D9/D10-path/D11/D12, Phase I (Release).*
|
||||
*Nächster offener Schritt: **D9** (GripEditing: select-Tool erweitern — Handles an Endpunkten/Centern, drag=modify in Transaktion). Danach: D10 array-path, D11 Snaps, D12 BlockAttribute, Phase I.*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G+H komplett, D1–D4 fertig (Ellipse, Spline, Konstruktion, MTEXT), BUG-1-Kollisionsklasse geschlossen. FE 659/659 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: D9/D10-path/D11/D12, Phase I (Release).*
|
||||
|
||||
@@ -127,15 +127,81 @@ export const dimensionTool: ToolExtensionV2 = make2PointAnnotate('dimension', 'B
|
||||
export const leaderTool: ToolExtensionV2 = make2PointAnnotate('leader', 'Leader', '\u21b1', buildLeader);
|
||||
|
||||
/** 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 = {
|
||||
manifest: {
|
||||
id: 'core-annotate',
|
||||
name: 'Beschriftung (Kern)',
|
||||
version: '1.0.0',
|
||||
version: '1.1.0',
|
||||
author: 'web-cad team',
|
||||
description: 'Text, Bemaßung, Leader (V2).',
|
||||
category: 'tools',
|
||||
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