Lock drag direction using Shift (#1858)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Robert van Hoesel 2020-09-11 17:22:40 +02:00 committed by GitHub
parent d07099aadd
commit c6736fa14e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 84 additions and 75 deletions

View File

@ -209,7 +209,7 @@ const gesture: Gesture = {
initialScale: null,
};
type PointerDownState = Readonly<{
export type PointerDownState = Readonly<{
// The first position at which pointerDown happened
origin: Readonly<{ x: number; y: number }>;
// Same as "origin" but snapped to the grid, if grid is on
@ -218,6 +218,9 @@ type PointerDownState = Readonly<{
scrollbars: ReturnType<typeof isOverScrollBars>;
// The previous pointer position
lastCoords: { x: number; y: number };
// map of original elements data
// (for now only a subset of props for perf reasons)
originalElements: Map<string, Pick<ExcalidrawElement, "x" | "y" | "angle">>;
resize: {
// Handle when resizing, might change during the pointer interaction
handleType: MaybeTransformHandleType;
@ -229,8 +232,6 @@ type PointerDownState = Readonly<{
arrowDirection: "origin" | "end";
// This is a center point of selected elements determined on the initial pointer down event (for rotation only)
center: { x: number; y: number };
// This is a list of selected elements determined on the initial pointer down event (for rotation only)
originalElements: readonly NonDeleted<ExcalidrawElement>[];
};
hit: {
// The element the pointer is "hitting", is determined on the initial
@ -2435,13 +2436,20 @@ class App extends React.Component<ExcalidrawProps, AppState> {
),
// we need to duplicate because we'll be updating this state
lastCoords: { ...origin },
originalElements: this.scene.getElements().reduce((acc, element) => {
acc.set(element.id, {
x: element.x,
y: element.y,
angle: element.angle,
});
return acc;
}, new Map() as PointerDownState["originalElements"]),
resize: {
handleType: false,
isResizing: false,
offset: { x: 0, y: 0 },
arrowDirection: "origin",
center: { x: (maxX + minX) / 2, y: (maxY + minY) / 2 },
originalElements: selectedElements.map((element) => ({ ...element })),
},
hit: {
element: null,
@ -2941,6 +2949,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
);
if (
transformElements(
pointerDownState,
transformHandleType,
(newTransformHandle) => {
pointerDownState.resize.handleType = newTransformHandle;
@ -2954,7 +2963,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
resizeY,
pointerDownState.resize.center.x,
pointerDownState.resize.center.y,
pointerDownState.resize.originalElements,
)
) {
this.maybeSuggestBindingForAll(selectedElements);
@ -3004,7 +3012,25 @@ class App extends React.Component<ExcalidrawProps, AppState> {
pointerCoords.y - pointerDownState.drag.offset.y,
this.state.gridSize,
);
dragSelectedElements(selectedElements, dragX, dragY, this.scene);
const [dragDistanceX, dragDistanceY] = [
Math.abs(pointerCoords.x - pointerDownState.origin.x),
Math.abs(pointerCoords.y - pointerDownState.origin.y),
];
// We only drag in one direction if shift is pressed
const lockDirection = event.shiftKey;
dragSelectedElements(
pointerDownState,
selectedElements,
dragX,
dragY,
this.scene,
lockDirection,
dragDistanceX,
dragDistanceY,
);
this.maybeSuggestBindingForAll(selectedElements);
// We duplicate the selected element if alt is pressed on pointer move

View File

@ -5,21 +5,41 @@ import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers";
import Scene from "../scene/Scene";
import { NonDeletedExcalidrawElement } from "./types";
import { PointerDownState } from "../components/App";
export const dragSelectedElements = (
pointerDownState: PointerDownState,
selectedElements: NonDeletedExcalidrawElement[],
pointerX: number,
pointerY: number,
scene: Scene,
lockDirection: boolean = false,
distanceX: number = 0,
distanceY: number = 0,
) => {
const [x1, y1] = getCommonBounds(selectedElements);
const offset = { x: pointerX - x1, y: pointerY - y1 };
selectedElements.forEach((element) => {
let x, y;
if (lockDirection) {
const lockX = lockDirection && distanceX < distanceY;
const lockY = lockDirection && distanceX > distanceY;
const original = pointerDownState.originalElements.get(element.id);
x = lockX && original ? original.x : element.x + offset.x;
y = lockY && original ? original.y : element.y + offset.y;
} else {
x = element.x + offset.x;
y = element.y + offset.y;
}
mutateElement(element, {
x: element.x + offset.x,
y: element.y + offset.y,
x,
y,
});
updateBoundElements(element, {
simultaneouslyUpdated: selectedElements,
});
updateBoundElements(element, { simultaneouslyUpdated: selectedElements });
});
};

View File

@ -26,6 +26,7 @@ import {
TransformHandleType,
MaybeTransformHandleType,
} from "./transformHandles";
import { PointerDownState } from "../components/App";
const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) {
@ -36,6 +37,7 @@ const normalizeAngle = (angle: number): number => {
// Returns true when transform (resizing/rotation) happened
export const transformElements = (
pointerDownState: PointerDownState,
transformHandleType: MaybeTransformHandleType,
setTransformHandle: (nextTransformHandle: MaybeTransformHandleType) => void,
selectedElements: readonly NonDeletedExcalidrawElement[],
@ -47,7 +49,6 @@ export const transformElements = (
pointerY: number,
centerX: number,
centerY: number,
originalElements: readonly NonDeletedExcalidrawElement[],
) => {
if (selectedElements.length === 1) {
const [element] = selectedElements;
@ -120,13 +121,13 @@ export const transformElements = (
} else if (selectedElements.length > 1) {
if (transformHandleType === "rotation") {
rotateMultipleElements(
pointerDownState,
selectedElements,
pointerX,
pointerY,
isRotateWithDiscreteAngle,
centerX,
centerY,
originalElements,
);
return true;
} else if (
@ -619,13 +620,13 @@ const resizeMultipleElements = (
};
const rotateMultipleElements = (
pointerDownState: PointerDownState,
elements: readonly NonDeletedExcalidrawElement[],
pointerX: number,
pointerY: number,
isRotateWithDiscreteAngle: boolean,
centerX: number,
centerY: number,
originalElements: readonly NonDeletedExcalidrawElement[],
) => {
let centerAngle =
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
@ -637,17 +638,19 @@ const rotateMultipleElements = (
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const origAngle =
pointerDownState.originalElements.get(element.id)?.angle ?? element.angle;
const [rotatedCX, rotatedCY] = rotate(
cx,
cy,
centerX,
centerY,
centerAngle + originalElements[index].angle - element.angle,
centerAngle + origAngle - element.angle,
);
mutateElement(element, {
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + originalElements[index].angle),
angle: normalizeAngle(centerAngle + origAngle),
});
});
};

View File

@ -25,29 +25,3 @@ Object {
"y": 47,
}
`;
exports[`resize element with aspect ratio when SHIFT is clicked rectangle 1`] = `
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 50,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
"versionNonce": 401146281,
"width": 30,
"x": 29,
"y": 47,
}
`;

View File

@ -4,6 +4,10 @@ import { render, fireEvent } from "./test-utils";
import App from "../components/App";
import * as Renderer from "../renderer/renderScene";
import { reseed } from "../random";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { getTransformHandles } from "../element/transformHandles";
const mouse = new Pointer("mouse");
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@ -62,43 +66,25 @@ describe("resize element", () => {
describe("resize element with aspect ratio when SHIFT is clicked", () => {
it("rectangle", () => {
const { getByToolName, container } = render(<App />);
const canvas = container.querySelector("canvas")!;
render(<App />);
{
// create element
const tool = getByToolName("rectangle");
fireEvent.click(tool);
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
fireEvent.pointerUp(canvas);
const rectangle = UI.createElement("rectangle", {
x: 0,
width: 30,
height: 50,
});
expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
mouse.select(rectangle);
renderScene.mockClear();
}
// select the element first
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
fireEvent.pointerUp(canvas);
// select a handler rectangle (top-left)
fireEvent.pointerDown(canvas, { clientX: 21, clientY: 13 });
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40, shiftKey: true });
fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1);
expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
h.elements.forEach((element) => expect(element).toMatchSnapshot());
const se = getTransformHandles(rectangle, h.state.zoom, "mouse").se!;
const clientX = se[0] + se[2] / 2;
const clientY = se[1] + se[3] / 2;
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.reset();
mouse.down(clientX, clientY);
mouse.move(1, 1);
mouse.up();
});
expect([h.elements[0].width, h.elements[0].height]).toEqual([51, 51]);
});
});