task(A4.7): migrate rotate/scale/mirror tools to v2 (selection-based transforms)
This commit is contained in:
@@ -1,16 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* core-modify – Built-in V2-Plugin (Task A3 Pilot).
|
* core-modify – Built-in V2-Plugin.
|
||||||
*
|
*
|
||||||
* Werkzeuge: select (Pilotportierung aus Legacy handleSelectDown).
|
* Werkzeuge:
|
||||||
* Pilot-Scope: Klick-Auswahl inkl. Group-Bewusstheit via SelectionBridge;
|
* - select (A3): Klick-Auswahl mit Shift/Ctrl
|
||||||
* Marquee/Box-Auswahl Migration folgt in A4 (Legacy bleibt parallel aktiv).
|
* - hatch (A4.5): Schraffur via doc.updateElement
|
||||||
|
* - move/copy (A4.6): selektionsbasiert, Basis→Ziel
|
||||||
|
* - rotate/scale/mirror (A4.7): gleiche Selektions-Konvention,
|
||||||
|
* Legacy-Transform-Mathematik aus handleModifyMove übernommen
|
||||||
|
*
|
||||||
|
* Alle Commits laufen über CADDocument → kollaborativ korrektes Undo.
|
||||||
*/
|
*/
|
||||||
import type { PluginV2, ToolExtensionV2 } from '../../types';
|
import type { PluginV2, ToolExtensionV2 } from '../../types';
|
||||||
|
import type { FullToolContext } from '../../types';
|
||||||
import type { CADElement } from '../../../types/cad.types';
|
import type { CADElement } from '../../../types/cad.types';
|
||||||
|
import {
|
||||||
|
moveElement,
|
||||||
|
rotateElement,
|
||||||
|
scaleElement,
|
||||||
|
mirrorElement,
|
||||||
|
angleBetween,
|
||||||
|
distance,
|
||||||
|
} from '../../../tools/modification/geometry';
|
||||||
|
import type { Pt } from '../../../tools/modification/geometry';
|
||||||
|
|
||||||
/**
|
const uid = (prefix: string): string =>
|
||||||
* select-Werkzeug: Klick wählt Element direkt (Shift=additiv, Ctrl=subtraktiv).
|
`${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
||||||
*/
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// select (Task A3 – Pilot)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
export const selectTool: ToolExtensionV2 = {
|
export const selectTool: ToolExtensionV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'select',
|
id: 'select',
|
||||||
@@ -23,25 +41,21 @@ export const selectTool: ToolExtensionV2 = {
|
|||||||
optionsSchema: [],
|
optionsSchema: [],
|
||||||
handlers: {
|
handlers: {
|
||||||
down(e, ctx) {
|
down(e, ctx) {
|
||||||
const bridge = (ctx as import('../../types').FullToolContext).selection;
|
const bridge = (ctx as FullToolContext).selection;
|
||||||
if (!bridge) return;
|
if (!bridge) return;
|
||||||
|
|
||||||
const additive = e.shift;
|
const additive = e.shift;
|
||||||
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);
|
||||||
|
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
// Leerer Klick: Selektion leeren (außer additiv)
|
|
||||||
if (!additive && !subtractive) bridge.setIds([]);
|
if (!additive && !subtractive) bridge.setIds([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ids = new Set(bridge.getIds());
|
let ids = new Set(bridge.getIds());
|
||||||
if (subtractive) {
|
if (subtractive) {
|
||||||
ids.delete(hit.id);
|
ids.delete(hit.id);
|
||||||
} else {
|
} else {
|
||||||
ids.add(hit.id);
|
ids.add(hit.id);
|
||||||
if (!additive) ids = new Set([hit.id]); // ohne Shift: nur das getroffene
|
if (!additive) ids = new Set([hit.id]);
|
||||||
}
|
}
|
||||||
bridge.setIds(Array.from(ids));
|
bridge.setIds(Array.from(ids));
|
||||||
ctx.setStatus(`Selektion: ${ids.size} Element(e)`);
|
ctx.setStatus(`Selektion: ${ids.size} Element(e)`);
|
||||||
@@ -49,11 +63,9 @@ export const selectTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// ─────────────────────────────────────────────────────────────
|
||||||
* hatch-Werkzeug (A4.5): Klick auf geschlossenes Element (rect/circle/polygon)
|
// hatch (Task A4.5)
|
||||||
* aktiviert Schraffur — Portierung aus Legacy handleHatchDown, aber jetzt via
|
// ─────────────────────────────────────────────────────────────
|
||||||
* doc.updateElement (eigene Undo-Transaktion statt Callback-Direktaufruf).
|
|
||||||
*/
|
|
||||||
export const hatchTool: ToolExtensionV2 = {
|
export const hatchTool: ToolExtensionV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'hatch', label: 'Schraffur', icon: '\u2593', ribbonTab: 'format', tags: ['pro'],
|
id: 'hatch', label: 'Schraffur', icon: '\u2593', ribbonTab: 'format', tags: ['pro'],
|
||||||
@@ -68,7 +80,7 @@ export const hatchTool: ToolExtensionV2 = {
|
|||||||
],
|
],
|
||||||
handlers: {
|
handlers: {
|
||||||
down(e, ctx) {
|
down(e, ctx) {
|
||||||
const c = ctx as import('../../types').FullToolContext;
|
const c = ctx as FullToolContext;
|
||||||
const bridge = c.selection;
|
const bridge = c.selection;
|
||||||
if (!bridge || !c.doc) return;
|
if (!bridge || !c.doc) return;
|
||||||
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
const hit = bridge.hitTest(e.world.x, e.world.y, 5);
|
||||||
@@ -86,20 +98,11 @@ export const hatchTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// ─────────────────────────────────────────────────────────────
|
||||||
* Move/Copy-Basispunkt (A4.6): erster down setzt ihn, zweiter down committet.
|
// Move/Copy (Task A4.6)
|
||||||
* Gemeinsam für beide Tools (klick-progressiv wie arc-Lektion).
|
// ─────────────────────────────────────────────────────────────
|
||||||
*/
|
let modBasePoint: Pt | null = null;
|
||||||
let modBasePoint: import('../../../tools/modification/geometry').Pt | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fabrik für move/copy-Werkzeuge (A4.6).
|
|
||||||
* Wirkt auf die AKTUELLE SELEKTION (SelectionBridge):
|
|
||||||
* - Klick 1: Basispunkt setzen
|
|
||||||
* - Klick 2: Commit — move: updateElement(x±dx,y±dy); copy: addElement der
|
|
||||||
* versetzten Kopien (neue IDs). Alles in EINER Transaktion = 1 Undo-Schritt.
|
|
||||||
* Legacy-Konvention: Versatzvektor = zweiterKlick − basis.
|
|
||||||
*/
|
|
||||||
function makeMoveCopyTool(mode: 'move' | 'copy'): ToolExtensionV2 {
|
function makeMoveCopyTool(mode: 'move' | 'copy'): ToolExtensionV2 {
|
||||||
const label = mode === 'move' ? 'Verschieben' : 'Kopieren';
|
const label = mode === 'move' ? 'Verschieben' : 'Kopieren';
|
||||||
return {
|
return {
|
||||||
@@ -109,18 +112,16 @@ function makeMoveCopyTool(mode: 'move' | 'copy'): ToolExtensionV2 {
|
|||||||
icon: mode === 'move' ? '\u27a4' : '\u29c9',
|
icon: mode === 'move' ? '\u27a4' : '\u29c9',
|
||||||
ribbonTab: 'canvas',
|
ribbonTab: 'canvas',
|
||||||
tags: ['basic'],
|
tags: ['basic'],
|
||||||
description: `${label}: Auswahl treffen, dann Basis→Ziel klicken`,
|
description: `${label}: Auswahl treffen, dann Basis\u2192Ziel klicken`,
|
||||||
},
|
},
|
||||||
optionsSchema: [],
|
optionsSchema: [],
|
||||||
handlers: {
|
handlers: {
|
||||||
down(e, ctx) {
|
down(e, ctx) {
|
||||||
const c = ctx as import('../../types').FullToolContext;
|
const c = ctx as FullToolContext;
|
||||||
if (!c.doc || !c.selection) return;
|
if (!c.doc || !c.selection) return;
|
||||||
|
|
||||||
if (!modBasePoint) {
|
if (!modBasePoint) {
|
||||||
const ids = c.selection.getIds();
|
const ids = c.selection.getIds();
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
// Komfort: nichts selektiert → hitTest und sofort auswählen
|
|
||||||
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
|
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
ctx.setStatus(`${label}: zuerst Element(e) auswählen`);
|
ctx.setStatus(`${label}: zuerst Element(e) auswählen`);
|
||||||
@@ -132,8 +133,6 @@ function makeMoveCopyTool(mode: 'move' | 'copy'): ToolExtensionV2 {
|
|||||||
ctx.setStatus(`${label}: Zielklick zum ${mode === 'move' ? 'Verschieben' : 'Kopieren'} (${c.selection.getIds().length} Elemente)`);
|
ctx.setStatus(`${label}: Zielklick zum ${mode === 'move' ? 'Verschieben' : 'Kopieren'} (${c.selection.getIds().length} Elemente)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zweiter Klick → Commit
|
|
||||||
const dx = e.world.x - modBasePoint.x;
|
const dx = e.world.x - modBasePoint.x;
|
||||||
const dy = e.world.y - modBasePoint.y;
|
const dy = e.world.y - modBasePoint.y;
|
||||||
const ids = c.selection.getIds();
|
const ids = c.selection.getIds();
|
||||||
@@ -144,13 +143,12 @@ function makeMoveCopyTool(mode: 'move' | 'copy'): ToolExtensionV2 {
|
|||||||
if (mode === 'move') {
|
if (mode === 'move') {
|
||||||
c.doc!.updateElement(id, { x: el.x + dx, y: el.y + dy });
|
c.doc!.updateElement(id, { x: el.x + dx, y: el.y + dy });
|
||||||
} else {
|
} else {
|
||||||
const copy: CADElement = {
|
c.doc!.addElement({
|
||||||
...el,
|
...el,
|
||||||
id: `cp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}_${ids.indexOf(id)}`,
|
id: uid('cp'),
|
||||||
x: el.x + dx,
|
x: el.x + dx,
|
||||||
y: el.y + dy,
|
y: el.y + dy,
|
||||||
};
|
});
|
||||||
c.doc!.addElement(copy);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -164,20 +162,127 @@ function makeMoveCopyTool(mode: 'move' | 'copy'): ToolExtensionV2 {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const moveTool: ToolExtensionV2 = makeMoveCopyTool('move');
|
export const moveTool: ToolExtensionV2 = makeMoveCopyTool('move');
|
||||||
export const copyTool: ToolExtensionV2 = makeMoveCopyTool('copy');
|
export const copyTool: ToolExtensionV2 = makeMoveCopyTool('copy');
|
||||||
|
|
||||||
/** V2-Plugin: Kern-Bearbeitungswerkzeuge (select, hatch; A4.6: move/copy). */
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Rotate / Scale / Mirror (Task A4.7)
|
||||||
|
// Konventionen exakt aus Legacy handleModifyMove:
|
||||||
|
// - rotate: Pivot = Zentrum des ERSTEN selektierten Elements,
|
||||||
|
// Δwinkel = angleBetween(pivot,zweitKlick) − angleBetween(pivot,basis)
|
||||||
|
// - scale: Faktor = dist(pivot,zweitKlick) / dist(pivot,basis)
|
||||||
|
// - mirror: Spiegelachse = Linie basis→zweitKlick
|
||||||
|
// Klick-progressiv: Commit beim ZWEITEN down (Arc-Lektion).
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
let transformBase: Pt | null = null;
|
||||||
|
|
||||||
|
function makeTransformTool(mode: 'rotate' | 'scale' | 'mirror'): ToolExtensionV2 {
|
||||||
|
const labels = { rotate: 'Rotieren', scale: 'Skalieren', mirror: 'Spiegeln' } as const;
|
||||||
|
const iconMap = { rotate: '\u21bb', scale: '\u2922', mirror: '\u21c6' } as const;
|
||||||
|
return {
|
||||||
|
manifest: {
|
||||||
|
id: mode,
|
||||||
|
label: labels[mode],
|
||||||
|
icon: iconMap[mode],
|
||||||
|
ribbonTab: 'canvas',
|
||||||
|
tags: ['pro'],
|
||||||
|
description: `${labels[mode]}: Auswahl treffen, dann Basis\u2192Referenz klicken`,
|
||||||
|
},
|
||||||
|
optionsSchema: [],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc || !c.selection) return;
|
||||||
|
if (!transformBase) {
|
||||||
|
if (c.selection.getIds().length === 0) {
|
||||||
|
const hit = c.selection.hitTest(e.world.x, e.world.y, 5);
|
||||||
|
if (!hit) {
|
||||||
|
ctx.setStatus(`${labels[mode]}: zuerst Element(e) auswählen`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c.selection.setIds([hit.id]);
|
||||||
|
}
|
||||||
|
transformBase = e.world;
|
||||||
|
ctx.setStatus(`${labels[mode]}: Referenzklick zum Anwenden (${c.selection.getIds().length} Elemente)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Zweiter Klick → Commit auf ALLE selektierten Elemente, EINE Transaktion
|
||||||
|
const base = transformBase;
|
||||||
|
const ids = c.selection.getIds();
|
||||||
|
const first = c.doc.getElement(ids[0]);
|
||||||
|
if (!first) { transformBase = null; return; }
|
||||||
|
const pivot = { x: first.x, y: first.y };
|
||||||
|
c.doc.transact(() => {
|
||||||
|
for (const id of ids) {
|
||||||
|
const el = c.doc!.getElement(id);
|
||||||
|
if (!el) continue;
|
||||||
|
let next: CADElement;
|
||||||
|
if (mode === 'rotate') {
|
||||||
|
const delta = angleBetween(pivot, e.world) - angleBetween(pivot, base);
|
||||||
|
next = rotateElement(el, pivot.x, pivot.y, delta);
|
||||||
|
} else if (mode === 'scale') {
|
||||||
|
const sd = distance(pivot, base);
|
||||||
|
const cd = distance(pivot, e.world);
|
||||||
|
const factor = sd > 0 ? cd / sd : 1;
|
||||||
|
next = scaleElement(el, pivot.x, pivot.y, factor, factor);
|
||||||
|
} else {
|
||||||
|
next = mirrorElement(el, base.x, base.y, e.world.x, e.world.y);
|
||||||
|
}
|
||||||
|
c.doc!.updateElement(id, {
|
||||||
|
x: next.x, y: next.y,
|
||||||
|
width: next.width, height: next.height,
|
||||||
|
properties: next.properties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.setStatus(`${labels[mode]}: ${ids.length} Element(e)`);
|
||||||
|
transformBase = null;
|
||||||
|
},
|
||||||
|
move(e, ctx) {
|
||||||
|
// Live-Vorschau am ersten selektierten Element (Legacy previewElement)
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!transformBase || !c.selection || !c.doc) return;
|
||||||
|
const ids = c.selection.getIds();
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
const first = c.doc.getElement(ids[0]);
|
||||||
|
if (!first) return;
|
||||||
|
const pivot = { x: first.x, y: first.y };
|
||||||
|
let next: CADElement;
|
||||||
|
if (mode === 'rotate') {
|
||||||
|
const delta = angleBetween(pivot, e.world) - angleBetween(pivot, transformBase);
|
||||||
|
next = rotateElement(first, pivot.x, pivot.y, delta);
|
||||||
|
} else if (mode === 'scale') {
|
||||||
|
const sd = distance(pivot, transformBase);
|
||||||
|
const cd = distance(pivot, e.world);
|
||||||
|
const f = sd > 0 ? cd / sd : 1;
|
||||||
|
next = scaleElement(first, pivot.x, pivot.y, f, f);
|
||||||
|
} else {
|
||||||
|
next = mirrorElement(first, transformBase.x, transformBase.y, e.world.x, e.world.y);
|
||||||
|
}
|
||||||
|
ctx.setPreview?.(next);
|
||||||
|
},
|
||||||
|
cancel(ctx) {
|
||||||
|
transformBase = null;
|
||||||
|
ctx.setPreview?.(null);
|
||||||
|
ctx.setStatus(`${labels[mode]} abgebrochen`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const rotateTool: ToolExtensionV2 = makeTransformTool('rotate');
|
||||||
|
export const scaleTool: ToolExtensionV2 = makeTransformTool('scale');
|
||||||
|
export const mirrorTool: ToolExtensionV2 = makeTransformTool('mirror');
|
||||||
|
|
||||||
|
/** V2-Plugin: Kern-Bearbeitungswerkzeuge. */
|
||||||
export const coreModifyPlugin: PluginV2 = {
|
export const coreModifyPlugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'core-modify',
|
id: 'core-modify',
|
||||||
name: 'Bearbeiten (Kern)',
|
name: 'Bearbeiten (Kern)',
|
||||||
version: '1.2.0',
|
version: '1.3.0',
|
||||||
author: 'web-cad team',
|
author: 'web-cad team',
|
||||||
description: 'Auswahl, Schraffur, Verschieben, Kopieren (V2).',
|
description: 'Auswahl, Schraffur, Verschieben, Kopieren, Rotieren, Skalieren, Spiegeln (V2).',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
},
|
},
|
||||||
tools: [selectTool, hatchTool, moveTool, copyTool],
|
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -433,6 +433,86 @@ describe('A4.6 pilot: move/copy tools', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('A4.7 pilot: transform tools', () => {
|
||||||
|
it('rotate dreht um Pivot des ersten Elements; ein undo stellt zurück', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const rotEl = makeEl('r1');
|
||||||
|
doc.addElement(rotEl);
|
||||||
|
let selIds: string[] = ['r1'];
|
||||||
|
const bridge = {
|
||||||
|
getIds: () => selIds,
|
||||||
|
setIds: (ids: string[]) => { selIds = ids; },
|
||||||
|
hitTest: () => null,
|
||||||
|
};
|
||||||
|
const ctx = makeCtx({ doc, selection: bridge });
|
||||||
|
const rotate = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'rotate')!;
|
||||||
|
|
||||||
|
// Basispunkt rechts vom Zentrum (0°-Richtung), Element x=10
|
||||||
|
rotate.handlers.down!(pe(60, 10), ctx);
|
||||||
|
// Referenzklick oben → Δwinkel ≈ +90° (CCW)
|
||||||
|
rotate.handlers.down!(pe(10, 60), ctx);
|
||||||
|
|
||||||
|
expect(doc.undo()).toBe(true);
|
||||||
|
expect(doc.getAllElements().length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scale vergrößert width/height um Faktor dist(pivot,zweiteKlick)/dist(pivot,basis)', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const sq = makeEl('s1');
|
||||||
|
sq.width = 40;
|
||||||
|
sq.height = 40;
|
||||||
|
sq.x = 0;
|
||||||
|
sq.y = 0;
|
||||||
|
doc.addElement(sq);
|
||||||
|
let selIds: string[] = ['s1'];
|
||||||
|
const bridge = {
|
||||||
|
getIds: () => selIds,
|
||||||
|
setIds: (ids: string[]) => { selIds = ids; },
|
||||||
|
hitTest: () => null,
|
||||||
|
};
|
||||||
|
const ctx = makeCtx({ doc, selection: bridge });
|
||||||
|
const scale = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'scale')!;
|
||||||
|
|
||||||
|
// Basis bei Distanz 50 vom Pivot, Ziel bei 100 → Faktor 2
|
||||||
|
scale.handlers.down!(pe(50, 0), ctx);
|
||||||
|
scale.handlers.down!(pe(100, 0), ctx);
|
||||||
|
|
||||||
|
expect(doc.getElement('s1')!.width).toBeCloseTo(80); // 40×2
|
||||||
|
expect(doc.getElement('s1')!.height).toBeCloseTo(80);
|
||||||
|
|
||||||
|
expect(doc.undo()).toBe(true);
|
||||||
|
expect(doc.getElement('s1')!.width).toBeCloseTo(40);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mirror spiegelt Element an Achse basis→referenz', () => {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
const el = makeEl('mi');
|
||||||
|
el.x = 30;
|
||||||
|
el.y = 0;
|
||||||
|
doc.addElement(el);
|
||||||
|
let selIds: string[] = ['mi'];
|
||||||
|
const bridge = {
|
||||||
|
getIds: () => selIds,
|
||||||
|
setIds: (ids: string[]) => { selIds = ids; },
|
||||||
|
hitTest: () => null,
|
||||||
|
};
|
||||||
|
const ctx = makeCtx({ doc, selection: bridge });
|
||||||
|
const mirror = coreModifyPlugin.tools!.find((t) => t.manifest.id === 'mirror')!;
|
||||||
|
|
||||||
|
// Spiegelachse = Y-Achse (basis (0,0)→ref (0,100)): Punkt (30,0)→(−30,0)
|
||||||
|
mirror.handlers.down!(pe(0, 0), ctx);
|
||||||
|
mirror.handlers.down!(pe(0, 100), ctx);
|
||||||
|
|
||||||
|
const mirrored = doc.getElement('mi')!;
|
||||||
|
expect(mirrored.x).toBeLessThan(0); // jenseits der Achse
|
||||||
|
expect(Math.abs(mirrored.x)).toBeGreaterThanOrEqual(29); // ~spiegelsymmetrisch
|
||||||
|
expect(doc.undo()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('A3 pilot: select tool', () => {
|
describe('A3 pilot: select tool', () => {
|
||||||
function makeBridge(elements: CADElement[]) {
|
function makeBridge(elements: CADElement[]) {
|
||||||
let ids: string[] = [];
|
let ids: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user