Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eeaa7806c6 | |||
| c6f38eee5b |
+4
-3
@@ -606,7 +606,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
|
|||||||
- [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)*
|
- [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)*
|
||||||
- [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] 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] D8 PolylineEditor (Vertex add/del, segment→arc glätten) *(2026-08-29 verifiziert: polylineEditTool in core-modify per Log D-fin + Code-Grep)*
|
||||||
- [ ] D9 GripEditing (select-Tool erweitern: Handles an Endpunkten/Centern, drag=modify in Transaktion)
|
- [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
|
- [ ] 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
|
||||||
- [ ] D11 Snaps erweitern (SnapEngine + Optionen): tangent, perpendicular, quadrant, center, from-offset (Basispunkt+Eingabefeld dx/dy)
|
- [ ] 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)
|
- [ ] 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 |
|
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
| 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 | 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 | 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) |
|
||||||
@@ -746,5 +747,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: **D9** (GripEditing: select-Tool erweitern — Handles an Endpunkten/Centern, drag=modify in Transaktion). Danach: D10 array-path, D11 Snaps, D12 BlockAttribute, Phase I.*
|
*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 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).*
|
*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).*
|
||||||
|
|||||||
@@ -82,6 +82,109 @@ function hitHandle(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Task D9: GripEditing — Vertex-/Center-Grips (reine Funktion).
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface GripPoint {
|
||||||
|
kind: 'vertex' | 'center' | 'corner';
|
||||||
|
point: Pt;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element-spezifische Griff-Punkte (Task D9):
|
||||||
|
* line = beide Endpunkte, polyline/polygon = alle Vertices,
|
||||||
|
* circle/arc = Zentrum, rect/default = 4 BBox-Ecken (C3fin-Skalierung).
|
||||||
|
*/
|
||||||
|
export function getGripPoints(el: CADElement): GripPoint[] {
|
||||||
|
const props = el.properties as Record<string, unknown>;
|
||||||
|
const type = String(el.type);
|
||||||
|
if (
|
||||||
|
type === 'line' &&
|
||||||
|
props.x1 !== undefined && props.y1 !== undefined &&
|
||||||
|
props.x2 !== undefined && props.y2 !== undefined
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
{ kind: 'vertex', point: { x: props.x1 as number, y: props.y1 as number }, index: 0 },
|
||||||
|
{ kind: 'vertex', point: { x: props.x2 as number, y: props.y2 as number }, index: 1 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (type === 'polyline' || type === 'polygon') {
|
||||||
|
const pts = (props.points as Pt[] | undefined) ?? [];
|
||||||
|
return pts.map((p, i) => ({ kind: 'vertex' as const, point: { x: p.x, y: p.y }, index: i }));
|
||||||
|
}
|
||||||
|
if (type === 'circle' || type === 'arc') {
|
||||||
|
return [{ kind: 'center', point: { x: el.x, y: el.y }, index: 0 }];
|
||||||
|
}
|
||||||
|
// rect und alle anderen: BBox-Ecken (Corner-Drag/Skalierung, C3fin)
|
||||||
|
const minX = el.x - el.width / 2;
|
||||||
|
const minY = el.y - el.height / 2;
|
||||||
|
const maxX = el.x + el.width / 2;
|
||||||
|
const maxY = el.y + el.height / 2;
|
||||||
|
return [
|
||||||
|
{ kind: 'corner', point: { x: minX, y: minY }, index: 0 },
|
||||||
|
{ kind: 'corner', point: { x: maxX, y: minY }, index: 1 },
|
||||||
|
{ kind: 'corner', point: { x: maxX, y: maxY }, index: 2 },
|
||||||
|
{ kind: 'corner', point: { x: minX, y: maxY }, index: 3 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wendet eine Vertex-/Center-Drag-Position an (Task D9) und liefert
|
||||||
|
* das aktualisierte Element (reine Funktion — für Preview UND Commit).
|
||||||
|
*/
|
||||||
|
function applyVertexDrag(
|
||||||
|
el: CADElement,
|
||||||
|
drag: { el: CADElement; gripKind: 'vertex' | 'center'; index: number },
|
||||||
|
wx: number,
|
||||||
|
wy: number,
|
||||||
|
): CADElement {
|
||||||
|
const props = { ...(el.properties as Record<string, unknown>) };
|
||||||
|
const type = String(el.type);
|
||||||
|
if (drag.gripKind === 'center') {
|
||||||
|
return { ...el, x: wx, y: wy, properties: props as CADElement['properties'] } as CADElement;
|
||||||
|
}
|
||||||
|
if (type === 'line') {
|
||||||
|
if (drag.index === 0) { props.x1 = wx; props.y1 = wy; }
|
||||||
|
else { props.x2 = wx; props.y2 = wy; }
|
||||||
|
// BBox aktualisieren
|
||||||
|
const x1 = props.x1 as number, y1 = props.y1 as number, x2 = props.x2 as number, y2 = props.y2 as number;
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
x: (x1 + x2) / 2,
|
||||||
|
y: (y1 + y2) / 2,
|
||||||
|
width: Math.abs(x2 - x1),
|
||||||
|
height: Math.abs(y2 - y1),
|
||||||
|
properties: props as CADElement['properties'],
|
||||||
|
} as CADElement;
|
||||||
|
}
|
||||||
|
if (type === 'polyline' || type === 'polygon') {
|
||||||
|
const pts = [...((props.points as Pt[] | undefined) ?? [])];
|
||||||
|
if (drag.index < pts.length) pts[drag.index] = { x: wx, y: wy };
|
||||||
|
props.points = pts;
|
||||||
|
const xs = pts.map((p) => p.x), ys = pts.map((p) => p.y);
|
||||||
|
const minX = Math.min(...xs), minY = Math.min(...ys);
|
||||||
|
const maxX = Math.max(...xs), maxY = Math.max(...ys);
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
x: minX + (maxX - minX) / 2,
|
||||||
|
y: minY + (maxY - minY) / 2,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY,
|
||||||
|
properties: props as CADElement['properties'],
|
||||||
|
} as CADElement;
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vertex-/Center-Drag-Sitzung (Task D9). */
|
||||||
|
let vertexDrag: {
|
||||||
|
el: CADElement;
|
||||||
|
gripKind: 'vertex' | 'center';
|
||||||
|
index: number;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
export const selectTool: ToolExtensionV2 = {
|
export const selectTool: ToolExtensionV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'select',
|
id: 'select',
|
||||||
@@ -101,6 +204,28 @@ export const selectTool: ToolExtensionV2 = {
|
|||||||
const subtractive = e.ctrl;
|
const subtractive = e.ctrl;
|
||||||
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
||||||
|
|
||||||
|
// ── Task D9: Vertex-/Center-Grip-Prüfung VOR BBox-Corners ──
|
||||||
|
// (spezifischere Grips gewinnen gegenüber Corner-Skalierung)
|
||||||
|
if (!additive && !subtractive && c.doc) {
|
||||||
|
const GRIP_TOL = 8;
|
||||||
|
for (const id of bridge.getIds()) {
|
||||||
|
const el = c.doc.getElement(id);
|
||||||
|
if (!el) continue;
|
||||||
|
for (const g of getGripPoints(el)) {
|
||||||
|
if (g.kind === 'corner') continue; // Corner unten (C3fin)
|
||||||
|
if (
|
||||||
|
Math.abs(e.world.x - g.point.x) <= GRIP_TOL &&
|
||||||
|
Math.abs(e.world.y - g.point.y) <= GRIP_TOL
|
||||||
|
) {
|
||||||
|
vertexDrag = { el, gripKind: g.kind, index: g.index };
|
||||||
|
const what = g.kind === 'center' ? 'Zentrum' : `Punkt ${g.index + 1}`;
|
||||||
|
ctx.setStatus(`${what} gegriffen — ziehen zum Verschieben`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Handle-Drag-Start (Task C3fin interaktiv) ──
|
// ── Handle-Drag-Start (Task C3fin interaktiv) ──
|
||||||
// Wenn bereits Elemente selektiert sind und der Klick auf eine Ecke
|
// Wenn bereits Elemente selektiert sind und der Klick auf eine Ecke
|
||||||
// eines davon trifft → Handle-Drag starten statt neu zu wählen.
|
// eines davon trifft → Handle-Drag starten statt neu zu wählen.
|
||||||
@@ -133,6 +258,15 @@ export const selectTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
move(e, ctx) {
|
move(e, ctx) {
|
||||||
|
// ── Task D9: Vertex-Drag-Preview (nur der Punkt aktualisiert) ──
|
||||||
|
if (vertexDrag) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
const el = c.doc?.getElement(vertexDrag.el.id);
|
||||||
|
if (!el) return;
|
||||||
|
const updated = applyVertexDrag(el, vertexDrag, e.world.x, e.world.y);
|
||||||
|
c.setPreview?.(updated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Live-Skalierung während Handle-Drag:
|
// Live-Skalierung während Handle-Drag:
|
||||||
if (!handleDrag) return;
|
if (!handleDrag) return;
|
||||||
const c = ctx as FullToolContext;
|
const c = ctx as FullToolContext;
|
||||||
@@ -174,6 +308,26 @@ export const selectTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
up(e, ctx) {
|
up(e, ctx) {
|
||||||
|
// ── Task D9: Vertex-Commit (nur der Punkt, 1 Undo-Schritt) ──
|
||||||
|
if (vertexDrag) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
const el = c.doc?.getElement(vertexDrag.el.id);
|
||||||
|
if (el && c.doc) {
|
||||||
|
const updated = applyVertexDrag(el, vertexDrag, e.world.x, e.world.y);
|
||||||
|
c.doc.transact(() => {
|
||||||
|
c.doc!.updateElement(el.id, {
|
||||||
|
x: updated.x,
|
||||||
|
y: updated.y,
|
||||||
|
properties: updated.properties as CADElement['properties'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const what = vertexDrag.gripKind === 'center' ? 'Zentrum' : `Punkt ${vertexDrag.index + 1}`;
|
||||||
|
ctx.setStatus(`${what} verschoben`);
|
||||||
|
}
|
||||||
|
vertexDrag = null;
|
||||||
|
ctx.setPreview?.(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!handleDrag) return;
|
if (!handleDrag) return;
|
||||||
const c = ctx as FullToolContext;
|
const c = ctx as FullToolContext;
|
||||||
if (!c.doc) { handleDrag = null; return; }
|
if (!c.doc) { handleDrag = null; return; }
|
||||||
@@ -224,6 +378,7 @@ export const selectTool: ToolExtensionV2 = {
|
|||||||
|
|
||||||
cancel(ctx) {
|
cancel(ctx) {
|
||||||
handleDrag = null;
|
handleDrag = null;
|
||||||
|
vertexDrag = null;
|
||||||
ctx.setPreview?.(null);
|
ctx.setPreview?.(null);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* Task D9 – GripEditing: Vertex-/Center-Grips im select-Tool.
|
||||||
|
*
|
||||||
|
* getGripPoints: element-spezifische Griff-Punkte (line=Endpunkte,
|
||||||
|
* polyline=Vertices, circle/arc=Zentrum, rect=BBox-Ecken).
|
||||||
|
* Drag eines Vertex-Grips verschiebt NUR diesen Punkt in einer
|
||||||
|
* Transaktion (undo-fähig); BBox-Skalierung (C3fin) bleibt parallel
|
||||||
|
* für Elemente ohne Vertex-Grips (rect u.a.).
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { selectTool, getGripPoints } 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() {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const selectedIds = new Set<string>();
|
||||||
|
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),
|
||||||
|
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) => {
|
||||||
|
// Einfacher Hit-Test: trifft wenn ein Element-BBox den Punkt enthaelt
|
||||||
|
for (const el of doc.getAllElements()) {
|
||||||
|
const p = el.properties as Record<string, unknown>;
|
||||||
|
if (p.x1 !== undefined) {
|
||||||
|
const minX = Math.min(p.x1 as number, p.x2 as number);
|
||||||
|
const maxX = Math.max(p.x1 as number, p.x2 as number);
|
||||||
|
const minY = Math.min(p.y1 as number, p.y2 as number);
|
||||||
|
const maxY = Math.max(p.y1 as number, p.y2 as number);
|
||||||
|
if (x >= minX - tol && x <= maxX + tol && y >= minY - tol && y <= maxY + tol) return el;
|
||||||
|
}
|
||||||
|
if (x >= el.x - el.width / 2 - tol && x <= el.x + el.width / 2 + tol &&
|
||||||
|
y >= el.y - el.height / 2 - tol && y <= el.y + el.height / 2 + tol) return el;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return { ctx, doc, selectedIds, previews, statuses };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pe(x: number, y: number, mods: { shift?: boolean; ctrl?: boolean } = {}): { world: Pt; shift: boolean; ctrl: boolean } {
|
||||||
|
return { world: { x, y }, shift: mods.shift ?? false, ctrl: mods.ctrl ?? false } as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
selectTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getGripPoints (reine Funktion) ────────────────────────
|
||||||
|
|
||||||
|
describe('D9: getGripPoints', () => {
|
||||||
|
it('line: beide Endpunkte als Grips', () => {
|
||||||
|
const el = {
|
||||||
|
id: 'l1', type: 'line', layerId: 'l', x: 50, y: 50, width: 100, height: 0,
|
||||||
|
properties: { x1: 0, y1: 50, x2: 100, y2: 50 },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
const grips = getGripPoints(el);
|
||||||
|
expect(grips.map((g) => g.kind)).toEqual(['vertex', 'vertex']);
|
||||||
|
expect(grips[0].point).toEqual({ x: 0, y: 50 });
|
||||||
|
expect(grips[1].point).toEqual({ x: 100, y: 50 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('polyline: alle Vertices als Grips', () => {
|
||||||
|
const el = {
|
||||||
|
id: 'p1', type: 'polyline', layerId: 'l', x: 50, y: 0, width: 100, height: 100,
|
||||||
|
properties: { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 100 }] },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
const grips = getGripPoints(el);
|
||||||
|
expect(grips.length).toBe(3);
|
||||||
|
expect(grips.every((g) => g.kind === 'vertex')).toBe(true);
|
||||||
|
expect(grips[2].point).toEqual({ x: 100, y: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('circle: Zentrum als Center-Grip', () => {
|
||||||
|
const el = {
|
||||||
|
id: 'c1', type: 'circle', layerId: 'l', x: 30, y: 40, width: 20, height: 20,
|
||||||
|
properties: { radius: 10 },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
const grips = getGripPoints(el);
|
||||||
|
expect(grips).toHaveLength(1);
|
||||||
|
expect(grips[0].kind).toBe('center');
|
||||||
|
expect(grips[0].point).toEqual({ x: 30, y: 40 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rect: BBox-Ecken als Corner-Grips (C3fin bleibt)', () => {
|
||||||
|
const el = {
|
||||||
|
id: 'r1', type: 'rect', layerId: 'l', x: 50, y: 25, width: 100, height: 50,
|
||||||
|
properties: {},
|
||||||
|
} as unknown as CADElement;
|
||||||
|
const grips = getGripPoints(el);
|
||||||
|
expect(grips.length).toBe(4);
|
||||||
|
expect(grips.every((g) => g.kind === 'corner')).toBe(true);
|
||||||
|
expect(grips[0].point).toEqual({ x: 0, y: 0 });
|
||||||
|
expect(grips[2].point).toEqual({ x: 100, y: 50 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Grips tragen index fuer Drag-Zuordnung', () => {
|
||||||
|
const el = {
|
||||||
|
id: 'p1', type: 'polyline', layerId: 'l', x: 0, y: 0, width: 10, height: 10,
|
||||||
|
properties: { points: [{ x: 0, y: 0 }, { x: 10, y: 10 }] },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
const grips = getGripPoints(el);
|
||||||
|
expect(grips[0].index).toBe(0);
|
||||||
|
expect(grips[1].index).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Drag-Interaktion: Vertex-Zug (undo-faehig) ─────────────
|
||||||
|
|
||||||
|
describe('D9: Vertex-Drag im selectTool', () => {
|
||||||
|
it('Linien-Endpunkt ziehen verschiebt NUR diesen Punkt (1 Undo)', () => {
|
||||||
|
const { ctx, doc, selectedIds } = mkCtx();
|
||||||
|
const line = {
|
||||||
|
id: 'l1', type: 'line', layerId: 'l', x: 50, y: 0, width: 100, height: 0,
|
||||||
|
properties: { x1: 0, y1: 0, x2: 100, y2: 0 },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
doc.addElement(line);
|
||||||
|
selectedIds.add('l1');
|
||||||
|
|
||||||
|
// Klick auf Endpunkt (100,0) startet Vertex-Drag
|
||||||
|
selectTool.handlers.down!(pe(100, 0), ctx);
|
||||||
|
selectTool.handlers.up!(pe(120, 30), ctx);
|
||||||
|
|
||||||
|
const updated = doc.getElement('l1')!;
|
||||||
|
expect(updated.properties.x2).toBe(120);
|
||||||
|
expect(updated.properties.y2).toBe(30);
|
||||||
|
// Anderer Endpunkt unveraendert
|
||||||
|
expect(updated.properties.x1).toBe(0);
|
||||||
|
expect(updated.properties.y1).toBe(0);
|
||||||
|
// Undo stellt her
|
||||||
|
doc.undo();
|
||||||
|
const restored = doc.getElement('l1')!;
|
||||||
|
expect(restored.properties.x2).toBe(100);
|
||||||
|
expect(restored.properties.y2).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Polyline-Vertex ziehen aktualisiert nur diesen Vertex', () => {
|
||||||
|
const { ctx, doc, selectedIds } = mkCtx();
|
||||||
|
const poly = {
|
||||||
|
id: 'p1', type: 'polyline', layerId: 'l', x: 50, y: 50, width: 100, height: 100,
|
||||||
|
properties: { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 100 }] },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
doc.addElement(poly);
|
||||||
|
selectedIds.add('p1');
|
||||||
|
|
||||||
|
// Mittleren Vertex (100,0) greifen und auf (100,50) ziehen
|
||||||
|
selectTool.handlers.down!(pe(100, 0), ctx);
|
||||||
|
selectTool.handlers.up!(pe(100, 50), ctx);
|
||||||
|
|
||||||
|
const pts = doc.getElement('p1')!.properties.points as Pt[];
|
||||||
|
expect(pts[1]).toEqual({ x: 100, y: 50 });
|
||||||
|
expect(pts[0]).toEqual({ x: 0, y: 0 });
|
||||||
|
expect(pts[2]).toEqual({ x: 100, y: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Kreis-Zentrum ziehen verschiebt x/y (Radius bleibt)', () => {
|
||||||
|
const { ctx, doc, selectedIds } = mkCtx();
|
||||||
|
const circle = {
|
||||||
|
id: 'c1', type: 'circle', layerId: 'l', x: 30, y: 40, width: 20, height: 20,
|
||||||
|
properties: { radius: 10 },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
doc.addElement(circle);
|
||||||
|
selectedIds.add('c1');
|
||||||
|
|
||||||
|
selectTool.handlers.down!(pe(30, 40), ctx);
|
||||||
|
selectTool.handlers.up!(pe(60, 80), ctx);
|
||||||
|
|
||||||
|
const updated = doc.getElement('c1')!;
|
||||||
|
expect(updated.x).toBe(60);
|
||||||
|
expect(updated.y).toBe(80);
|
||||||
|
expect(updated.properties.radius).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('BBox-Corner-Drag bei rect funktioniert weiterhin (Skalierung)', () => {
|
||||||
|
const { ctx, doc, selectedIds } = mkCtx();
|
||||||
|
const rect = {
|
||||||
|
id: 'r1', type: 'rect', layerId: 'l', x: 50, y: 25, width: 100, height: 50,
|
||||||
|
properties: {},
|
||||||
|
} as unknown as CADElement;
|
||||||
|
doc.addElement(rect);
|
||||||
|
selectedIds.add('r1');
|
||||||
|
|
||||||
|
// Ecke unten-rechts (100,50) greifen und auf (150,75) ziehen
|
||||||
|
selectTool.handlers.down!(pe(100, 50), ctx);
|
||||||
|
selectTool.handlers.up!(pe(150, 75), ctx);
|
||||||
|
|
||||||
|
const updated = doc.getElement('r1')!;
|
||||||
|
expect(updated.width).toBeCloseTo(150, 0);
|
||||||
|
expect(updated.height).toBeCloseTo(75, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Vertex-Grip gewinnt gegen BBox-Corner bei Ueberlappung (line-Endpunkt am BBox-Rand)', () => {
|
||||||
|
// line (0,0)-(100,0): Endpunkt (0,0) ist AUCH BBox-Ecke.
|
||||||
|
// Der Drag muss als Vertex (Punkt verschieben) wirken, nicht als Scale.
|
||||||
|
const { ctx, doc, selectedIds } = mkCtx();
|
||||||
|
const line = {
|
||||||
|
id: 'l1', type: 'line', layerId: 'l', x: 50, y: 0, width: 100, height: 0,
|
||||||
|
properties: { x1: 0, y1: 0, x2: 100, y2: 0 },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
doc.addElement(line);
|
||||||
|
selectedIds.add('l1');
|
||||||
|
|
||||||
|
selectTool.handlers.down!(pe(0, 0), ctx);
|
||||||
|
selectTool.handlers.up!(pe(-30, -20), ctx);
|
||||||
|
|
||||||
|
const updated = doc.getElement('l1')!;
|
||||||
|
// x1/y1 verschoben, x2 unveraendert → Vertex-Drag, kein Scale
|
||||||
|
expect(updated.properties.x1).toBe(-30);
|
||||||
|
expect(updated.properties.y1).toBe(-20);
|
||||||
|
expect(updated.properties.x2).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Preview bei Vertex-Drag zeigt aktualisiertes Element', () => {
|
||||||
|
const { ctx, doc, selectedIds, previews } = mkCtx();
|
||||||
|
const line = {
|
||||||
|
id: 'l1', type: 'line', layerId: 'l', x: 50, y: 0, width: 100, height: 0,
|
||||||
|
properties: { x1: 0, y1: 0, x2: 100, y2: 0 },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
doc.addElement(line);
|
||||||
|
selectedIds.add('l1');
|
||||||
|
|
||||||
|
selectTool.handlers.down!(pe(100, 0), ctx);
|
||||||
|
selectTool.handlers.move!(pe(90, 40), ctx);
|
||||||
|
expect(previews.length).toBeGreaterThanOrEqual(1);
|
||||||
|
const last = previews[previews.length - 1];
|
||||||
|
expect(last).not.toBeNull();
|
||||||
|
const lastProps = last!.properties as Record<string, unknown>;
|
||||||
|
expect(lastProps.x2).toBe(90);
|
||||||
|
expect(lastProps.y2).toBe(40);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user