245 lines
9.4 KiB
TypeScript
245 lines
9.4 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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);
|
|||
|
|
});
|
|||
|
|
});
|