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, distributeVertically,
} from "./actionDistribute"; } from "./actionDistribute";
export { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
export { export {
actionCopy, actionCopy,
actionCut, actionCut,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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