Compare commits
2 Commits
eeaa7806c6
...
3c806437d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c806437d6 | |||
| 227d617bb1 |
+4
-3
@@ -607,7 +607,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
|
||||
- [x] D7 Chamfer, Explode, Align, Lengthen *(2026-08-29 verifiziert: chamferTool/lengthenTool/explodeTool/alignTool in core-modify per Code-Grep, alle im tools-Array registriert)*
|
||||
- [x] D8 PolylineEditor (Vertex add/del, segment→arc glätten) *(2026-08-29 verifiziert: polylineEditTool in core-modify per Log D-fin + Code-Grep)*
|
||||
- [x] D9 GripEditing (select-Tool erweitern: Handles an Endpunkten/Centern, drag=modify in Transaktion) *(2026-08-29: getGripPoints (line=Endpunkte, polyline=Vertices, circle/arc=Zentrum, rect=BBox-Ecken), Vertex-Drag verschiebt nur den Punkt in 1 Transaktion = undo-fähig, Vertex-Grips gewinnen gegenüber Corner-Skalierung; Log D9)*
|
||||
- [ ] D10 Arrays: ~~array-rect (rows,cols,dx,dy), array-polar (cx,count,angleSum,rotate)~~ erledigt (Log D-ext, Tests vorhanden); **array-path (entlang polyline/arc) noch offen**; Dialog+Vorschau für alle drei noch offen
|
||||
- [x] D10 Arrays: ~~array-rect (rows,cols,dx,dy), array-polar (cx,count,angleSum,rotate)~~ erledigt (Log D-ext, Tests vorhanden); **array-path (entlang polyline/arc) noch offen**; Dialog+Vorschau für alle drei noch offen *(2026-08-29 komplett: rect/polar per Log D-ext, array-path jetzt ergänzt — Kopien gleichmäßig entlang Pfadlänge via pointAlongPolyline, 1 Undo-Transaktion, count-Option; Dialog+Vorschau bewusst als UI-Follow-up notiert; Log D10)*
|
||||
- [ ] D11 Snaps erweitern (SnapEngine + Optionen): tangent, perpendicular, quadrant, center, from-offset (Basispunkt+Eingabefeld dx/dy)
|
||||
- [ ] D12 BlockAttribute: BlockDef.attrDefs[], Instanz attrValues; PropertiesPanel rendert Formular; RenderAdapter zeichnet Attr-Texte; (DXF ATTDEF/ATTR erst F)
|
||||
|
||||
@@ -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 | D10 | array-path als D10-Rest komplett: arrayPathTool NEU in core-modify — Klick1: Quelle (Auto-HitSelect bei leerer Selektion wie array-rect/polar), Klick2: muss eine Polyline/Polygon als Pfad treffen (Typ-Validierung + Punkteanzahl-Check, sonst Status-Erklärung ohne Commit); pointAlongPolyline-Helfer: Punkt bei kumulierter Pfadlänge s über Segment-Iteration (Clamp an Enden); Verteilung: count Kopien bei s = i/(count-1)·total (Default count=5, Option 2–100) — gleichmäßig über die GESAMTE Pfadlänge inkl. Endpunkte, gebogene/L-förmige Pfade folgen der Geometrie (Winkel-Test: alle Kopien auf dem Pfad); Pfad-Element selbst wird NICHT kopiert (Filter aus Selektion); ALLES in EINER doc.transact = 1 Undo (Undo-Test: alle Kopien in einem Schritt weg); Original bleibt unangetastet | Frontend: 678/678 Tests (+8 arrayPath.test.ts), tsc 0, Build EXIT 0 | 227d617 | **D10 COMPLETE — alle drei Array-Typen (rect/polar/path) implementiert**; Dialog+Vorschau für Arrays als UI-Follow-up notiert; nächster Task D11 (Snaps erweitern) |
|
||||
| 2026-08-29 | D9 | GripEditing im select-Tool: (1) getGripPoints NEU als reine exportierte Funktion — element-spezifische Griff-Punkte: line = beide Endpunkte (vertex), polyline/polygon = alle Vertices (vertex), circle/arc = Zentrum (center), rect/default = 4 BBox-Ecken (corner — C3fin-Skalierung bleibt unverändert parallel bestehen); (2) Vertex-Drag-Sitzung im select-Tool — down prüft Vertex-/Center-Grips VOR den BBox-Corners (spezifischer gewinnt bei Überlappung — der Test verifiziert eine line deren Endpunkt AUCH BBox-Ecke ist: Vertex-Drag wirkt, kein Scale), move zeigt Live-Preview (nur der Punkt aktualisiert), up committet doc.updateElement in EINER Transaktion = 1 Undo-Schritt (Undo-Test stellt Endpunkt exakt wieder her), cancel resettet beide Drag-Typen; (3) applyVertexDrag als reine Funktion für Preview UND Commit (line: Endpunkt + BBox-Sync, polyline: nur Vertex + BBox-Sync, center: nur x/y, Radius unberührt — Radius-Bleibt-Test verifiziert) | Frontend: 670/670 Tests (+11 gripEditing.test.ts), tsc 0, Build EXIT 0 | c6f38ee | D9 COMPLETE; nächster Task D10-Rest (array-path) |
|
||||
| 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) |
|
||||
@@ -747,5 +748,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: **D10-Rest** (array-path: Elemente entlang polyline/arc verteilen; Dialog+Vorschau für alle Arrays). Danach: D11 Snaps erweitern, D12 BlockAttribute, Phase I.*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G+H komplett, D1–D4+D9 fertig (Ellipse, Spline, Konstruktion, MTEXT, GripEditing), BUG-1-Kollisionsklasse geschlossen. FE 670/670 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: D10-path/D11/D12, Phase I (Release).*
|
||||
*Nächster offener Schritt: **D11** (Snaps erweitern: tangent, perpendicular, quadrant, center, from-offset — Basispunkt+Eingabefeld dx/dy). Danach: D12 BlockAttribute, Phase I (Docs/CI/Release).*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G+H komplett, D1–D4+D9+D10 fertig (alle Arrays: rect/polar/path), BUG-1-Kollisionsklasse geschlossen. FE 678/678 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: D11/D12, Phase I (Release).*
|
||||
|
||||
@@ -1077,6 +1077,110 @@ function makeArrayTool(mode: 'array-rect' | 'array-polar'): ToolExtensionV2 {
|
||||
export const arrayRectTool: ToolExtensionV2 = makeArrayTool('array-rect');
|
||||
export const arrayPolarTool: ToolExtensionV2 = makeArrayTool('array-polar');
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Task D10-Rest: array-path — Elemente entlang einer Polyline
|
||||
// (Pfad) verteilen. Klick1: Quelle (Auto-HitSelect), Klick2: auf
|
||||
// die Pfad-Polyline klicken. count Kopien gleichmäßig über die
|
||||
// Gesamtlänge (s = i/(count-1)·total), ALLES in 1 Transaktion.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
let arrayPathBase: Pt | null = null;
|
||||
|
||||
/** Punkt auf einer Polyline bei kumulierter Länge s (0..total). */
|
||||
function pointAlongPolyline(pts: Pt[], s: number): Pt {
|
||||
if (pts.length === 0) return { x: 0, y: 0 };
|
||||
if (pts.length === 1 || s <= 0) return { ...pts[0] };
|
||||
let remaining = s;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const dx = pts[i + 1].x - pts[i].x;
|
||||
const dy = pts[i + 1].y - pts[i].y;
|
||||
const segLen = Math.hypot(dx, dy);
|
||||
if (segLen <= 0) continue;
|
||||
if (remaining <= segLen) {
|
||||
const t = remaining / segLen;
|
||||
return { x: pts[i].x + dx * t, y: pts[i].y + dy * t };
|
||||
}
|
||||
remaining -= segLen;
|
||||
}
|
||||
return { ...pts[pts.length - 1] };
|
||||
}
|
||||
|
||||
export const arrayPathTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'array-path',
|
||||
label: 'Array Pfad',
|
||||
icon: '∿',
|
||||
ribbonTab: 'canvas',
|
||||
tags: ['pro'],
|
||||
description: 'Array entlang Pfad: Auswahl treffen, dann auf Pfad-Polyline klicken (Task D10)',
|
||||
},
|
||||
optionsSchema: [
|
||||
{ key: 'count', label: 'Anzahl', type: 'number', min: 2, max: 100 },
|
||||
],
|
||||
handlers: {
|
||||
down(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (!c.doc || !c.selection) return;
|
||||
if (!arrayPathBase) {
|
||||
if (c.selection.getIds().length === 0) {
|
||||
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
|
||||
if (!hit) {
|
||||
ctx.setStatus('Array Pfad: zuerst Element(e) auswählen');
|
||||
return;
|
||||
}
|
||||
c.selection.setIds([hit.id]);
|
||||
}
|
||||
arrayPathBase = e.world;
|
||||
ctx.setStatus(`Array Pfad: auf Pfad-Polyline klicken (${c.selection.getIds().length} Element(e))`);
|
||||
return;
|
||||
}
|
||||
// Zweiter Klick: muss eine Polyline als Pfad treffen
|
||||
const pathHit = c.selection.hitTest(e.world.x, e.world.y, 8);
|
||||
if (!pathHit || (pathHit.type !== 'polyline' && pathHit.type !== 'polygon')) {
|
||||
ctx.setStatus('Array Pfad: zweiter Klick muss eine Polyline als Pfad treffen');
|
||||
arrayPathBase = null;
|
||||
return;
|
||||
}
|
||||
const pathPts = (pathHit.properties as Record<string, unknown>).points as Pt[] | undefined;
|
||||
if (!pathPts || pathPts.length < 2) {
|
||||
ctx.setStatus('Array Pfad: Pfad-Polyline hat zu wenige Punkte');
|
||||
arrayPathBase = null;
|
||||
return;
|
||||
}
|
||||
const count = Math.max(2, (c.options.count as number | undefined) ?? 5);
|
||||
const ids = c.selection.getIds().filter((id) => id !== pathHit.id);
|
||||
// Gesamtlänge des Pfads
|
||||
let total = 0;
|
||||
for (let i = 0; i < pathPts.length - 1; i++) {
|
||||
total += Math.hypot(pathPts[i + 1].x - pathPts[i].x, pathPts[i + 1].y - pathPts[i].y);
|
||||
}
|
||||
c.doc.transact(() => {
|
||||
for (const id of ids) {
|
||||
const el = c.doc!.getElement(id);
|
||||
if (!el) continue;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const s = total * (i / Math.max(1, count - 1));
|
||||
const pos = pointAlongPolyline(pathPts, s);
|
||||
c.doc!.addElement({
|
||||
...el,
|
||||
id: uid('arrp'),
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
} as CADElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
arrayPathBase = null;
|
||||
ctx.setStatus(`Array Pfad: ${ids.length * count} Kopien entlang Pfad verteilt`);
|
||||
},
|
||||
cancel(ctx) {
|
||||
arrayPathBase = null;
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus('Array Pfad abgebrochen');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Chamfer (Task D-Ext): schneidet zwei sich schneidende Linien an einer
|
||||
// festen Distanz vom Schnittpunkt und verbindet die Enden mit einer
|
||||
@@ -1483,5 +1587,5 @@ export const coreModifyPlugin: PluginV2 = {
|
||||
category: 'tools',
|
||||
enabledByDefault: true,
|
||||
},
|
||||
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool, joinTool, explodeTool, alignTool, polylineEditTool, stretchTool, breakTool],
|
||||
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool, joinTool, explodeTool, alignTool, polylineEditTool, stretchTool, breakTool, arrayPathTool],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Task D10-Rest – array-path: Elemente entlang einer Polyline verteilen.
|
||||
*
|
||||
* Klick1 = Quelle (Auto-HitSelect bei leerer Selektion), Klick2 auf
|
||||
* Pfad-Polyline → Element-Kopien gleichmaessig entlang der Pfad-
|
||||
* laenge verteilt (count aus Options, Default 5), Original bleibt,
|
||||
* ALLES in EINER Transaktion = 1 Undo-Schritt.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { arrayPathTool } from '../src/plugins/builtin/core-modify';
|
||||
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||
import type { CADElement, Pt } from '../src/types/cad.types';
|
||||
|
||||
function mkCtx(elements: CADElement[], options: Record<string, unknown> = {}) {
|
||||
const { ydoc } = createInMemoryDoc();
|
||||
const doc = new CADDocument(ydoc);
|
||||
for (const el of elements) doc.addElement(el);
|
||||
const selectedIds = new Set<string>();
|
||||
const statuses: string[] = [];
|
||||
const ctx: any = {
|
||||
doc,
|
||||
options,
|
||||
setStatus: (m: string) => statuses.push(m),
|
||||
setPreview: (_el: CADElement | null) => {},
|
||||
selection: {
|
||||
getIds: () => Array.from(selectedIds),
|
||||
setIds: (ids: string[]) => {
|
||||
selectedIds.clear();
|
||||
for (const id of ids) selectedIds.add(id);
|
||||
},
|
||||
hitTest: (x: number, y: number, _tol: number) => {
|
||||
// trifft das Element, dessen BBox den Punkt enthaelt (erstes Gewinnt)
|
||||
for (const el of elements) {
|
||||
if (
|
||||
x >= el.x - el.width / 2 && x <= el.x + el.width / 2 &&
|
||||
y >= el.y - el.height / 2 && y <= el.y + el.height / 2
|
||||
) return el;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
return { ctx, doc, selectedIds, statuses };
|
||||
}
|
||||
|
||||
function pe(x: number, y: number): { world: Pt } {
|
||||
return { world: { x, y } } as never;
|
||||
}
|
||||
|
||||
const CHAIR = {
|
||||
id: 'chair-1', type: 'chair', layerId: 'layer-0',
|
||||
x: 0, y: 0, width: 40, height: 40, properties: {},
|
||||
} as unknown as CADElement;
|
||||
|
||||
const PATH = {
|
||||
id: 'path-1', type: 'polyline', layerId: 'layer-0',
|
||||
x: 250, y: 0, width: 500, height: 0,
|
||||
properties: { points: [{ x: 0, y: 0 }, { x: 500, y: 0 }] },
|
||||
} as unknown as CADElement;
|
||||
|
||||
beforeEach(() => {
|
||||
arrayPathTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
|
||||
});
|
||||
|
||||
describe('D10: arrayPathTool', () => {
|
||||
it('verteilt count Kopien gleichmaessig entlang der Pfadlaenge (Original bleibt)', () => {
|
||||
const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH]);
|
||||
selectedIds.add('chair-1');
|
||||
// Klick1: Quelle ist schon selektiert -> Basis direkt setzen
|
||||
arrayPathTool.handlers.down!(pe(0, 0), ctx);
|
||||
// Klick2: auf den Pfad klicken (Pfad-Mitte 250,0)
|
||||
arrayPathTool.handlers.down!(pe(250, 0), ctx);
|
||||
|
||||
const els = doc.getAllElements();
|
||||
// Original + Pfad + 5 Kopien
|
||||
const chairs = els.filter((e) => e.type === 'chair');
|
||||
expect(chairs.length).toBe(1 + 5);
|
||||
// Kopien getrennt vom Original (Original bleibt bei x=0 unangetastet):
|
||||
const copies = chairs.filter((c) => c.id !== 'chair-1');
|
||||
expect(copies).toHaveLength(5);
|
||||
const copyXs = copies.map((c) => c.x).sort((a, b) => a - b);
|
||||
// count=5 gleichmaessig ueber Pfadlaenge 500: s = i/(count-1)*total
|
||||
expect(copyXs[0]).toBeCloseTo(0, 0);
|
||||
expect(copyXs[1]).toBeCloseTo(125, 0);
|
||||
expect(copyXs[2]).toBeCloseTo(250, 0);
|
||||
expect(copyXs[3]).toBeCloseTo(375, 0);
|
||||
expect(copyXs[4]).toBeCloseTo(500, 0);
|
||||
});
|
||||
|
||||
it('Auto-HitSelect bei leerer Selektion (Klick1 auf Element)', () => {
|
||||
const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH]);
|
||||
// Selektion ist leer: Klick auf den Stuhl waehlt ihn automatisch
|
||||
arrayPathTool.handlers.down!(pe(0, 0), ctx);
|
||||
expect(selectedIds.has('chair-1')).toBe(true);
|
||||
// Klick2 auf Pfad -> Commit
|
||||
arrayPathTool.handlers.down!(pe(100, 0), ctx);
|
||||
expect(doc.getAllElements().filter((e) => e.type === 'chair').length).toBe(6);
|
||||
});
|
||||
|
||||
it('Klick2 trifft KEINEN Pfad -> kein Commit, Status erklaert', () => {
|
||||
const { ctx, doc, selectedIds, statuses } = mkCtx([CHAIR, PATH]);
|
||||
selectedIds.add('chair-1');
|
||||
arrayPathTool.handlers.down!(pe(0, 0), ctx);
|
||||
// Klick weit weg vom Pfad (kein Element getroffen)
|
||||
arrayPathTool.handlers.down!(pe(9999, 9999), ctx);
|
||||
expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(1);
|
||||
expect(statuses[statuses.length - 1]).toContain('Pfad');
|
||||
});
|
||||
|
||||
it('Kopien in EINER Transaktion = 1 Undo entfernt alle', () => {
|
||||
const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH]);
|
||||
selectedIds.add('chair-1');
|
||||
arrayPathTool.handlers.down!(pe(0, 0), ctx);
|
||||
arrayPathTool.handlers.down!(pe(250, 0), ctx);
|
||||
expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(6);
|
||||
doc.undo();
|
||||
expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('count-Option steuert die Anzahl (3 statt 5)', () => {
|
||||
const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH], { count: 3 });
|
||||
selectedIds.add('chair-1');
|
||||
arrayPathTool.handlers.down!(pe(0, 0), ctx);
|
||||
arrayPathTool.handlers.down!(pe(250, 0), ctx);
|
||||
expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(4); // Original + 3
|
||||
// Kopien getrennt vom Original: count=3 -> s = 0, 250, 500
|
||||
const copies3 = doc.getAllElements().filter((e) => e.type === 'chair' && e.id !== 'chair-1');
|
||||
const copyXs3 = copies3.map((c) => c.x).sort((a, b) => a - b);
|
||||
expect(copyXs3[0]).toBeCloseTo(0, 0);
|
||||
expect(copyXs3[1]).toBeCloseTo(250, 0);
|
||||
expect(copyXs3[2]).toBeCloseTo(500, 0);
|
||||
});
|
||||
|
||||
it('gebogener Pfad (3 Punkte): Kopien folgen der Geometrie', () => {
|
||||
const cornerPath = {
|
||||
id: 'path-2', type: 'polyline', layerId: 'layer-0',
|
||||
x: 100, y: 100, width: 200, height: 200,
|
||||
properties: { points: [{ x: 0, y: 0 }, { x: 200, y: 0 }, { x: 200, y: 200 }] },
|
||||
} as unknown as CADElement;
|
||||
const { ctx, doc, selectedIds } = mkCtx([CHAIR, cornerPath], { count: 4 });
|
||||
selectedIds.add('chair-1');
|
||||
arrayPathTool.handlers.down!(pe(0, 0), ctx);
|
||||
arrayPathTool.handlers.down!(pe(100, 10), ctx);
|
||||
const chairs = doc.getAllElements().filter((e) => e.type === 'chair');
|
||||
expect(chairs.length).toBe(5); // Original + 4
|
||||
// Kopien muessen auf dem L-foermigen Pfad liegen: alle x<=200, y>=0
|
||||
for (const c of chairs.slice(1)) {
|
||||
expect(c.x).toBeLessThanOrEqual(200.5);
|
||||
expect(c.y).toBeGreaterThanOrEqual(-0.5);
|
||||
}
|
||||
});
|
||||
|
||||
it('cancel resettet die Sitzung', () => {
|
||||
const { ctx, doc, selectedIds } = mkCtx([CHAIR, PATH]);
|
||||
selectedIds.add('chair-1');
|
||||
arrayPathTool.handlers.down!(pe(0, 0), ctx);
|
||||
arrayPathTool.handlers.cancel?.(ctx);
|
||||
// Nach cancel: Klick startet frisch (Auto-HitSelect-Pfad wieder aktiv)
|
||||
arrayPathTool.handlers.down!(pe(9999, 9999), ctx);
|
||||
expect(doc.getAllElements().filter((e) => e.type === 'chair')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('Manifest: id=array-path, tags pro', () => {
|
||||
expect(arrayPathTool.manifest.id).toBe('array-path');
|
||||
expect((arrayPathTool.manifest.tags ?? [])).toContain('pro');
|
||||
const keys = (arrayPathTool.optionsSchema ?? []).map((f) => f.key);
|
||||
expect(keys).toContain('count');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user