Browse Source

improvement: Enhance resize for non generic elements (#2720)

João Forja 4 years ago
parent
commit
e26f374ca6
6 changed files with 112 additions and 323 deletions
  1. 0 3
      src/components/App.tsx
  2. 0 1
      src/element/index.ts
  3. 111 206
      src/element/resizeElements.ts
  4. 0 54
      src/element/resizeTest.ts
  5. 0 58
      src/math.ts
  6. 1 1
      src/tests/resize.test.tsx

+ 0 - 3
src/components/App.tsx

@@ -3587,9 +3587,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       transformElements(
         pointerDownState,
         transformHandleType,
-        (newTransformHandle) => {
-          pointerDownState.resize.handleType = newTransformHandle;
-        },
         selectedElements,
         pointerDownState.resize.arrowDirection,
         getRotateWithDiscreteAngleKey(event),

+ 0 - 1
src/element/index.ts

@@ -34,7 +34,6 @@ export {
 export {
   resizeTest,
   getCursorForResizingElement,
-  normalizeTransformHandleType,
   getElementWithTransformHandleType,
   getTransformHandleTypeFromCoords,
 } from "./resizeTest";

+ 111 - 206
src/element/resizeElements.ts

@@ -4,7 +4,6 @@ import { rescalePoints } from "../points";
 import {
   rotate,
   adjustXYWithRotation,
-  getFlipAdjustment,
   centerPoint,
   rotatePoint,
 } from "../math";
@@ -13,21 +12,16 @@ import {
   ExcalidrawTextElement,
   NonDeletedExcalidrawElement,
   NonDeleted,
-  ExcalidrawGenericElement,
-  ExcalidrawElement,
 } from "./types";
 import {
   getElementAbsoluteCoords,
   getCommonBounds,
   getResizedElementAbsoluteCoords,
 } from "./bounds";
-import { isGenericElement, isLinearElement, isTextElement } from "./typeChecks";
+import { isLinearElement, isTextElement } from "./typeChecks";
 import { mutateElement } from "./mutateElement";
 import { getPerfectElementSize } from "./sizeHelpers";
-import {
-  getCursorForResizingElement,
-  normalizeTransformHandleType,
-} from "./resizeTest";
+import { getCursorForResizingElement } from "./resizeTest";
 import { measureText, getFontString } from "../utils";
 import { updateBoundElements } from "./binding";
 import {
@@ -49,7 +43,6 @@ const normalizeAngle = (angle: number): number => {
 export const transformElements = (
   pointerDownState: PointerDownState,
   transformHandleType: MaybeTransformHandleType,
-  setTransformHandle: (nextTransformHandle: MaybeTransformHandleType) => void,
   selectedElements: readonly NonDeletedExcalidrawElement[],
   resizeArrowDirection: "origin" | "end",
   isRotateWithDiscreteAngle: boolean,
@@ -101,36 +94,15 @@ export const transformElements = (
       );
       updateBoundElements(element);
     } else if (transformHandleType) {
-      if (isGenericElement(element)) {
-        resizeSingleGenericElement(
-          pointerDownState.originalElements.get(element.id) as typeof element,
-          shouldKeepSidesRatio,
-          element,
-          transformHandleType,
-          isResizeCenterPoint,
-          pointerX,
-          pointerY,
-        );
-      } else {
-        const keepSquareAspectRatio = shouldKeepSidesRatio;
-        resizeSingleNonGenericElement(
-          element,
-          transformHandleType,
-          isResizeCenterPoint,
-          keepSquareAspectRatio,
-          pointerX,
-          pointerY,
-        );
-        setTransformHandle(
-          normalizeTransformHandleType(element, transformHandleType),
-        );
-        if (element.width < 0) {
-          mutateElement(element, { width: -element.width });
-        }
-        if (element.height < 0) {
-          mutateElement(element, { height: -element.height });
-        }
-      }
+      resizeSingleElement(
+        pointerDownState.originalElements.get(element.id) as typeof element,
+        shouldKeepSidesRatio,
+        element,
+        transformHandleType,
+        isResizeCenterPoint,
+        pointerX,
+        pointerY,
+      );
     }
 
     // update cursor
@@ -414,8 +386,8 @@ const resizeSingleTextElement = (
   }
 };
 
-const resizeSingleGenericElement = (
-  stateAtResizeStart: NonDeleted<ExcalidrawGenericElement>,
+const resizeSingleElement = (
+  stateAtResizeStart: NonDeletedExcalidrawElement,
   shouldKeepSidesRatio: boolean,
   element: NonDeletedExcalidrawElement,
   transformHandleDirection: TransformHandleDirection,
@@ -423,251 +395,184 @@ const resizeSingleGenericElement = (
   pointerX: number,
   pointerY: number,
 ) => {
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(stateAtResizeStart);
+  // Gets bounds corners
+  const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
+    stateAtResizeStart,
+    stateAtResizeStart.width,
+    stateAtResizeStart.height,
+  );
   const startTopLeft: Point = [x1, y1];
   const startBottomRight: Point = [x2, y2];
   const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
 
   // Calculate new dimensions based on cursor position
-  let newWidth = stateAtResizeStart.width;
-  let newHeight = stateAtResizeStart.height;
   const rotatedPointer = rotatePoint(
     [pointerX, pointerY],
     startCenter,
     -stateAtResizeStart.angle,
   );
+
+  //Get bounds corners rendered on screen
+  const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
+    element,
+    element.width,
+    element.height,
+  );
+  const boundsCurrentWidth = esx2 - esx1;
+  const boundsCurrentHeight = esy2 - esy1;
+
+  // It's important we set the initial scale value based on the width and height at resize start,
+  // otherwise previous dimensions affected by modifiers will be taken into account.
+  const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
+  const atStartBoundsHeight = startBottomRight[1] - startTopLeft[1];
+  let scaleX = atStartBoundsWidth / boundsCurrentWidth;
+  let scaleY = atStartBoundsHeight / boundsCurrentHeight;
+
   if (transformHandleDirection.includes("e")) {
-    newWidth = rotatedPointer[0] - startTopLeft[0];
+    scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
   }
   if (transformHandleDirection.includes("s")) {
-    newHeight = rotatedPointer[1] - startTopLeft[1];
+    scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight;
   }
   if (transformHandleDirection.includes("w")) {
-    newWidth = startBottomRight[0] - rotatedPointer[0];
+    scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
   }
   if (transformHandleDirection.includes("n")) {
-    newHeight = startBottomRight[1] - rotatedPointer[1];
+    scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
   }
+  // Linear elements dimensions differ from bounds dimensions
+  const eleInitialWidth = stateAtResizeStart.width;
+  const eleInitialHeight = stateAtResizeStart.height;
+  // We have to use dimensions of element on screen, otherwise the scaling of the
+  // dimensions won't match the cursor for linear elements.
+  let eleNewWidth = element.width * scaleX;
+  let eleNewHeight = element.height * scaleY;
 
   // adjust dimensions for resizing from center
   if (isResizeFromCenter) {
-    newWidth = 2 * newWidth - stateAtResizeStart.width;
-    newHeight = 2 * newHeight - stateAtResizeStart.height;
+    eleNewWidth = 2 * eleNewWidth - eleInitialWidth;
+    eleNewHeight = 2 * eleNewHeight - eleInitialHeight;
   }
 
   // adjust dimensions to keep sides ratio
   if (shouldKeepSidesRatio) {
-    const widthRatio = Math.abs(newWidth) / stateAtResizeStart.width;
-    const heightRatio = Math.abs(newHeight) / stateAtResizeStart.height;
+    const widthRatio = Math.abs(eleNewWidth) / eleInitialWidth;
+    const heightRatio = Math.abs(eleNewHeight) / eleInitialHeight;
     if (transformHandleDirection.length === 1) {
-      newHeight *= widthRatio;
-      newWidth *= heightRatio;
+      eleNewHeight *= widthRatio;
+      eleNewWidth *= heightRatio;
     }
     if (transformHandleDirection.length === 2) {
       const ratio = Math.max(widthRatio, heightRatio);
-      newWidth = stateAtResizeStart.width * ratio * Math.sign(newWidth);
-      newHeight = stateAtResizeStart.height * ratio * Math.sign(newHeight);
+      eleNewWidth = eleInitialWidth * ratio * Math.sign(eleNewWidth);
+      eleNewHeight = eleInitialHeight * ratio * Math.sign(eleNewHeight);
     }
   }
 
+  const [
+    newBoundsX1,
+    newBoundsY1,
+    newBoundsX2,
+    newBoundsY2,
+  ] = getResizedElementAbsoluteCoords(
+    stateAtResizeStart,
+    eleNewWidth,
+    eleNewHeight,
+  );
+  const newBoundsWidth = newBoundsX2 - newBoundsX1;
+  const newBoundsHeight = newBoundsY2 - newBoundsY1;
+
   // Calculate new topLeft based on fixed corner during resize
-  let newTopLeft = startTopLeft as [number, number];
+  let newTopLeft = [...startTopLeft] as [number, number];
   if (["n", "w", "nw"].includes(transformHandleDirection)) {
     newTopLeft = [
-      startBottomRight[0] - Math.abs(newWidth),
-      startBottomRight[1] - Math.abs(newHeight),
+      startBottomRight[0] - Math.abs(newBoundsWidth),
+      startBottomRight[1] - Math.abs(newBoundsHeight),
     ];
   }
   if (transformHandleDirection === "ne") {
-    const bottomLeft = [
-      stateAtResizeStart.x,
-      stateAtResizeStart.y + stateAtResizeStart.height,
-    ];
-    newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newHeight)];
+    const bottomLeft = [startTopLeft[0], startBottomRight[1]];
+    newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
   }
   if (transformHandleDirection === "sw") {
-    const topRight = [
-      stateAtResizeStart.x + stateAtResizeStart.width,
-      stateAtResizeStart.y,
-    ];
-    newTopLeft = [topRight[0] - Math.abs(newWidth), topRight[1]];
+    const topRight = [startBottomRight[0], startTopLeft[1]];
+    newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
   }
 
   // Keeps opposite handle fixed during resize
   if (shouldKeepSidesRatio) {
     if (["s", "n"].includes(transformHandleDirection)) {
-      newTopLeft[0] = startCenter[0] - newWidth / 2;
+      newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
     }
     if (["e", "w"].includes(transformHandleDirection)) {
-      newTopLeft[1] = startCenter[1] - newHeight / 2;
+      newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
     }
   }
 
   // Flip horizontally
-  if (newWidth < 0) {
+  if (eleNewWidth < 0) {
     if (transformHandleDirection.includes("e")) {
-      newTopLeft[0] -= Math.abs(newWidth);
+      newTopLeft[0] -= Math.abs(newBoundsWidth);
     }
     if (transformHandleDirection.includes("w")) {
-      newTopLeft[0] += Math.abs(newWidth);
+      newTopLeft[0] += Math.abs(newBoundsWidth);
     }
   }
   // Flip vertically
-  if (newHeight < 0) {
+  if (eleNewHeight < 0) {
     if (transformHandleDirection.includes("s")) {
-      newTopLeft[1] -= Math.abs(newHeight);
+      newTopLeft[1] -= Math.abs(newBoundsHeight);
     }
     if (transformHandleDirection.includes("n")) {
-      newTopLeft[1] += Math.abs(newHeight);
+      newTopLeft[1] += Math.abs(newBoundsHeight);
     }
   }
 
   if (isResizeFromCenter) {
-    newTopLeft[0] = startCenter[0] - Math.abs(newWidth) / 2;
-    newTopLeft[1] = startCenter[1] - Math.abs(newHeight) / 2;
+    newTopLeft[0] = startCenter[0] - Math.abs(newBoundsWidth) / 2;
+    newTopLeft[1] = startCenter[1] - Math.abs(newBoundsHeight) / 2;
   }
 
   // adjust topLeft to new rotation point
   const angle = stateAtResizeStart.angle;
   const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
   const newCenter: Point = [
-    newTopLeft[0] + Math.abs(newWidth) / 2,
-    newTopLeft[1] + Math.abs(newHeight) / 2,
+    newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
+    newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
   ];
   const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
   newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
 
-  const resizedElement = {
-    width: Math.abs(newWidth),
-    height: Math.abs(newHeight),
-    x: newTopLeft[0],
-    y: newTopLeft[1],
-  };
-  updateBoundElements(element, {
-    newSize: { width: resizedElement.width, height: resizedElement.height },
-  });
-  mutateElement(element, resizedElement);
-};
-
-const resizeSingleNonGenericElement = (
-  element: NonDeleted<Exclude<ExcalidrawElement, ExcalidrawGenericElement>>,
-  transformHandleType: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
-  isResizeFromCenter: boolean,
-  keepSquareAspectRatio: boolean,
-  pointerX: number,
-  pointerY: number,
-) => {
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
-  const cx = (x1 + x2) / 2;
-  const cy = (y1 + y2) / 2;
-
-  // rotation pointer with reverse angle
-  const [rotatedX, rotatedY] = rotate(
-    pointerX,
-    pointerY,
-    cx,
-    cy,
-    -element.angle,
+  // Readjust points for linear elements
+  const rescaledPoints = rescalePointsInElement(
+    stateAtResizeStart,
+    eleNewWidth,
+    eleNewHeight,
   );
+  // For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
+  // So we need to readjust (x,y) to be where the first point should be
+  const newOrigin = [...newTopLeft];
+  newOrigin[0] += stateAtResizeStart.x - newBoundsX1;
+  newOrigin[1] += stateAtResizeStart.y - newBoundsY1;
 
-  let scaleX = 1;
-  let scaleY = 1;
-  if (
-    transformHandleType === "e" ||
-    transformHandleType === "ne" ||
-    transformHandleType === "se"
-  ) {
-    scaleX = (rotatedX - x1) / (x2 - x1);
-  }
-  if (
-    transformHandleType === "s" ||
-    transformHandleType === "sw" ||
-    transformHandleType === "se"
-  ) {
-    scaleY = (rotatedY - y1) / (y2 - y1);
-  }
-  if (
-    transformHandleType === "w" ||
-    transformHandleType === "nw" ||
-    transformHandleType === "sw"
-  ) {
-    scaleX = (x2 - rotatedX) / (x2 - x1);
-  }
-  if (
-    transformHandleType === "n" ||
-    transformHandleType === "nw" ||
-    transformHandleType === "ne"
-  ) {
-    scaleY = (y2 - rotatedY) / (y2 - y1);
-  }
-  let nextWidth = element.width * scaleX;
-  let nextHeight = element.height * scaleY;
-  if (keepSquareAspectRatio) {
-    nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
-  }
-
-  const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
-    element,
-    nextWidth,
-    nextHeight,
-  );
-  const deltaX1 = (x1 - nextX1) / 2;
-  const deltaY1 = (y1 - nextY1) / 2;
-  const deltaX2 = (x2 - nextX2) / 2;
-  const deltaY2 = (y2 - nextY2) / 2;
-
-  const rescaledPoints = rescalePointsInElement(element, nextWidth, nextHeight);
-
-  updateBoundElements(element, {
-    newSize: { width: nextWidth, height: nextHeight },
-  });
-  const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords(
-    {
-      ...element,
-      ...rescaledPoints,
-    },
-    Math.abs(nextWidth),
-    Math.abs(nextHeight),
-  );
-  const [flipDiffX, flipDiffY] = getFlipAdjustment(
-    transformHandleType,
-    nextWidth,
-    nextHeight,
-    nextX1,
-    nextY1,
-    nextX2,
-    nextY2,
-    finalX1,
-    finalY1,
-    finalX2,
-    finalY2,
-    isLinearElement(element),
-    element.angle,
-  );
-  const [nextElementX, nextElementY] = adjustXYWithRotation(
-    getSidesForTransformHandle(transformHandleType, isResizeFromCenter),
-    element.x - flipDiffX,
-    element.y - flipDiffY,
-    element.angle,
-    deltaX1,
-    deltaY1,
-    deltaX2,
-    deltaY2,
-  );
+  const resizedElement = {
+    width: Math.abs(eleNewWidth),
+    height: Math.abs(eleNewHeight),
+    x: newOrigin[0],
+    y: newOrigin[1],
+    ...rescaledPoints,
+  };
 
   if (
-    nextWidth !== 0 &&
-    nextHeight !== 0 &&
-    Number.isFinite(nextElementX) &&
-    Number.isFinite(nextElementY)
+    resizedElement.width !== 0 &&
+    resizedElement.height !== 0 &&
+    Number.isFinite(resizedElement.x) &&
+    Number.isFinite(resizedElement.y)
   ) {
-    mutateElement(element, {
-      width: nextWidth,
-      height: nextHeight,
-      x: nextElementX,
-      y: nextElementY,
-      ...rescaledPoints,
+    updateBoundElements(element, {
+      newSize: { width: resizedElement.width, height: resizedElement.height },
     });
+    mutateElement(element, resizedElement);
   }
 };
 

+ 0 - 54
src/element/resizeTest.ts

@@ -173,57 +173,3 @@ export const getCursorForResizingElement = (resizingElement: {
 
   return cursor ? `${cursor}-resize` : "";
 };
-
-export const normalizeTransformHandleType = (
-  element: ExcalidrawElement,
-  transformHandleType: TransformHandleType,
-): TransformHandleType => {
-  if (element.width >= 0 && element.height >= 0) {
-    return transformHandleType;
-  }
-
-  if (element.width < 0 && element.height < 0) {
-    switch (transformHandleType) {
-      case "nw":
-        return "se";
-      case "ne":
-        return "sw";
-      case "se":
-        return "nw";
-      case "sw":
-        return "ne";
-    }
-  } else if (element.width < 0) {
-    switch (transformHandleType) {
-      case "nw":
-        return "ne";
-      case "ne":
-        return "nw";
-      case "se":
-        return "sw";
-      case "sw":
-        return "se";
-      case "e":
-        return "w";
-      case "w":
-        return "e";
-    }
-  } else {
-    switch (transformHandleType) {
-      case "nw":
-        return "sw";
-      case "ne":
-        return "se";
-      case "se":
-        return "ne";
-      case "sw":
-        return "nw";
-      case "n":
-        return "s";
-      case "s":
-        return "n";
-    }
-  }
-
-  return transformHandleType;
-};

+ 0 - 58
src/math.ts

@@ -70,64 +70,6 @@ export const adjustXYWithRotation = (
   return [x, y];
 };
 
-export const getFlipAdjustment = (
-  side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
-  nextWidth: number,
-  nextHeight: number,
-  nextX1: number,
-  nextY1: number,
-  nextX2: number,
-  nextY2: number,
-  finalX1: number,
-  finalY1: number,
-  finalX2: number,
-  finalY2: number,
-  needsRotation: boolean,
-  angle: number,
-): [number, number] => {
-  const cos = Math.cos(angle);
-  const sin = Math.sin(angle);
-  let flipDiffX = 0;
-  let flipDiffY = 0;
-  if (nextWidth < 0) {
-    if (side === "e" || side === "ne" || side === "se") {
-      if (needsRotation) {
-        flipDiffX += (finalX2 - nextX1) * cos;
-        flipDiffY += (finalX2 - nextX1) * sin;
-      } else {
-        flipDiffX += finalX2 - nextX1;
-      }
-    }
-    if (side === "w" || side === "nw" || side === "sw") {
-      if (needsRotation) {
-        flipDiffX += (finalX1 - nextX2) * cos;
-        flipDiffY += (finalX1 - nextX2) * sin;
-      } else {
-        flipDiffX += finalX1 - nextX2;
-      }
-    }
-  }
-  if (nextHeight < 0) {
-    if (side === "s" || side === "se" || side === "sw") {
-      if (needsRotation) {
-        flipDiffY += (finalY2 - nextY1) * cos;
-        flipDiffX += (finalY2 - nextY1) * -sin;
-      } else {
-        flipDiffY += finalY2 - nextY1;
-      }
-    }
-    if (side === "n" || side === "ne" || side === "nw") {
-      if (needsRotation) {
-        flipDiffY += (finalY1 - nextY2) * cos;
-        flipDiffX += (finalY1 - nextY2) * -sin;
-      } else {
-        flipDiffY += finalY1 - nextY2;
-      }
-    }
-  }
-  return [flipDiffX, flipDiffY];
-};
-
 export const getPointOnAPath = (point: Point, path: Point[]) => {
   const [px, py] = point;
   const [start, ...other] = path;

+ 1 - 1
src/tests/resize.test.tsx

@@ -41,7 +41,7 @@ describe("resize rectangle ellipses and diamond elements", () => {
     ${"s"}  | ${[_, 39]}      | ${[100, 139]} | ${[elemData.x, elemData.x]}
     ${"e"}  | ${[-20, _]}     | ${[80, 100]}  | ${[elemData.x, elemData.y]}
     ${"w"}  | ${[-20, _]}     | ${[120, 100]} | ${[-20, elemData.y]}
-    ${"ne"} | ${[10, 55]}     | ${[110, 45]}  | ${[elemData.x, 55]}
+    ${"ne"} | ${[5, 55]}      | ${[105, 45]}  | ${[elemData.x, 55]}
     ${"se"} | ${[-30, -10]}   | ${[70, 90]}   | ${[elemData.x, elemData.y]}
     ${"nw"} | ${[-300, -200]} | ${[400, 300]} | ${[-300, -200]}
     ${"sw"} | ${[40, -20]}    | ${[60, 80]}   | ${[40, 0]}