소스 검색

Lock drag direction using `Shift` (#1858)

Co-authored-by: dwelle <luzar.david@gmail.com>
Robert van Hoesel 4 년 전
부모
커밋
c6736fa14e
5개의 변경된 파일87개의 추가작업 그리고 78개의 파일을 삭제
  1. 32 6
      src/components/App.tsx
  2. 23 3
      src/element/dragElements.ts
  3. 8 5
      src/element/resizeElements.ts
  4. 0 26
      src/tests/__snapshots__/resize.test.tsx.snap
  5. 24 38
      src/tests/resize.test.tsx

+ 32 - 6
src/components/App.tsx

@@ -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

+ 23 - 3
src/element/dragElements.ts

@@ -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 });
   });
 };
 

+ 8 - 5
src/element/resizeElements.ts

@@ -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),
     });
   });
 };

+ 0 - 26
src/tests/__snapshots__/resize.test.tsx.snap

@@ -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,
-}
-`;

+ 24 - 38
src/tests/resize.test.tsx

@@ -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")!;
-
-    {
-      // 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);
-
-      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]);
-
-      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());
+    render(<App />);
+
+    const rectangle = UI.createElement("rectangle", {
+      x: 0,
+      width: 30,
+      height: 50,
+    });
+
+    mouse.select(rectangle);
+
+    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]);
   });
 });