From 73d8c5b7c1c1dba19d9e335ecadbc6ae43e78a7f Mon Sep 17 00:00:00 2001
From: Daishi Kato <dai-shi@users.noreply.github.com>
Date: Tue, 5 May 2020 00:25:40 +0900
Subject: [PATCH] fix resizing lines with abs coords bigger than element w/h
 (#1427)

---
 src/element/bounds.ts         | 174 ++++++------
 src/element/index.ts          |   1 -
 src/element/resizeElements.ts | 514 ++++++++++++++++++++++------------
 src/math.ts                   | 161 ++++++-----
 4 files changed, 498 insertions(+), 352 deletions(-)

diff --git a/src/element/bounds.ts b/src/element/bounds.ts
index 6311779a..e5a413b0 100644
--- a/src/element/bounds.ts
+++ b/src/element/bounds.ts
@@ -1,9 +1,11 @@
 import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
 import { rotate } from "../math";
-import { Drawable, Op } from "roughjs/bin/core";
+import rough from "roughjs/bin/rough";
+import { Drawable, Op, Options } from "roughjs/bin/core";
 import { Point } from "../types";
 import { getShapeForElement } from "../renderer/renderElement";
 import { isLinearElement } from "./typeChecks";
+import { rescalePoints } from "../points";
 
 // If the element is created from right to left, the width is going to be negative
 // This set of functions retrieves the absolute position of the 4 points.
@@ -11,7 +13,7 @@ export function getElementAbsoluteCoords(
   element: ExcalidrawElement,
 ): [number, number, number, number] {
   if (isLinearElement(element)) {
-    return getLinearElementAbsoluteBounds(element);
+    return getLinearElementAbsoluteCoords(element);
   }
   return [
     element.x,
@@ -45,35 +47,10 @@ export function getCurvePathOps(shape: Drawable): Op[] {
   return shape.sets[0].ops;
 }
 
-export function getLinearElementAbsoluteBounds(
-  element: ExcalidrawLinearElement,
-): [number, number, number, number] {
-  if (element.points.length < 2 || !getShapeForElement(element)) {
-    const { minX, minY, maxX, maxY } = element.points.reduce(
-      (limits, [x, y]) => {
-        limits.minY = Math.min(limits.minY, y);
-        limits.minX = Math.min(limits.minX, x);
-
-        limits.maxX = Math.max(limits.maxX, x);
-        limits.maxY = Math.max(limits.maxY, y);
-
-        return limits;
-      },
-      { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
-    );
-    return [
-      minX + element.x,
-      minY + element.y,
-      maxX + element.x,
-      maxY + element.y,
-    ];
-  }
-
-  const shape = getShapeForElement(element) as Drawable[];
-
-  // first element is always the curve
-  const ops = getCurvePathOps(shape[0]);
-
+const getMinMaxXYFromCurvePathOps = (
+  ops: Op[],
+  transformXY?: (x: number, y: number) => [number, number],
+): [number, number, number, number] => {
   let currentP: Point = [0, 0];
 
   const { minX, minY, maxX, maxY } = ops.reduce(
@@ -104,8 +81,11 @@ export function getLinearElementAbsoluteBounds(
 
         let t = 0;
         while (t <= 1.0) {
-          const x = equation(t, 0);
-          const y = equation(t, 1);
+          let x = equation(t, 0);
+          let y = equation(t, 1);
+          if (transformXY) {
+            [x, y] = transformXY(x, y);
+          }
 
           limits.minY = Math.min(limits.minY, y);
           limits.minX = Math.min(limits.minX, x);
@@ -125,13 +105,47 @@ export function getLinearElementAbsoluteBounds(
     { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
   );
 
+  return [minX, minY, maxX, maxY];
+};
+
+const getLinearElementAbsoluteCoords = (
+  element: ExcalidrawLinearElement,
+): [number, number, number, number] => {
+  if (element.points.length < 2 || !getShapeForElement(element)) {
+    const { minX, minY, maxX, maxY } = element.points.reduce(
+      (limits, [x, y]) => {
+        limits.minY = Math.min(limits.minY, y);
+        limits.minX = Math.min(limits.minX, x);
+
+        limits.maxX = Math.max(limits.maxX, x);
+        limits.maxY = Math.max(limits.maxY, y);
+
+        return limits;
+      },
+      { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
+    );
+    return [
+      minX + element.x,
+      minY + element.y,
+      maxX + element.x,
+      maxY + element.y,
+    ];
+  }
+
+  const shape = getShapeForElement(element) as Drawable[];
+
+  // first element is always the curve
+  const ops = getCurvePathOps(shape[0]);
+
+  const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
+
   return [
     minX + element.x,
     minY + element.y,
     maxX + element.x,
     maxY + element.y,
   ];
-}
+};
 
 export function getArrowPoints(
   element: ExcalidrawLinearElement,
@@ -197,8 +211,6 @@ export function getArrowPoints(
   return [x2, y2, x3, y3, x4, y4];
 }
 
-// this function has some code in common with getLinearElementAbsoluteBounds
-// there might be more efficient way
 const getLinearElementRotatedBounds = (
   element: ExcalidrawLinearElement,
   cx: number,
@@ -224,56 +236,9 @@ const getLinearElementRotatedBounds = (
   // first element is always the curve
   const ops = getCurvePathOps(shape[0]);
 
-  let currentP: Point = [0, 0];
-
-  const { minX, minY, maxX, maxY } = ops.reduce(
-    (limits, { op, data }) => {
-      // There are only four operation types:
-      // move, bcurveTo, lineTo, and curveTo
-      if (op === "move") {
-        // change starting point
-        currentP = (data as unknown) as Point;
-        // move operation does not draw anything; so, it always
-        // returns false
-      } else if (op === "bcurveTo") {
-        // create points from bezier curve
-        // bezier curve stores data as a flattened array of three positions
-        // [x1, y1, x2, y2, x3, y3]
-        const p1 = [data[0], data[1]] as Point;
-        const p2 = [data[2], data[3]] as Point;
-        const p3 = [data[4], data[5]] as Point;
-
-        const p0 = currentP;
-        currentP = p3;
-
-        const equation = (t: number, idx: number) =>
-          Math.pow(1 - t, 3) * p3[idx] +
-          3 * t * Math.pow(1 - t, 2) * p2[idx] +
-          3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
-          p0[idx] * Math.pow(t, 3);
-
-        let t = 0;
-        while (t <= 1.0) {
-          let x = equation(t, 0);
-          let y = equation(t, 1);
-          [x, y] = rotate(element.x + x, element.y + y, cx, cy, element.angle);
-          limits.minY = Math.min(limits.minY, y);
-          limits.minX = Math.min(limits.minX, x);
-          limits.maxX = Math.max(limits.maxX, x);
-          limits.maxY = Math.max(limits.maxY, y);
-          t += 0.1;
-        }
-      } else if (op === "lineTo") {
-        // TODO: Implement this
-      } else if (op === "qcurveTo") {
-        // TODO: Implement this
-      }
-      return limits;
-    },
-    { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
-  );
-
-  return [minX, minY, maxX, maxY];
+  const transformXY = (x: number, y: number) =>
+    rotate(element.x + x, element.y + y, cx, cy, element.angle);
+  return getMinMaxXYFromCurvePathOps(ops, transformXY);
 };
 
 export const getElementBounds = (
@@ -338,3 +303,40 @@ export const getCommonBounds = (
 
   return [minX, minY, maxX, maxY];
 };
+
+export const getResizedElementAbsoluteCoords = (
+  element: ExcalidrawElement,
+  nextWidth: number,
+  nextHeight: number,
+): [number, number, number, number] => {
+  if (!isLinearElement(element) || element.points.length <= 2) {
+    return [
+      element.x,
+      element.y,
+      element.x + nextWidth,
+      element.y + nextHeight,
+    ];
+  }
+
+  const points = rescalePoints(
+    0,
+    nextWidth,
+    rescalePoints(1, nextHeight, element.points),
+  );
+
+  const options: Options = {
+    strokeWidth: element.strokeWidth,
+    roughness: element.roughness,
+    seed: element.seed,
+  };
+  const gen = rough.generator();
+  const curve = gen.curve(points as [number, number][], options);
+  const ops = getCurvePathOps(curve);
+  const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
+  return [
+    minX + element.x,
+    minY + element.y,
+    maxX + element.x,
+    maxY + element.y,
+  ];
+};
diff --git a/src/element/index.ts b/src/element/index.ts
index 95b55f0c..b6d4c88c 100644
--- a/src/element/index.ts
+++ b/src/element/index.ts
@@ -17,7 +17,6 @@ export {
   getCommonBounds,
   getDiamondPoints,
   getArrowPoints,
-  getLinearElementAbsoluteBounds,
 } from "./bounds";
 
 export {
diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts
index e05e463e..60ad8a15 100644
--- a/src/element/resizeElements.ts
+++ b/src/element/resizeElements.ts
@@ -3,17 +3,21 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
 import { getSelectedElements, globalSceneState } from "../scene";
 import { rescalePoints } from "../points";
 
-import { rotate, resizeXYWidthHightWithRotation } from "../math";
+import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
 import {
   ExcalidrawLinearElement,
   NonDeletedExcalidrawElement,
   NonDeleted,
   ResizeArrowFnType,
 } from "./types";
-import { getElementAbsoluteCoords, getCommonBounds } from "./bounds";
+import {
+  getElementAbsoluteCoords,
+  getCommonBounds,
+  getResizedElementAbsoluteCoords,
+} from "./bounds";
 import { isLinearElement } from "./typeChecks";
 import { mutateElement } from "./mutateElement";
-import { getPerfectElementSize, normalizeDimensions } from "./sizeHelpers";
+import { getPerfectElementSize } from "./sizeHelpers";
 import {
   resizeTest,
   getCursorForResizingElement,
@@ -26,6 +30,155 @@ import {
 
 type ResizeTestType = ReturnType<typeof resizeTest>;
 
+export const resizeElements = (
+  resizeHandle: ResizeTestType,
+  setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
+  appState: AppState,
+  setAppState: (obj: any) => void,
+  resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
+  setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
+  event: PointerEvent, // XXX we want to make it independent?
+  xPointer: number,
+  yPointer: number,
+  lastX: number, // XXX eliminate in #1339
+  lastY: number, // XXX eliminate in #1339
+) => {
+  setAppState({
+    isResizing: resizeHandle && resizeHandle !== "rotation",
+    isRotating: resizeHandle === "rotation",
+  });
+  const selectedElements = getSelectedElements(
+    globalSceneState.getElements(),
+    appState,
+  );
+  const handleOffset = 4 / appState.zoom; // XXX import constant
+  const dashedLinePadding = 4 / appState.zoom; // XXX import constant
+  const offsetPointer = handleOffset + dashedLinePadding;
+  if (selectedElements.length === 1) {
+    const [element] = selectedElements;
+    if (resizeHandle === "rotation") {
+      rotateSingleElement(element, xPointer, yPointer, event.shiftKey);
+    } else if (
+      isLinearElement(element) &&
+      element.points.length === 2 &&
+      (resizeHandle === "nw" ||
+        resizeHandle === "ne" ||
+        resizeHandle === "sw" ||
+        resizeHandle === "se")
+    ) {
+      resizeSingleTwoPointElement(
+        element,
+        resizeHandle,
+        resizeArrowFn,
+        setResizeArrowFn,
+        event.shiftKey,
+        xPointer,
+        yPointer,
+        lastX,
+        lastY,
+      );
+    } else if (resizeHandle) {
+      resizeSingleElement(
+        element,
+        resizeHandle,
+        getResizeWithSidesSameLengthKey(event),
+        getResizeCenterPointKey(event),
+        xPointer,
+        yPointer,
+        offsetPointer,
+      );
+      setResizeHandle(normalizeResizeHandle(element, resizeHandle));
+      if (element.width < 0) {
+        mutateElement(element, { width: -element.width });
+      }
+      if (element.height < 0) {
+        mutateElement(element, { height: -element.height });
+      }
+    }
+
+    // XXX do we need this?
+    document.documentElement.style.cursor = getCursorForResizingElement({
+      element,
+      resizeHandle,
+    });
+    // XXX why do we need this?
+    if (appState.resizingElement) {
+      mutateElement(appState.resizingElement, {
+        x: element.x,
+        y: element.y,
+      });
+    }
+
+    return true;
+  } else if (
+    selectedElements.length > 1 &&
+    (resizeHandle === "nw" ||
+      resizeHandle === "ne" ||
+      resizeHandle === "sw" ||
+      resizeHandle === "se")
+  ) {
+    resizeMultipleElements(
+      selectedElements,
+      resizeHandle,
+      xPointer,
+      yPointer,
+      offsetPointer,
+    );
+    return true;
+  }
+  return false;
+};
+
+const rotateSingleElement = (
+  element: NonDeletedExcalidrawElement,
+  xPointer: number,
+  yPointer: number,
+  isAngleLocking: boolean,
+) => {
+  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 (isAngleLocking) {
+    angle += SHIFT_LOCKING_ANGLE / 2;
+    angle -= angle % SHIFT_LOCKING_ANGLE;
+  }
+  if (angle >= 2 * Math.PI) {
+    angle -= 2 * Math.PI;
+  }
+  mutateElement(element, { angle });
+};
+
+const resizeSingleTwoPointElement = (
+  element: NonDeleted<ExcalidrawLinearElement>,
+  resizeHandle: "nw" | "ne" | "sw" | "se",
+  resizeArrowFn: ResizeArrowFnType | null,
+  setResizeArrowFn: (fn: ResizeArrowFnType) => void,
+  sidesWithSameLength: boolean,
+  xPointer: number,
+  yPointer: number,
+  lastX: number,
+  lastY: number,
+) => {
+  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,
+    sidesWithSameLength,
+    xPointer,
+    yPointer,
+    lastX,
+    lastY,
+  );
+};
+
 const arrowResizeOrigin: ResizeArrowFnType = (
   element,
   pointIndex,
@@ -116,199 +269,196 @@ const applyResizeArrowFn = (
   setResizeArrowFn(resizeArrowFn);
 };
 
-export const resizeElements = (
-  resizeHandle: ResizeTestType,
-  setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
-  appState: AppState,
-  setAppState: (obj: any) => void,
-  resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
-  setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
-  event: PointerEvent, // XXX we want to make it independent?
+const resizeSingleElement = (
+  element: NonDeletedExcalidrawElement,
+  resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
+  sidesWithSameLength: boolean,
+  isResizeFromCenter: boolean,
   xPointer: number,
   yPointer: number,
-  lastX: number, // XXX eliminate in #1339
-  lastY: number, // XXX eliminate in #1339
+  offsetPointer: number,
 ) => {
-  setAppState({
-    isResizing: resizeHandle !== "rotation",
-    isRotating: resizeHandle === "rotation",
-  });
-  const selectedElements = getSelectedElements(
-    globalSceneState.getElements(),
-    appState,
+  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(
+    xPointer,
+    yPointer,
+    cx,
+    cy,
+    -element.angle,
   );
-  const handleOffset = 4 / appState.zoom; // XXX import constant
-  const dashedLinePadding = 4 / appState.zoom; // XXX import constant
-  const offsetPointer = handleOffset + dashedLinePadding;
-  const minSize = 0;
-  if (selectedElements.length === 1) {
-    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;
+  // XXX this might be slow with closure
+  const adjustWithOffsetPointer = (wh: number) => {
+    if (wh > offsetPointer) {
+      return wh - offsetPointer;
+    } else if (wh < -offsetPointer) {
+      return wh + offsetPointer;
+    }
+    return 0;
+  };
+  let scaleX = 1;
+  let scaleY = 1;
+  if (resizeHandle === "e" || resizeHandle === "ne" || resizeHandle === "se") {
+    scaleX = adjustWithOffsetPointer(rotatedX - x1) / (x2 - x1);
+  }
+  if (resizeHandle === "s" || resizeHandle === "sw" || resizeHandle === "se") {
+    scaleY = adjustWithOffsetPointer(rotatedY - y1) / (y2 - y1);
+  }
+  if (resizeHandle === "w" || resizeHandle === "nw" || resizeHandle === "sw") {
+    scaleX = adjustWithOffsetPointer(x2 - rotatedX) / (x2 - x1);
+  }
+  if (resizeHandle === "n" || resizeHandle === "nw" || resizeHandle === "ne") {
+    scaleY = adjustWithOffsetPointer(y2 - rotatedY) / (y2 - y1);
+  }
+  let nextWidth = element.width * scaleX;
+  let nextHeight = element.height * scaleY;
+  if (sidesWithSameLength) {
+    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 = isLinearElement(element)
+    ? {
+        points: rescalePoints(
+          0,
+          nextWidth,
+          rescalePoints(1, nextHeight, element.points),
+        ),
       }
-      if (angle >= 2 * Math.PI) {
-        angle -= 2 * Math.PI;
-      }
-      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,
+    : {};
+  const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords(
+    {
+      ...element,
+      ...rescaledPoints,
+    },
+    Math.abs(nextWidth),
+    Math.abs(nextHeight),
+  );
+  const [flipDiffX, flipDiffY] = getFlipAdjustment(
+    resizeHandle,
+    nextWidth,
+    nextHeight,
+    nextX1,
+    nextY1,
+    nextX2,
+    nextY2,
+    finalX1,
+    finalY1,
+    finalX2,
+    finalY2,
+    isLinearElement(element),
+    element.angle,
+  );
+  const [nextElementX, nextElementY] = adjustXYWithRotation(
+    resizeHandle,
+    element.x - flipDiffX,
+    element.y - flipDiffY,
+    element.angle,
+    deltaX1,
+    deltaY1,
+    deltaX2,
+    deltaY2,
+    isResizeFromCenter,
+  );
+  if (
+    nextWidth !== 0 &&
+    nextHeight !== 0 &&
+    Number.isFinite(nextElementX) &&
+    Number.isFinite(nextElementY)
+  ) {
+    mutateElement(element, {
+      width: nextWidth,
+      height: nextHeight,
+      x: nextElementX,
+      y: nextElementY,
+      ...rescaledPoints,
+    });
+  }
+};
+
+const resizeMultipleElements = (
+  elements: readonly NonDeletedExcalidrawElement[],
+  resizeHandle: "nw" | "ne" | "sw" | "se",
+  xPointer: number,
+  yPointer: number,
+  offsetPointer: number,
+) => {
+  const [x1, y1, x2, y2] = getCommonBounds(elements);
+  switch (resizeHandle) {
+    case "se": {
+      const scale = Math.max(
+        (xPointer - offsetPointer - x1) / (x2 - x1),
+        (yPointer - offsetPointer - y1) / (y2 - y1),
       );
-    } else if (resizeHandle) {
-      const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
-      const resized = resizeXYWidthHightWithRotation(
-        resizeHandle,
-        x1,
-        y1,
-        x2,
-        y2,
-        element.width,
-        element.height,
-        element.x,
-        element.y,
-        element.angle,
-        xPointer,
-        yPointer,
-        offsetPointer,
-        getResizeWithSidesSameLengthKey(event),
-        getResizeCenterPointKey(event),
-      );
-      if (
-        Math.abs(resized.width) > minSize &&
-        Math.abs(resized.height) > minSize
-      ) {
-        mutateElement(element, {
-          ...resized,
-          ...(isLinearElement(element)
-            ? {
-                points: rescalePoints(
-                  0,
-                  resized.width,
-                  rescalePoints(1, resized.height, element.points),
-                ),
-              }
-            : {}),
+      if (scale > 0) {
+        elements.forEach((element) => {
+          const width = element.width * scale;
+          const height = element.height * scale;
+          const x = element.x + (element.x - x1) * (scale - 1);
+          const y = element.y + (element.y - y1) * (scale - 1);
+          mutateElement(element, { width, height, x, y });
         });
       }
+      break;
     }
-
-    if (resizeHandle) {
-      setResizeHandle(normalizeResizeHandle(element, resizeHandle));
+    case "nw": {
+      const scale = Math.max(
+        (x2 - offsetPointer - xPointer) / (x2 - x1),
+        (y2 - offsetPointer - yPointer) / (y2 - y1),
+      );
+      if (scale > 0) {
+        elements.forEach((element) => {
+          const width = element.width * scale;
+          const height = element.height * scale;
+          const x = element.x - (x2 - element.x) * (scale - 1);
+          const y = element.y - (y2 - element.y) * (scale - 1);
+          mutateElement(element, { width, height, x, y });
+        });
+      }
+      break;
     }
-    normalizeDimensions(element);
-
-    // do we need this?
-    document.documentElement.style.cursor = getCursorForResizingElement({
-      element,
-      resizeHandle,
-    });
-    // why do we need this?
-    if (appState.resizingElement) {
-      mutateElement(appState.resizingElement, {
-        x: element.x,
-        y: element.y,
-      });
+    case "ne": {
+      const scale = Math.max(
+        (xPointer - offsetPointer - x1) / (x2 - x1),
+        (y2 - offsetPointer - yPointer) / (y2 - y1),
+      );
+      if (scale > 0) {
+        elements.forEach((element) => {
+          const width = element.width * scale;
+          const height = element.height * scale;
+          const x = element.x + (element.x - x1) * (scale - 1);
+          const y = element.y - (y2 - element.y) * (scale - 1);
+          mutateElement(element, { width, height, x, y });
+        });
+      }
+      break;
     }
-
-    return true;
-  } else if (selectedElements.length > 1) {
-    const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
-    const minScale = Math.max(minSize / (x2 - x1), minSize / (y2 - y1));
-    switch (resizeHandle) {
-      case "se": {
-        const scale = Math.max(
-          (xPointer - offsetPointer - x1) / (x2 - x1),
-          (yPointer - offsetPointer - y1) / (y2 - y1),
-        );
-        if (scale > minScale) {
-          selectedElements.forEach((element) => {
-            const width = element.width * scale;
-            const height = element.height * scale;
-            const x = element.x + (element.x - x1) * (scale - 1);
-            const y = element.y + (element.y - y1) * (scale - 1);
-            mutateElement(element, { width, height, x, y });
-          });
-        }
-        return true;
-      }
-      case "nw": {
-        const scale = Math.max(
-          (x2 - offsetPointer - xPointer) / (x2 - x1),
-          (y2 - offsetPointer - yPointer) / (y2 - y1),
-        );
-        if (scale > minScale) {
-          selectedElements.forEach((element) => {
-            const width = element.width * scale;
-            const height = element.height * scale;
-            const x = element.x - (x2 - element.x) * (scale - 1);
-            const y = element.y - (y2 - element.y) * (scale - 1);
-            mutateElement(element, { width, height, x, y });
-          });
-        }
-        return true;
-      }
-      case "ne": {
-        const scale = Math.max(
-          (xPointer - offsetPointer - x1) / (x2 - x1),
-          (y2 - offsetPointer - yPointer) / (y2 - y1),
-        );
-        if (scale > minScale) {
-          selectedElements.forEach((element) => {
-            const width = element.width * scale;
-            const height = element.height * scale;
-            const x = element.x + (element.x - x1) * (scale - 1);
-            const y = element.y - (y2 - element.y) * (scale - 1);
-            mutateElement(element, { width, height, x, y });
-          });
-        }
-        return true;
-      }
-      case "sw": {
-        const scale = Math.max(
-          (x2 - offsetPointer - xPointer) / (x2 - x1),
-          (yPointer - offsetPointer - y1) / (y2 - y1),
-        );
-        if (scale > minScale) {
-          selectedElements.forEach((element) => {
-            const width = element.width * scale;
-            const height = element.height * scale;
-            const x = element.x - (x2 - element.x) * (scale - 1);
-            const y = element.y + (element.y - y1) * (scale - 1);
-            mutateElement(element, { width, height, x, y });
-          });
-        }
-        return true;
+    case "sw": {
+      const scale = Math.max(
+        (x2 - offsetPointer - xPointer) / (x2 - x1),
+        (yPointer - offsetPointer - y1) / (y2 - y1),
+      );
+      if (scale > 0) {
+        elements.forEach((element) => {
+          const width = element.width * scale;
+          const height = element.height * scale;
+          const x = element.x - (x2 - element.x) * (scale - 1);
+          const y = element.y + (element.y - y1) * (scale - 1);
+          mutateElement(element, { width, height, x, y });
+        });
       }
+      break;
     }
   }
-  return false;
 };
 
 export const canResizeMutlipleElements = (
diff --git a/src/math.ts b/src/math.ts
index f5a6e1e3..08da7405 100644
--- a/src/math.ts
+++ b/src/math.ts
@@ -56,123 +56,118 @@ export function rotate(
   ];
 }
 
-const adjustXYWithRotation = (
+export const adjustXYWithRotation = (
   side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
   x: number,
   y: number,
   angle: number,
-  deltaX: number,
-  deltaY: number,
+  deltaX1: number,
+  deltaY1: number,
+  deltaX2: number,
+  deltaY2: number,
   isResizeFromCenter: boolean,
-) => {
+): [number, number] => {
   const cos = Math.cos(angle);
   const sin = Math.sin(angle);
   if (side === "e" || side === "ne" || side === "se") {
     if (isResizeFromCenter) {
-      x += deltaX;
+      x += deltaX1 + deltaX2;
     } else {
-      x += deltaX * (1 - cos);
-      y += deltaX * -sin;
+      x += deltaX1 * (1 + cos);
+      y += deltaX1 * sin;
+      x += deltaX2 * (1 - cos);
+      y += deltaX2 * -sin;
     }
   }
   if (side === "s" || side === "sw" || side === "se") {
     if (isResizeFromCenter) {
-      y += deltaY;
+      y += deltaY1 + deltaY2;
     } else {
-      x += deltaY * sin;
-      y += deltaY * (1 - cos);
+      x += deltaY1 * -sin;
+      y += deltaY1 * (1 + cos);
+      x += deltaY2 * sin;
+      y += deltaY2 * (1 - cos);
     }
   }
   if (side === "w" || side === "nw" || side === "sw") {
     if (isResizeFromCenter) {
-      x += deltaX;
+      x += deltaX1 + deltaX2;
     } else {
-      x += deltaX * (1 + cos);
-      y += deltaX * sin;
+      x += deltaX1 * (1 - cos);
+      y += deltaX1 * -sin;
+      x += deltaX2 * (1 + cos);
+      y += deltaX2 * sin;
     }
   }
   if (side === "n" || side === "nw" || side === "ne") {
     if (isResizeFromCenter) {
-      y += deltaY;
+      y += deltaY1 + deltaY2;
     } else {
-      x += deltaY * -sin;
-      y += deltaY * (1 + cos);
+      x += deltaY1 * sin;
+      y += deltaY1 * (1 - cos);
+      x += deltaY2 * -sin;
+      y += deltaY2 * (1 + cos);
     }
   }
-  return { x, y };
+  return [x, y];
 };
 
-export const resizeXYWidthHightWithRotation = (
+export const getFlipAdjustment = (
   side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
-  x1: number,
-  y1: number,
-  x2: number,
-  y2: number,
-  elementWidth: number,
-  elementHeight: number,
-  elementX: number,
-  elementY: number,
+  nextWidth: number,
+  nextHeight: number,
+  nextX1: number,
+  nextY1: number,
+  nextX2: number,
+  nextY2: number,
+  finalX1: number,
+  finalY1: number,
+  finalX2: number,
+  finalY2: number,
+  needsRotation: boolean,
   angle: number,
-  xPointer: number,
-  yPointer: number,
-  offsetPointer: number,
-  sidesWithSameLength: boolean,
-  isResizeFromCenter: boolean,
-) => {
-  // center point for rotation
-  const cx = (x1 + x2) / 2;
-  const cy = (y1 + y2) / 2;
-
-  // rotation with current angle
-  const [rotatedX, rotatedY] = rotate(xPointer, yPointer, cx, cy, -angle);
-
-  // XXX this might be slow with closure
-  const adjustWithOffsetPointer = (w: number) => {
-    if (w > offsetPointer) {
-      return w - offsetPointer;
-    } else if (w < -offsetPointer) {
-      return w + offsetPointer;
+): [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;
+      }
     }
-    return 0;
-  };
-
-  let scaleX = 1;
-  let scaleY = 1;
-  if (side === "e" || side === "ne" || side === "se") {
-    scaleX = adjustWithOffsetPointer(rotatedX - x1) / (x2 - x1);
   }
-  if (side === "s" || side === "sw" || side === "se") {
-    scaleY = adjustWithOffsetPointer(rotatedY - y1) / (y2 - y1);
+  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;
+      }
+    }
   }
-  if (side === "w" || side === "nw" || side === "sw") {
-    scaleX = adjustWithOffsetPointer(x2 - rotatedX) / (x2 - x1);
-  }
-  if (side === "n" || side === "nw" || side === "ne") {
-    scaleY = adjustWithOffsetPointer(y2 - rotatedY) / (y2 - y1);
-  }
-
-  let nextWidth = elementWidth * scaleX;
-  let nextHeight = elementHeight * scaleY;
-  if (sidesWithSameLength) {
-    nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
-  }
-
-  const deltaX = (elementWidth - nextWidth) / 2;
-  const deltaY = (elementHeight - nextHeight) / 2;
-
-  return {
-    width: nextWidth,
-    height: nextHeight,
-    ...adjustXYWithRotation(
-      side,
-      elementX,
-      elementY,
-      angle,
-      deltaX,
-      deltaY,
-      isResizeFromCenter,
-    ),
-  };
+  return [flipDiffX, flipDiffY];
 };
 
 export const getPointOnAPath = (point: Point, path: Point[]) => {