Compare commits

...

2 Commits

4 changed files with 387 additions and 5 deletions
+4 -3
View File
@@ -599,7 +599,7 @@ export interface RenderAdapter {
Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Workflowtest + Commit `task(D*)`): Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Workflowtest + Commit `task(D*)`):
- [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] 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] 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)*
- [ ] D3 Point, XLine, Ray (construction-Layer-Tag `properties.construction:true`, Standard-Layer 'defpoints' auto-anlegen) - [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, - [ ] D4 MTEXT (Mehrzeilig,
, fontSize/bold/italic Properties; Editor = InlineTextEditor erweitern) , 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] 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)*
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen | | Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 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 | 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) | | 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) |
| 2026-08-29 | H5 | Flächenberechnung: (1) polygonArea NEU in geometry.ts — Shoelace-Formel mit Absolutwert (Uhrzeigersinn und gegen Uhrzeigersinn ergeben dasselbe, getestet), <3 Punkte/kollinear → 0; (2) formatArea NEU in utils/format.ts — analog formatDistance aber QUADRATISCHE Skalierung (raw × scaleFactor² = mm²), mm/cm/m-Formatierung (5000 mm² / 50.0 cm² / 5.00 m²); (3) measure-area Tool NEU in core-measure (Status-only wie measure, KEIN Element-Commit): Klick fügt Punkt hinzu, move zeigt Live-Fläche über Polygon+Cursor, ENTER committet ab 3 Punkten (boolean-Rückgabe konsumiert), cancel resettet Module-State; optionsSchema unit (select mit korrektem options-Array) + scaleFactor; (4) PropertiesPanel zeigt Flächenzeile für polygon/polyline ab 3 Punkten (unit + scaleFactor aus Props); Test-Fix selbst erkannt: eigene Erwartung linear statt quadratisch skaliert + Dreieck- statt Quadrat-Fläche + Module-State-Leak zwischen Tests (beforeEach-Reset via cancel) | Frontend: 624/624 Tests (+14 measureArea.test.ts), tsc 0 (nach 2 selbst gefixten Typ-Fehlern: PropertyField heißt options nicht values, key-Handler braucht boolean), Build EXIT 0 | b210ccc | **H5 COMPLETE — PHASE H VOLLSTÄNDIG** (Modi/Templates/Rollen/i18n/Flächen); nächster Schritt: Rest-D-Tools ab D1 (Ellipse) | | 2026-08-29 | H5 | Flächenberechnung: (1) polygonArea NEU in geometry.ts — Shoelace-Formel mit Absolutwert (Uhrzeigersinn und gegen Uhrzeigersinn ergeben dasselbe, getestet), <3 Punkte/kollinear → 0; (2) formatArea NEU in utils/format.ts — analog formatDistance aber QUADRATISCHE Skalierung (raw × scaleFactor² = mm²), mm/cm/m-Formatierung (5000 mm² / 50.0 cm² / 5.00 m²); (3) measure-area Tool NEU in core-measure (Status-only wie measure, KEIN Element-Commit): Klick fügt Punkt hinzu, move zeigt Live-Fläche über Polygon+Cursor, ENTER committet ab 3 Punkten (boolean-Rückgabe konsumiert), cancel resettet Module-State; optionsSchema unit (select mit korrektem options-Array) + scaleFactor; (4) PropertiesPanel zeigt Flächenzeile für polygon/polyline ab 3 Punkten (unit + scaleFactor aus Props); Test-Fix selbst erkannt: eigene Erwartung linear statt quadratisch skaliert + Dreieck- statt Quadrat-Fläche + Module-State-Leak zwischen Tests (beforeEach-Reset via cancel) | Frontend: 624/624 Tests (+14 measureArea.test.ts), tsc 0 (nach 2 selbst gefixten Typ-Fehlern: PropertyField heißt options nicht values, key-Handler braucht boolean), Build EXIT 0 | b210ccc | **H5 COMPLETE — PHASE H VOLLSTÄNDIG** (Modi/Templates/Rollen/i18n/Flächen); nächster Schritt: Rest-D-Tools ab D1 (Ellipse) |
@@ -744,5 +745,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) - `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
--- ---
*Nächster offener Schritt: **D3** (Point/XLine/Ray mit construction-Layer-Tag, Standard-Layer defpoints auto-anlegen). Danach: D4 MTEXT, D9 GripEditing, D10 array-path, D11 Snaps, D12 BlockAttribute, Phase I.* *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 fertig (Ellipse- + Spline-Tool), BUG-1-Kollisionsklasse geschlossen. FE 642/642 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: D3D4/D9/D10-path/D11/D12, Phase I (Release).* *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).*
@@ -112,6 +112,11 @@ export class CADDocument {
} }
/** Fügt einen Layer hinzu (überschreibt gleiche ID, kein Undo-Schritt). */
addLayer(layer: CADLayer): void {
this.ydoc.data.layers.set(layer.id, layer);
}
/** /**
* Ersetzt das komplette Layer-Set (Task H2: Projekt-Templates). * Ersetzt das komplette Layer-Set (Task H2: Projekt-Templates).
* Bestehende Layer werden entfernt, Template-Layer übernommen. * Bestehende Layer werden entfernt, Template-Layer übernommen.
@@ -662,15 +662,244 @@ export const splineTool: ToolExtensionV2 = {
}, },
}; };
// ─────────────────────────────────────────────────────────────
// Task D3: Point/XLine/Ray — Konstruktions-Elemente auf defpoints.
// ─────────────────────────────────────────────────────────────
/** Auto-Anlegen des defpoints-Layers (Task D3) falls fehlt. */
function ensureDefpointsLayer(ctx: FullToolContext): void {
const doc = ctx.doc as unknown as {
getLayer(id: string): unknown;
addLayer(l: unknown): void;
} | undefined;
if (!doc || doc.getLayer('defpoints')) return;
doc.addLayer({
id: 'defpoints',
name: 'defpoints',
visible: true,
locked: false,
color: '#8a8f98',
lineType: 'dotted',
transparency: 0.5,
sortOrder: 999,
parentId: null,
});
}
/** Konstruktions-Element-Basis-Props (construction-Tags, Task D3). */
function constructionProps(): Record<string, unknown> {
return { stroke: '#8a8f98', strokeWidth: 1, construction: true };
}
export const pointTool: ToolExtensionV2 = {
manifest: {
id: 'point',
label: 'Punkt',
icon: '\u2022',
ribbonTab: 'canvas',
tags: ['basic'],
description: 'Konstruktionspunkt platzieren (defpoints-Layer)',
},
optionsSchema: [],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc) return;
ensureDefpointsLayer(c);
const r = 1.5;
const el: CADElement = {
id: uid('point'),
type: 'circle',
layerId: 'defpoints',
x: e.world.x,
y: e.world.y,
width: r * 2,
height: r * 2,
properties: { ...constructionProps(), radius: r, point: true },
} as unknown as CADElement;
c.doc.transact(() => c.doc!.addElement(el));
ctx.setStatus(`Punkt platziert (${el.id})`);
},
},
};
/** XLine: sehr lange Linie in BEIDE Richtungen um den Basispunkt. */
let xlineBase: Pt | null = null;
export const xlineTool: ToolExtensionV2 = {
manifest: {
id: 'xline',
label: 'Konstruktionslinie',
icon: '\u2194',
ribbonTab: 'canvas',
tags: ['basic'],
description: 'Unendliche Konstruktionslinie: Basispunkt + Richtung klicken',
},
optionsSchema: [],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc) return;
if (!xlineBase) {
xlineBase = e.world;
ctx.setStatus('Konstruktionslinie: Basis gesetzt Richtung klicken');
return;
}
ensureDefpointsLayer(c);
const dx = e.world.x - xlineBase.x;
const dy = e.world.y - xlineBase.y;
const len = Math.hypot(dx, dy);
if (len < 0.001) { xlineBase = null; return; }
const ux = dx / len, uy = dy / len;
const L = 100000;
const el: CADElement = {
id: uid('xline'),
type: 'line',
layerId: 'defpoints',
x: xlineBase.x,
y: xlineBase.y,
width: 0,
height: 0,
properties: {
...constructionProps(),
x1: xlineBase.x - ux * L,
y1: xlineBase.y - uy * L,
x2: xlineBase.x + ux * L,
y2: xlineBase.y + uy * L,
},
} as unknown as CADElement;
c.doc.transact(() => c.doc!.addElement(el));
xlineBase = null;
ctx.setPreview?.(null);
ctx.setStatus(`Konstruktionslinie erstellt (${el.id})`);
},
move(e, ctx) {
const c = ctx as FullToolContext;
if (!xlineBase) return;
const dx = e.world.x - xlineBase.x;
const dy = e.world.y - xlineBase.y;
const len = Math.hypot(dx, dy);
if (len < 0.001) return;
const ux = dx / len, uy = dy / len;
const L = 100000;
ctx.setPreview?.({
id: '__preview_xline__',
type: 'line',
layerId: 'defpoints',
x: xlineBase.x,
y: xlineBase.y,
width: 0,
height: 0,
properties: {
...constructionProps(),
x1: xlineBase.x - ux * L,
y1: xlineBase.y - uy * L,
x2: xlineBase.x + ux * L,
y2: xlineBase.y + uy * L,
},
} as unknown as CADElement);
},
cancel(ctx) {
xlineBase = null;
ctx.setPreview?.(null);
ctx.setStatus('Konstruktionslinie abgebrochen');
},
},
};
/** Ray: Linie ab Basispunkt NUR in Klickrichtung. */
let rayBase: Pt | null = null;
export const rayTool: ToolExtensionV2 = {
manifest: {
id: 'ray',
label: 'Strahl',
icon: '\u2192',
ribbonTab: 'canvas',
tags: ['basic'],
description: 'Strahl ab Basispunkt in Klickrichtung',
},
optionsSchema: [],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc) return;
if (!rayBase) {
rayBase = e.world;
ctx.setStatus('Strahl: Basis gesetzt Richtung klicken');
return;
}
ensureDefpointsLayer(c);
const dx = e.world.x - rayBase.x;
const dy = e.world.y - rayBase.y;
const len = Math.hypot(dx, dy);
if (len < 0.001) { rayBase = null; return; }
const ux = dx / len, uy = dy / len;
const L = 100000;
const el: CADElement = {
id: uid('ray'),
type: 'line',
layerId: 'defpoints',
x: rayBase.x,
y: rayBase.y,
width: 0,
height: 0,
properties: {
...constructionProps(),
x1: rayBase.x,
y1: rayBase.y,
x2: rayBase.x + ux * L,
y2: rayBase.y + uy * L,
},
} as unknown as CADElement;
c.doc.transact(() => c.doc!.addElement(el));
rayBase = null;
ctx.setPreview?.(null);
ctx.setStatus(`Strahl erstellt (${el.id})`);
},
move(e, ctx) {
const c = ctx as FullToolContext;
if (!rayBase) return;
const dx = e.world.x - rayBase.x;
const dy = e.world.y - rayBase.y;
const len = Math.hypot(dx, dy);
if (len < 0.001) return;
const ux = dx / len, uy = dy / len;
const L = 100000;
ctx.setPreview?.({
id: '__preview_ray__',
type: 'line',
layerId: 'defpoints',
x: rayBase.x,
y: rayBase.y,
width: 0,
height: 0,
properties: {
...constructionProps(),
x1: rayBase.x,
y1: rayBase.y,
x2: rayBase.x + ux * L,
y2: rayBase.y + uy * L,
},
} as unknown as CADElement);
},
cancel(ctx) {
rayBase = null;
ctx.setPreview?.(null);
ctx.setStatus('Strahl abgebrochen');
},
},
};
export const coreDrawingPlugin: PluginV2 = { export const coreDrawingPlugin: PluginV2 = {
manifest: { manifest: {
id: 'core-drawing', id: 'core-drawing',
name: 'Zeichnen (Kern)', name: 'Zeichnen (Kern)',
version: '1.4.0', version: '1.5.0',
author: 'web-cad team', author: 'web-cad team',
description: 'Zeichenwerkzeuge V2: Linie, Rechteck, Kreis, Bogen, Polylinie, Polygon, RevCloud.', description: 'Zeichenwerkzeuge V2: Linie, Rechteck, Kreis, Bogen, Polylinie, Polygon, RevCloud.',
category: 'tools', category: 'tools',
enabledByDefault: true, enabledByDefault: true,
}, },
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool, revcloudTool, ellipseTool, splineTool], tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool, revcloudTool, ellipseTool, splineTool, pointTool, xlineTool, rayTool],
}; };
+147
View File
@@ -0,0 +1,147 @@
/**
* Task D3 Point/XLine/Ray: Konstruktions-Elemente.
*
* pointTool: Klick-Platzierer (kleiner Kreis mit point:true-Marker,
* r=1.5 wie DXF-POINT-Import). xlineTool: 2 Klicks (Basis+Richtung),
* sehr lange Linie in BEIDE Richtungen. rayTool: halblang (nur eine
* Richtung). Alle mit construction:true und auf defpoints-Layer
* (auto-angelegt falls fehlt).
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { pointTool, xlineTool, rayTool } from '../src/plugins/builtin/core-drawing';
import { CADDocument } from '../src/kernel/document/CADDocument';
import { createInMemoryDoc } from './helpers/inMemoryDoc';
import type { CADElement, Pt } from '../src/types/cad.types';
function mkCtx() {
const { ydoc } = createInMemoryDoc();
const doc = new CADDocument(ydoc);
const previews: (CADElement | null)[] = [];
const statuses: string[] = [];
const ctx: any = {
doc,
options: {},
setStatus: (m: string) => statuses.push(m),
setPreview: (el: CADElement | null) => previews.push(el),
};
return { ctx, doc, previews, statuses };
}
function pe(x: number, y: number): { world: Pt } {
return { world: { x, y } } as never;
}
beforeEach(() => {
pointTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
xlineTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
rayTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
});
describe('D3: pointTool', () => {
it('Klick platziert Kreis mit point-Marker (r=1.5, wie DXF POINT)', () => {
const { ctx, doc } = mkCtx();
pointTool.handlers.down!(pe(55, 66), ctx);
const els = doc.getAllElements();
expect(els).toHaveLength(1);
const el = els[0];
expect(el.type).toBe('circle');
expect(el.x).toBe(55);
expect(el.y).toBe(66);
expect(el.properties.radius).toBe(1.5);
expect(el.properties.point).toBe(true);
});
it('defpoints-Layer wird automatisch angelegt falls fehlt', () => {
const { ctx, doc } = mkCtx();
expect(doc.getLayer('defpoints')).toBeUndefined();
pointTool.handlers.down!(pe(0, 0), ctx);
expect(doc.getLayer('defpoints')).toBeDefined();
expect(doc.getLayer('defpoints')!.name).toBe('defpoints');
});
it('Element landet auf defpoints-Layer mit construction:true', () => {
const { ctx, doc } = mkCtx();
pointTool.handlers.down!(pe(10, 10), ctx);
const el = doc.getAllElements()[0];
expect(el.layerId).toBe('defpoints');
expect(el.properties.construction).toBe(true);
});
});
describe('D3: xlineTool (Konstruktionslinie, beide Richtungen)', () => {
it('2 Klicks erzeugen sehr lange Linie durch beide Punkte in BEIDE Richtungen', () => {
const { ctx, doc } = mkCtx();
xlineTool.handlers.down!(pe(0, 0), ctx);
xlineTool.handlers.down!(pe(10, 10), ctx);
const els = doc.getAllElements();
expect(els).toHaveLength(1);
const el = els[0];
expect(el.type).toBe('line');
const dx = (el.properties.x2 as number) - (el.properties.x1 as number);
const dy = (el.properties.y2 as number) - (el.properties.y1 as number);
// Sehr lange: Länge >> Distanz der beiden Klicks (14.14)
expect(Math.hypot(dx, dy)).toBeGreaterThan(100000);
// Mittelpunkt der Linie = Basispunkt
expect((el.properties.x1 as number) + dx / 2).toBeCloseTo(0, 3);
expect((el.properties.y1 as number) + dy / 2).toBeCloseTo(0, 3);
});
it('Element auf defpoints-Layer mit construction:true', () => {
const { ctx, doc } = mkCtx();
xlineTool.handlers.down!(pe(0, 0), ctx);
xlineTool.handlers.down!(pe(5, 0), ctx);
const el = doc.getAllElements()[0];
expect(el.layerId).toBe('defpoints');
expect(el.properties.construction).toBe(true);
});
it('Preview bei move nach erstem Klick', () => {
const { ctx, previews } = mkCtx();
xlineTool.handlers.down!(pe(0, 0), ctx);
xlineTool.handlers.move!(pe(50, 50), ctx);
expect(previews.length).toBeGreaterThanOrEqual(1);
});
it('Erster Klick ohne zweiten erzeugt NICHTS (Session-State)', () => {
const { ctx, doc } = mkCtx();
xlineTool.handlers.down!(pe(0, 0), ctx);
expect(doc.getAllElements()).toHaveLength(0);
});
});
describe('D3: rayTool (Strahl, nur EINE Richtung)', () => {
it('2 Klicks erzeugen Linie ab Basispunkt NUR in Zugrichtung', () => {
const { ctx, doc } = mkCtx();
rayTool.handlers.down!(pe(0, 0), ctx);
rayTool.handlers.down!(pe(10, 0), ctx);
const el = doc.getAllElements()[0];
expect(el.type).toBe('line');
expect(el.properties.x1).toBe(0);
expect(el.properties.y1).toBe(0);
// Endpunkt weit in +x-Richtung
expect((el.properties.x2 as number)).toBeGreaterThanOrEqual(100000);
// NICHT in Gegenrichtung: x1 ist exakt der Basispunkt
expect(el.properties.x1).toBe(0);
});
it('construction:true + defpoints-Layer', () => {
const { ctx, doc } = mkCtx();
rayTool.handlers.down!(pe(0, 0), ctx);
rayTool.handlers.down!(pe(0, 10), ctx);
const el = doc.getAllElements()[0];
expect(el.layerId).toBe('defpoints');
expect(el.properties.construction).toBe(true);
});
});
describe('D3: Manifeste', () => {
it('ids/ribbonTab/tags korrekt', () => {
expect(pointTool.manifest.id).toBe('point');
expect(xlineTool.manifest.id).toBe('xline');
expect(rayTool.manifest.id).toBe('ray');
for (const t of [pointTool, xlineTool, rayTool]) {
expect(t.manifest.ribbonTab).toBe('canvas');
expect((t.manifest.tags ?? [])).toContain('basic');
}
});
});