Browse Source

Shift loses pointer fixing #1296 (#1330)

* change resize math to absolute instead of delta

* typings

* small change for width on rotation

* apply absolute resize to other sides

* revert&change math.ts

* polish, polish, polish

* refactor with offset

* eliminate nextX

* rename to offsetPointer

* fix curved lines

* prefer arrow function

* remove unused variables/comments for now

Co-authored-by: daishi <daishi@axlight.com>
José Quinto 5 năm trước cách đây
mục cha
commit
8efe0b7d05
3 tập tin đã thay đổi với 205 bổ sung254 xóa
  1. 116 240
      src/element/resizeElements.ts
  2. 15 0
      src/math.test.ts
  3. 74 14
      src/math.ts

+ 116 - 240
src/element/resizeElements.ts

@@ -2,7 +2,7 @@ import { AppState } from "../types";
 import { SHIFT_LOCKING_ANGLE } from "../constants";
 import { getSelectedElements, globalSceneState } from "../scene";
 import { rescalePoints } from "../points";
-import { rotate, adjustXYWithRotation } from "../math";
+import { rotate, resizeXYWidthHightWithRotation } from "../math";
 import {
   ExcalidrawLinearElement,
   NonDeletedExcalidrawElement,
@@ -27,7 +27,7 @@ export type ResizeArrowFnType = (
   deltaY: number,
   pointerX: number,
   pointerY: number,
-  perfect: boolean,
+  sidesWithSameLength: boolean,
 ) => void;
 
 const arrowResizeOrigin: ResizeArrowFnType = (
@@ -37,7 +37,7 @@ const arrowResizeOrigin: ResizeArrowFnType = (
   deltaY,
   pointerX,
   pointerY,
-  perfect,
+  sidesWithSameLength,
 ) => {
   const [px, py] = element.points[pointIndex];
   let x = element.x + deltaX;
@@ -45,7 +45,7 @@ const arrowResizeOrigin: ResizeArrowFnType = (
   let pointX = px - deltaX;
   let pointY = py - deltaY;
 
-  if (perfect) {
+  if (sidesWithSameLength) {
     const { width, height } = getPerfectElementSize(
       element.type,
       px + element.x - pointerX,
@@ -73,10 +73,10 @@ const arrowResizeEnd: ResizeArrowFnType = (
   deltaY,
   pointerX,
   pointerY,
-  perfect,
+  sidesWithSameLength,
 ) => {
   const [px, py] = element.points[pointIndex];
-  if (perfect) {
+  if (sidesWithSameLength) {
     const { width, height } = getPerfectElementSize(
       element.type,
       pointerX - element.x,
@@ -96,7 +96,31 @@ const arrowResizeEnd: ResizeArrowFnType = (
   }
 };
 
-export function resizeElements(
+const applyResizeArrowFn = (
+  element: NonDeleted<ExcalidrawLinearElement>,
+  resizeArrowFn: ResizeArrowFnType | null,
+  setResizeArrowFn: (fn: ResizeArrowFnType) => void,
+  isResizeEnd: boolean,
+  sidesWithSameLength: boolean,
+  x: number,
+  y: number,
+  lastX: number,
+  lastY: number,
+) => {
+  const angle = element.angle;
+  const [deltaX, deltaY] = rotate(x - lastX, y - lastY, 0, 0, -angle);
+  if (!resizeArrowFn) {
+    if (isResizeEnd) {
+      resizeArrowFn = arrowResizeEnd;
+    } else {
+      resizeArrowFn = arrowResizeOrigin;
+    }
+  }
+  resizeArrowFn(element, 1, deltaX, deltaY, x, y, sidesWithSameLength);
+  setResizeArrowFn(resizeArrowFn);
+};
+
+export const resizeElements = (
   resizeHandle: ResizeTestType,
   setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
   appState: AppState,
@@ -104,11 +128,11 @@ export function resizeElements(
   resizeArrowFn: ResizeArrowFnType | null,
   setResizeArrowFn: (fn: ResizeArrowFnType) => void,
   event: PointerEvent,
-  x: number,
-  y: number,
+  xPointer: number,
+  yPointer: number,
   lastX: number,
   lastY: number,
-) {
+) => {
   setAppState({
     isResizing: resizeHandle !== "rotation",
     isRotating: resizeHandle === "rotation",
@@ -117,224 +141,79 @@ export function resizeElements(
     globalSceneState.getElements(),
     appState,
   );
+  const handleOffset = 4 / appState.zoom; // XXX import constant
+  const dashedLinePadding = 4 / appState.zoom; // XXX import constant
+  const offsetPointer = handleOffset + dashedLinePadding;
+  const minSize = handleOffset * 4;
   if (selectedElements.length === 1) {
-    const element = selectedElements[0];
-    const angle = element.angle;
-    // reverse rotate delta
-    const [deltaX, deltaY] = rotate(x - lastX, y - lastY, 0, 0, -angle);
-    switch (resizeHandle) {
-      case "nw":
-        if (isLinearElement(element) && element.points.length === 2) {
-          const [, p1] = element.points;
-
-          if (!resizeArrowFn) {
-            if (p1[0] < 0 || p1[1] < 0) {
-              resizeArrowFn = arrowResizeEnd;
-            } else {
-              resizeArrowFn = arrowResizeOrigin;
-            }
-          }
-          resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
-          setResizeArrowFn(resizeArrowFn);
-        } else {
-          const width = element.width - deltaX;
-          const height = event.shiftKey ? width : element.height - deltaY;
-          const dY = element.height - height;
-          mutateElement(element, {
-            width,
-            height,
-            ...adjustXYWithRotation("nw", element, deltaX, dY, angle),
-            ...(isLinearElement(element) && width !== 0 && height !== 0
-              ? {
-                  points: rescalePoints(
-                    0,
-                    width,
-                    rescalePoints(1, height, element.points),
-                  ),
-                }
-              : {}),
-          });
-        }
-        break;
-      case "ne":
-        if (isLinearElement(element) && element.points.length === 2) {
-          const [, p1] = element.points;
-          if (!resizeArrowFn) {
-            if (p1[0] >= 0) {
-              resizeArrowFn = arrowResizeEnd;
-            } else {
-              resizeArrowFn = arrowResizeOrigin;
-            }
-          }
-          resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
-          setResizeArrowFn(resizeArrowFn);
-        } else {
-          const width = element.width + deltaX;
-          const height = event.shiftKey ? width : element.height - deltaY;
-          const dY = element.height - height;
-          mutateElement(element, {
-            width,
-            height,
-            ...adjustXYWithRotation("ne", element, deltaX, dY, angle),
-            ...(isLinearElement(element) && width !== 0 && height !== 0
-              ? {
-                  points: rescalePoints(
-                    0,
-                    width,
-                    rescalePoints(1, height, element.points),
-                  ),
-                }
-              : {}),
-          });
-        }
-        break;
-      case "sw":
-        if (isLinearElement(element) && element.points.length === 2) {
-          const [, p1] = element.points;
-          if (!resizeArrowFn) {
-            if (p1[0] <= 0) {
-              resizeArrowFn = arrowResizeEnd;
-            } else {
-              resizeArrowFn = arrowResizeOrigin;
-            }
-          }
-          resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
-          setResizeArrowFn(resizeArrowFn);
-        } else {
-          const width = element.width - deltaX;
-          const height = event.shiftKey ? width : element.height + deltaY;
-          const dY = height - element.height;
-          mutateElement(element, {
-            width,
-            height,
-            ...adjustXYWithRotation("sw", element, deltaX, dY, angle),
-            ...(isLinearElement(element) && width !== 0 && height !== 0
-              ? {
-                  points: rescalePoints(
-                    0,
-                    width,
-                    rescalePoints(1, height, element.points),
-                  ),
-                }
-              : {}),
-          });
-        }
-        break;
-      case "se":
-        if (isLinearElement(element) && element.points.length === 2) {
-          const [, p1] = element.points;
-          if (!resizeArrowFn) {
-            if (p1[0] > 0 || p1[1] > 0) {
-              resizeArrowFn = arrowResizeEnd;
-            } else {
-              resizeArrowFn = arrowResizeOrigin;
-            }
-          }
-          resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
-          setResizeArrowFn(resizeArrowFn);
-        } else {
-          const width = element.width + deltaX;
-          const height = event.shiftKey ? width : element.height + deltaY;
-          const dY = height - element.height;
-          mutateElement(element, {
-            width,
-            height,
-            ...adjustXYWithRotation("se", element, deltaX, dY, angle),
-            ...(isLinearElement(element) && width !== 0 && height !== 0
-              ? {
-                  points: rescalePoints(
-                    0,
-                    width,
-                    rescalePoints(1, height, element.points),
-                  ),
-                }
-              : {}),
-          });
-        }
-        break;
-      case "n": {
-        const height = element.height - deltaY;
-
-        if (isLinearElement(element) && height !== 0) {
-          mutateElement(element, {
-            height,
-            ...adjustXYWithRotation("n", element, 0, deltaY, angle),
-            points: rescalePoints(1, height, element.points),
-          });
-        } else {
-          mutateElement(element, {
-            height,
-            ...adjustXYWithRotation("n", element, 0, deltaY, angle),
-          });
-        }
-
-        break;
-      }
-      case "w": {
-        const width = element.width - deltaX;
-
-        if (isLinearElement(element) && width !== 0) {
-          mutateElement(element, {
-            width,
-            ...adjustXYWithRotation("w", element, deltaX, 0, angle),
-            points: rescalePoints(0, width, element.points),
-          });
-        } else {
-          mutateElement(element, {
-            width,
-            ...adjustXYWithRotation("w", element, deltaX, 0, angle),
-          });
-        }
-        break;
-      }
-      case "s": {
-        const height = element.height + deltaY;
-
-        if (isLinearElement(element) && height !== 0) {
-          mutateElement(element, {
-            height,
-            ...adjustXYWithRotation("s", element, 0, deltaY, angle),
-            points: rescalePoints(1, height, element.points),
-          });
-        } else {
-          mutateElement(element, {
-            height,
-            ...adjustXYWithRotation("s", element, 0, deltaY, angle),
-          });
-        }
-        break;
+    const [element] = selectedElements;
+    if (resizeHandle === "rotation") {
+      const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+      const cx = (x1 + x2) / 2;
+      const cy = (y1 + y2) / 2;
+      let angle = (5 * Math.PI) / 2 + Math.atan2(yPointer - cy, xPointer - cx);
+      if (event.shiftKey) {
+        angle += SHIFT_LOCKING_ANGLE / 2;
+        angle -= angle % SHIFT_LOCKING_ANGLE;
       }
-      case "e": {
-        const width = element.width + deltaX;
-
-        if (isLinearElement(element) && width !== 0) {
-          mutateElement(element, {
-            width,
-            ...adjustXYWithRotation("e", element, deltaX, 0, angle),
-            points: rescalePoints(0, width, element.points),
-          });
-        } else {
-          mutateElement(element, {
-            width,
-            ...adjustXYWithRotation("e", element, deltaX, 0, angle),
-          });
-        }
-        break;
+      if (angle >= 2 * Math.PI) {
+        angle -= 2 * Math.PI;
       }
-      case "rotation": {
-        const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
-        const cx = (x1 + x2) / 2;
-        const cy = (y1 + y2) / 2;
-        let angle = (5 * Math.PI) / 2 + Math.atan2(y - cy, x - cx);
-        if (event.shiftKey) {
-          angle += SHIFT_LOCKING_ANGLE / 2;
-          angle -= angle % SHIFT_LOCKING_ANGLE;
-        }
-        if (angle >= 2 * Math.PI) {
-          angle -= 2 * Math.PI;
-        }
-        mutateElement(element, { angle });
-        break;
+      mutateElement(element, { angle });
+    } else if (
+      isLinearElement(element) &&
+      element.points.length === 2 &&
+      (resizeHandle === "nw" ||
+        resizeHandle === "ne" ||
+        resizeHandle === "sw" ||
+        resizeHandle === "se")
+    ) {
+      const [, [px, py]] = element.points;
+      const isResizeEnd =
+        (resizeHandle === "nw" && (px < 0 || py < 0)) ||
+        (resizeHandle === "ne" && px >= 0) ||
+        (resizeHandle === "sw" && px <= 0) ||
+        (resizeHandle === "se" && (px > 0 || py > 0));
+      applyResizeArrowFn(
+        element,
+        resizeArrowFn,
+        setResizeArrowFn,
+        isResizeEnd,
+        event.shiftKey,
+        xPointer,
+        yPointer,
+        lastX,
+        lastY,
+      );
+    } else if (resizeHandle) {
+      const [x1, y1] = getElementAbsoluteCoords(element);
+      const resized = resizeXYWidthHightWithRotation(
+        resizeHandle,
+        x1,
+        y1,
+        element.width,
+        element.height,
+        x1 - element.x,
+        y1 - element.y,
+        element.angle,
+        xPointer,
+        yPointer,
+        offsetPointer,
+        event.shiftKey,
+      );
+      if (resized.width !== 0 && resized.height !== 0) {
+        mutateElement(element, {
+          ...resized,
+          ...(isLinearElement(element)
+            ? {
+                points: rescalePoints(
+                  0,
+                  resized.width,
+                  rescalePoints(1, resized.height, element.points),
+                ),
+              }
+            : {}),
+        });
       }
     }
 
@@ -359,15 +238,12 @@ export function resizeElements(
     return true;
   } else if (selectedElements.length > 1) {
     const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
-    const handleOffset = 4 / appState.zoom; // XXX import constant
-    const dashedLinePadding = 4 / appState.zoom; // XXX import constant
-    const minSize = handleOffset * 4;
     const minScale = Math.max(minSize / (x2 - x1), minSize / (y2 - y1));
     switch (resizeHandle) {
       case "se": {
         const scale = Math.max(
-          (x - handleOffset - dashedLinePadding - x1) / (x2 - x1),
-          (y - handleOffset - dashedLinePadding - y1) / (y2 - y1),
+          (xPointer - offsetPointer - x1) / (x2 - x1),
+          (yPointer - offsetPointer - y1) / (y2 - y1),
         );
         if (scale > minScale) {
           selectedElements.forEach((element) => {
@@ -382,8 +258,8 @@ export function resizeElements(
       }
       case "nw": {
         const scale = Math.max(
-          (x2 - handleOffset - dashedLinePadding - x) / (x2 - x1),
-          (y2 - handleOffset - dashedLinePadding - y) / (y2 - y1),
+          (x2 - offsetPointer - xPointer) / (x2 - x1),
+          (y2 - offsetPointer - yPointer) / (y2 - y1),
         );
         if (scale > minScale) {
           selectedElements.forEach((element) => {
@@ -398,8 +274,8 @@ export function resizeElements(
       }
       case "ne": {
         const scale = Math.max(
-          (x - handleOffset - dashedLinePadding - x1) / (x2 - x1),
-          (y2 - handleOffset - dashedLinePadding - y) / (y2 - y1),
+          (xPointer - offsetPointer - x1) / (x2 - x1),
+          (y2 - offsetPointer - yPointer) / (y2 - y1),
         );
         if (scale > minScale) {
           selectedElements.forEach((element) => {
@@ -414,8 +290,8 @@ export function resizeElements(
       }
       case "sw": {
         const scale = Math.max(
-          (x2 - handleOffset - dashedLinePadding - x) / (x2 - x1),
-          (y - handleOffset - dashedLinePadding - y1) / (y2 - y1),
+          (x2 - offsetPointer - xPointer) / (x2 - x1),
+          (yPointer - offsetPointer - y1) / (y2 - y1),
         );
         if (scale > minScale) {
           selectedElements.forEach((element) => {
@@ -431,12 +307,12 @@ export function resizeElements(
     }
   }
   return false;
-}
+};
 
-export function canResizeMutlipleElements(
+export const canResizeMutlipleElements = (
   elements: readonly NonDeletedExcalidrawElement[],
-) {
+) => {
   return elements.every((element) =>
     ["rectangle", "diamond", "ellipse"].includes(element.type),
   );
-}
+};

+ 15 - 0
src/math.test.ts

@@ -0,0 +1,15 @@
+import { rotate } from "./math";
+
+describe("rotate", () => {
+  it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
+    const x1 = 10;
+    const y1 = 20;
+    const x2 = 20;
+    const y2 = 30;
+    const angle = Math.PI / 2;
+    const [rotatedX, rotatedY] = rotate(x1, y1, x2, y2, angle);
+    expect([rotatedX, rotatedY]).toEqual([30, 20]);
+    const res2 = rotate(rotatedX, rotatedY, x2, y2, -angle);
+    expect(res2).toEqual([x1, x2]);
+  });
+});

+ 74 - 14
src/math.ts

@@ -56,32 +56,92 @@ export function rotate(
   ];
 }
 
-export function adjustXYWithRotation(
+const adjustXYWithRotation = (
   side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
-  position: { x: number; y: number },
+  x: number,
+  y: number,
+  angle: number,
   deltaX: number,
   deltaY: number,
-  angle: number,
-) {
-  let { x, y } = position;
+) => {
+  const cos = Math.cos(angle);
+  const sin = Math.sin(angle);
+  deltaX /= 2;
+  deltaY /= 2;
   if (side === "e" || side === "ne" || side === "se") {
-    x -= (deltaX / 2) * (1 - Math.cos(angle));
-    y -= (deltaX / 2) * -Math.sin(angle);
+    x += deltaX * (1 - cos);
+    y += deltaX * -sin;
   }
   if (side === "s" || side === "sw" || side === "se") {
-    x -= (deltaY / 2) * Math.sin(angle);
-    y -= (deltaY / 2) * (1 - Math.cos(angle));
+    x += deltaY * sin;
+    y += deltaY * (1 - cos);
   }
   if (side === "w" || side === "nw" || side === "sw") {
-    x += (deltaX / 2) * (1 + Math.cos(angle));
-    y += (deltaX / 2) * Math.sin(angle);
+    x += deltaX * (1 + cos);
+    y += deltaX * sin;
   }
   if (side === "n" || side === "nw" || side === "ne") {
-    x += (deltaY / 2) * -Math.sin(angle);
-    y += (deltaY / 2) * (1 + Math.cos(angle));
+    x += deltaY * -sin;
+    y += deltaY * (1 + cos);
   }
   return { x, y };
-}
+};
+
+export const resizeXYWidthHightWithRotation = (
+  side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
+  x: number,
+  y: number,
+  width: number,
+  height: number,
+  offsetX: number,
+  offsetY: number,
+  angle: number,
+  xPointer: number,
+  yPointer: number,
+  offsetPointer: number,
+  sidesWithSameLength: boolean,
+) => {
+  // center point for rotation
+  const cx = x + width / 2;
+  const cy = y + height / 2;
+
+  // rotation with current angle
+  const [rotatedX, rotatedY] = rotate(xPointer, yPointer, cx, cy, -angle);
+
+  let scaleX = 1;
+  let scaleY = 1;
+  if (side === "e" || side === "ne" || side === "se") {
+    scaleX = (rotatedX - offsetPointer - x) / width;
+  }
+  if (side === "s" || side === "sw" || side === "se") {
+    scaleY = (rotatedY - offsetPointer - y) / height;
+  }
+  if (side === "w" || side === "nw" || side === "sw") {
+    scaleX = (x + width - offsetPointer - rotatedX) / width;
+  }
+  if (side === "n" || side === "nw" || side === "ne") {
+    scaleY = (y + height - offsetPointer - rotatedY) / height;
+  }
+
+  let nextWidth = width * scaleX;
+  let nextHeight = height * scaleY;
+  if (sidesWithSameLength) {
+    nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
+  }
+
+  return {
+    width: nextWidth,
+    height: nextHeight,
+    ...adjustXYWithRotation(
+      side,
+      x - offsetX,
+      y - offsetY,
+      angle,
+      width - nextWidth,
+      height - nextHeight,
+    ),
+  };
+};
 
 export const getPointOnAPath = (point: Point, path: Point[]) => {
   const [px, py] = point;