feat: Add option to flip single element on the context menu (#2520)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Riley Schnee 2021-03-26 11:45:08 -04:00 committed by GitHub
parent 458e6d6c24
commit b0d7ff290f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 872 additions and 4 deletions

207
src/actions/actionFlip.ts Normal file
View File

@ -0,0 +1,207 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getElementMap, getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
import { AppState } from "../types";
import { getTransformHandles } from "../element/transformHandles";
import { isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
};
const enableActionFlipVertical = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1;
};
export const actionFlipHorizontal = register({
name: "flipHorizontal",
perform: (elements, appState) => {
return {
elements: flipSelectedElements(elements, appState, "horizontal"),
appState,
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyH",
contextItemLabel: "labels.flipHorizontal",
contextItemPredicate: (elements, appState) =>
enableActionFlipHorizontal(elements, appState),
});
export const actionFlipVertical = register({
name: "flipVertical",
perform: (elements, appState) => {
return {
elements: flipSelectedElements(elements, appState, "vertical"),
appState,
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyV",
contextItemLabel: "labels.flipVertical",
contextItemPredicate: (elements, appState) =>
enableActionFlipVertical(elements, appState),
});
const flipSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
flipDirection: "horizontal" | "vertical",
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
// remove once we allow for groups of elements to be flipped
if (selectedElements.length > 1) {
return elements;
}
const updatedElements = flipElements(
selectedElements,
appState,
flipDirection,
);
const updatedElementsMap = getElementMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element);
};
const flipElements = (
elements: NonDeleted<ExcalidrawElement>[],
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {
for (let i = 0; i < elements.length; i++) {
flipElement(elements[i], appState);
// If vertical flip, rotate an extra 180
if (flipDirection === "vertical") {
rotateElement(elements[i], Math.PI);
}
}
return elements;
};
const flipElement = (
element: NonDeleted<ExcalidrawElement>,
appState: AppState,
) => {
const originalX = element.x;
const originalY = element.y;
const width = element.width;
const height = element.height;
const originalAngle = normalizeAngle(element.angle);
let finalOffsetX = 0;
if (isLinearElement(element)) {
finalOffsetX =
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
element.width;
}
// Rotate back to zero, if necessary
mutateElement(element, {
angle: normalizeAngle(0),
});
// Flip unrotated by pulling TransformHandle to opposite side
const transformHandles = getTransformHandles(element, appState.zoom);
let usingNWHandle = true;
let newNCoordsX = 0;
let nHandle = transformHandles.nw;
if (!nHandle) {
// Use ne handle instead
usingNWHandle = false;
nHandle = transformHandles.ne;
if (!nHandle) {
mutateElement(element, {
angle: originalAngle,
});
return;
}
}
if (isLinearElement(element)) {
for (let i = 1; i < element.points.length; i++) {
LinearElementEditor.movePoint(element, i, [
-element.points[i][0],
element.points[i][1],
]);
}
LinearElementEditor.normalizePoints(element);
} else {
// calculate new x-coord for transformation
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
resizeSingleElement(
element,
true,
element,
usingNWHandle ? "nw" : "ne",
false,
newNCoordsX,
nHandle[1],
);
// fix the size to account for handle sizes
mutateElement(element, {
width,
height,
});
}
// Rotate by (360 degrees - original angle)
let angle = normalizeAngle(2 * Math.PI - originalAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(angle + 2 * Math.PI);
}
mutateElement(element, {
angle,
});
// Move back to original spot to appear "flipped in place"
mutateElement(element, {
x: originalX + finalOffsetX,
y: originalY,
});
updateBoundElements(element);
};
const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {
const originalX = element.x;
const originalY = element.y;
let angle = normalizeAngle(element.angle + rotationAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(2 * Math.PI + angle);
}
mutateElement(element, {
angle,
});
// Move back to original spot
mutateElement(element, {
x: originalX,
y: originalY,
});
};

View File

@ -66,6 +66,8 @@ export {
distributeVertically,
} from "./actionDistribute";
export { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
export {
actionCopy,
actionCut,

View File

@ -23,7 +23,9 @@ export type ShortcutName =
| "zenMode"
| "stats"
| "addToLibrary"
| "viewMode";
| "viewMode"
| "flipHorizontal"
| "flipVertical";
const shortcutMap: Record<ShortcutName, string[]> = {
cut: [getShortcutKey("CtrlOrCmd+X")],
@ -57,6 +59,8 @@ const shortcutMap: Record<ShortcutName, string[]> = {
zenMode: [getShortcutKey("Alt+Z")],
stats: [],
addToLibrary: [],
flipHorizontal: [getShortcutKey("Shift+H")],
flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")],
};

View File

@ -85,6 +85,8 @@ export type ActionName =
| "alignHorizontallyCentered"
| "distributeHorizontally"
| "distributeVertically"
| "flipHorizontal"
| "flipVertical"
| "viewMode"
| "exportWithDarkMode";

View File

@ -17,6 +17,8 @@ import {
actionDeleteSelected,
actionDuplicateSelection,
actionFinalize,
actionFlipHorizontal,
actionFlipVertical,
actionGroup,
actionPasteStyles,
actionSelectAll,
@ -3780,6 +3782,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.actionManager.getAppState(),
);
const maybeFlipHorizontal = actionFlipHorizontal.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
);
const maybeFlipVertical = actionFlipVertical.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
);
const separator = "separator";
const _isMobile = isMobile();
@ -3900,6 +3912,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
actionSendToBack,
actionBringToFront,
separator,
maybeFlipHorizontal && actionFlipHorizontal,
maybeFlipVertical && actionFlipVertical,
(maybeFlipHorizontal || maybeFlipVertical) && separator,
actionDuplicateSelection,
actionDeleteSelected,
],

View File

@ -349,6 +349,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.ungroup")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
/>
<Shortcut
label={t("labels.flipHorizontal")}
shortcuts={[getShortcutKey("Shift+H")]}
/>
<Shortcut
label={t("labels.flipVertical")}
shortcuts={[getShortcutKey("Shift+V")]}
/>
</ShortcutIsland>
</Column>
</Columns>

View File

@ -31,7 +31,7 @@ import {
import { PointerDownState } from "../components/App";
import { Point } from "../types";
const normalizeAngle = (angle: number): number => {
export const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) {
return angle - 2 * Math.PI;
}
@ -181,7 +181,7 @@ const getPerfectElementSizeWithRotation = (
return rotate(size.width, size.height, 0, 0, -angle);
};
const reshapeSingleTwoPointElement = (
export const reshapeSingleTwoPointElement = (
element: NonDeleted<ExcalidrawLinearElement>,
resizeArrowDirection: "origin" | "end",
isRotateWithDiscreteAngle: boolean,
@ -378,7 +378,7 @@ const resizeSingleTextElement = (
}
};
const resizeSingleElement = (
export const resizeSingleElement = (
stateAtResizeStart: NonDeletedExcalidrawElement,
shouldKeepSidesRatio: boolean,
element: NonDeletedExcalidrawElement,

View File

@ -92,6 +92,8 @@
"centerHorizontally": "Center horizontally",
"distributeHorizontally": "Distribute horizontally",
"distributeVertically": "Distribute vertically",
"flipHorizontal": "Flip horizontal",
"flipVertical": "Flip vertical",
"viewMode": "View mode",
"toggleExportColorScheme": "Toggle export color scheme",
"share": "Share"

View File

@ -5,3 +5,7 @@
First release of `@excalidraw/utils` to provide utilities functions.
- Added `exportToBlob` and `exportToSvg` to export an Excalidraw diagram definition, respectively, to a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and to a [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) ([#2246](https://github.com/excalidraw/excalidraw/pull/2246))
### Features
- Flip single elements horizontally or vertically [#2520](https://github.com/excalidraw/excalidraw/pull/2520)

615
src/tests/flip.test.tsx Normal file
View File

@ -0,0 +1,615 @@
import React from "react";
import ReactDOM from "react-dom";
import { render } from "./test-utils";
import App from "../components/App";
import { defaultLang, setLanguage } from "../i18n";
import { UI, Pointer } from "./helpers/ui";
import { API } from "./helpers/api";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
const { h } = window;
const mouse = new Pointer("mouse");
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
mouse.reset();
await setLanguage(defaultLang);
render(<App />);
});
const createAndSelectOneRectangle = (angle: number = 0) => {
UI.createElement("rectangle", {
x: 0,
y: 0,
width: 100,
height: 50,
angle,
});
};
const createAndSelectOneDiamond = (angle: number = 0) => {
UI.createElement("diamond", {
x: 0,
y: 0,
width: 100,
height: 50,
angle,
});
};
const createAndSelectOneEllipse = (angle: number = 0) => {
UI.createElement("ellipse", {
x: 0,
y: 0,
width: 100,
height: 50,
angle,
});
};
const createAndSelectOneArrow = (angle: number = 0) => {
UI.createElement("arrow", {
x: 0,
y: 0,
width: 100,
height: 50,
angle,
});
};
const createAndSelectOneLine = (angle: number = 0) => {
UI.createElement("line", {
x: 0,
y: 0,
width: 100,
height: 50,
angle,
});
};
const createAndReturnOneDraw = (angle: number = 0) => {
return UI.createElement("draw", {
x: 0,
y: 0,
width: 50,
height: 100,
angle,
});
};
// Rectangle element
it("flips an unrotated rectangle horizontally correctly", () => {
createAndSelectOneRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips an unrotated rectangle vertically correctly", () => {
createAndSelectOneRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips a rotated rectangle horizontally correctly", () => {
const originalAngle = (3 * Math.PI) / 4;
const expectedAngle = (5 * Math.PI) / 4;
createAndSelectOneRectangle(originalAngle);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
it("flips a rotated rectangle vertically correctly", () => {
const originalAngle = (3 * Math.PI) / 4;
const expectedAgnle = Math.PI / 4;
createAndSelectOneRectangle(originalAngle);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAgnle);
});
// Diamond element
it("flips an unrotated diamond horizontally correctly", () => {
createAndSelectOneDiamond();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips an unrotated diamond vertically correctly", () => {
createAndSelectOneDiamond();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips a rotated diamond horizontally correctly", () => {
const originalAngle = (5 * Math.PI) / 4;
const expectedAngle = (3 * Math.PI) / 4;
createAndSelectOneDiamond(originalAngle);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
it("flips a rotated diamond vertically correctly", () => {
const originalAngle = (5 * Math.PI) / 4;
const expectedAngle = (7 * Math.PI) / 4;
createAndSelectOneDiamond(originalAngle);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
// Ellipse element
it("flips an unrotated ellipse horizontally correctly", () => {
createAndSelectOneEllipse();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips an unrotated ellipse vertically correctly", () => {
createAndSelectOneEllipse();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips a rotated ellipse horizontally correctly", () => {
const originalAngle = (7 * Math.PI) / 4;
const expectedAngle = Math.PI / 4;
createAndSelectOneEllipse(originalAngle);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
it("flips a rotated ellipse vertically correctly", () => {
const originalAngle = (7 * Math.PI) / 4;
const expectedAngle = (5 * Math.PI) / 4;
createAndSelectOneEllipse(originalAngle);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[0].y).toEqual(0);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
// Arrow element
it("flips an unrotated arrow horizontally correctly", () => {
createAndSelectOneArrow();
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips an unrotated arrow vertically correctly", () => {
createAndSelectOneArrow();
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips a rotated arrow horizontally correctly", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
createAndSelectOneArrow(originalAngle);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
it("flips a rotated arrow vertically correctly", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (3 * Math.PI) / 4;
createAndSelectOneArrow(originalAngle);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
// Line element
it("flips an unrotated line horizontally correctly", () => {
createAndSelectOneLine();
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips an unrotated line vertically correctly", () => {
createAndSelectOneLine();
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
});
it("flips a rotated line horizontally correctly", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
createAndSelectOneLine(originalAngle);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
it("flips a rotated line vertically correctly", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (3 * Math.PI) / 4;
createAndSelectOneLine(originalAngle);
const originalWidth = API.getSelectedElements()[0].width;
const originalHeight = API.getSelectedElements()[0].height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if width and height did not change
expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
});
// Draw element
it("flips an unrotated drawing horizontally correctly", () => {
const draw = createAndReturnOneDraw();
// select draw, since not done automatically
h.state.selectedElementIds[draw.id] = true;
const originalWidth = draw.width;
const originalHeight = draw.height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if width and height did not change
expect(draw.width).toEqual(originalWidth);
expect(draw.height).toEqual(originalHeight);
});
it("flips an unrotated drawing vertically correctly", () => {
const draw = createAndReturnOneDraw();
// select draw, since not done automatically
h.state.selectedElementIds[draw.id] = true;
const originalWidth = draw.width;
const originalHeight = draw.height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if width and height did not change
expect(draw.width).toEqual(originalWidth);
expect(draw.height).toEqual(originalHeight);
});
it("flips a rotated drawing horizontally correctly", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const draw = createAndReturnOneDraw(originalAngle);
// select draw, since not done automatically
h.state.selectedElementIds[draw.id] = true;
const originalWidth = draw.width;
const originalHeight = draw.height;
h.app.actionManager.executeAction(actionFlipHorizontal);
// Check if width and height did not change
expect(draw.width).toEqual(originalWidth);
expect(draw.height).toEqual(originalHeight);
// Check angle
expect(draw.angle).toBeCloseTo(expectedAngle);
});
it("flips a rotated drawing vertically correctly", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (3 * Math.PI) / 4;
const draw = createAndReturnOneDraw(originalAngle);
// select draw, since not done automatically
h.state.selectedElementIds[draw.id] = true;
const originalWidth = draw.width;
const originalHeight = draw.height;
h.app.actionManager.executeAction(actionFlipVertical);
// Check if width and height did not change
expect(API.getSelectedElement().width).toEqual(originalWidth);
expect(API.getSelectedElement().height).toEqual(originalHeight);
// Check angle
expect(API.getSelectedElement().angle).toBeCloseTo(expectedAngle);
});

View File

@ -6,6 +6,7 @@ import {
import { CODES } from "../../keys";
import { ToolName } from "../queries/toolQueries";
import { fireEvent, GlobalTestState } from "../test-utils";
import { mutateElement } from "../../element/mutateElement";
import { API } from "./api";
const { h } = window;
@ -202,6 +203,7 @@ export class UI {
size = 10,
width = size,
height = width,
angle = 0,
}: {
position?: number;
x?: number;
@ -209,6 +211,7 @@ export class UI {
size?: number;
width?: number;
height?: number;
angle?: number;
} = {},
): (T extends "arrow" | "line" | "draw"
? ExcalidrawLinearElement
@ -231,6 +234,10 @@ export class UI {
const origElement = h.elements[h.elements.length - 1] as any;
if (angle !== 0) {
mutateElement(origElement, { angle });
}
return new Proxy(
{},
{

View File

@ -653,6 +653,8 @@ describe("regression tests", () => {
"pasteStyles",
"deleteSelectedElements",
"addToLibrary",
"flipHorizontal",
"flipVertical",
"sendBackward",
"bringForward",
"sendToBack",