فهرست منبع

fix: free draw flip not scaling correctly (#5752)

Antonio Della Fortuna 2 سال پیش
والد
کامیت
55110bf1b8
2فایلهای تغییر یافته به همراه150 افزوده شده و 52 حذف شده
  1. 64 22
      src/actions/actionFlip.ts
  2. 86 30
      src/tests/flip.test.tsx

+ 64 - 22
src/actions/actionFlip.ts

@@ -6,10 +6,14 @@ import { ExcalidrawElement, NonDeleted } from "../element/types";
 import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
 import { AppState } from "../types";
 import { getTransformHandles } from "../element/transformHandles";
-import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
 import { updateBoundElements } from "../element/binding";
-import { LinearElementEditor } from "../element/linearElementEditor";
 import { arrayToMap } from "../utils";
+import {
+  getElementAbsoluteCoords,
+  getElementPointsCoords,
+} from "../element/bounds";
+import { isLinearElement } from "../element/typeChecks";
+import { LinearElementEditor } from "../element/linearElementEditor";
 
 const enableActionFlipHorizontal = (
   elements: readonly ExcalidrawElement[],
@@ -118,13 +122,6 @@ const flipElement = (
   const height = element.height;
   const originalAngle = normalizeAngle(element.angle);
 
-  let finalOffsetX = 0;
-  if (isLinearElement(element) || isFreeDrawElement(element)) {
-    finalOffsetX =
-      element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
-      element.width;
-  }
-
   // Rotate back to zero, if necessary
   mutateElement(element, {
     angle: normalizeAngle(0),
@@ -132,7 +129,6 @@ const flipElement = (
   // Flip unrotated by pulling TransformHandle to opposite side
   const transformHandles = getTransformHandles(element, appState.zoom);
   let usingNWHandle = true;
-  let newNCoordsX = 0;
   let nHandle = transformHandles.nw;
   if (!nHandle) {
     // Use ne handle instead
@@ -146,30 +142,51 @@ const flipElement = (
     }
   }
 
+  let finalOffsetX = 0;
+  if (isLinearElement(element) && element.points.length < 3) {
+    finalOffsetX =
+      element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
+      element.width;
+  }
+
+  let initialPointsCoords;
   if (isLinearElement(element)) {
+    initialPointsCoords = getElementPointsCoords(
+      element,
+      element.points,
+      element.strokeSharpness,
+    );
+  }
+  const initialElementAbsoluteCoords = getElementAbsoluteCoords(element);
+
+  if (isLinearElement(element) && element.points.length < 3) {
     for (let index = 1; index < element.points.length; index++) {
       LinearElementEditor.movePoints(element, [
-        { index, point: [-element.points[index][0], element.points[index][1]] },
+        {
+          index,
+          point: [-element.points[index][0], element.points[index][1]],
+        },
       ]);
     }
     LinearElementEditor.normalizePoints(element);
   } else {
-    // calculate new x-coord for transformation
-    newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
+    const elWidth = initialPointsCoords
+      ? initialPointsCoords[2] - initialPointsCoords[0]
+      : initialElementAbsoluteCoords[2] - initialElementAbsoluteCoords[0];
+
+    const startPoint = initialPointsCoords
+      ? [initialPointsCoords[0], initialPointsCoords[1]]
+      : [initialElementAbsoluteCoords[0], initialElementAbsoluteCoords[1]];
+
     resizeSingleElement(
       new Map().set(element.id, element),
-      true,
+      false,
       element,
       usingNWHandle ? "nw" : "ne",
-      false,
-      newNCoordsX,
-      nHandle[1],
+      true,
+      usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth,
+      startPoint[1],
     );
-    // fix the size to account for handle sizes
-    mutateElement(element, {
-      width,
-      height,
-    });
   }
 
   // Rotate by (360 degrees - original angle)
@@ -186,9 +203,34 @@ const flipElement = (
   mutateElement(element, {
     x: originalX + finalOffsetX,
     y: originalY,
+    width,
+    height,
   });
 
   updateBoundElements(element);
+
+  if (initialPointsCoords && isLinearElement(element)) {
+    // Adjusting origin because when a beizer curve path exceeds min/max points it offsets the origin.
+    // There's still room for improvement since when the line roughness is > 1
+    // we still have a small offset of the origin when fliipping the element.
+    const finalPointsCoords = getElementPointsCoords(
+      element,
+      element.points,
+      element.strokeSharpness,
+    );
+
+    const topLeftCoordsDiff = initialPointsCoords[0] - finalPointsCoords[0];
+    const topRightCoordDiff = initialPointsCoords[2] - finalPointsCoords[2];
+
+    const coordsDiff = topLeftCoordsDiff + topRightCoordDiff;
+
+    mutateElement(element, {
+      x: element.x + coordsDiff * 0.5,
+      y: element.y,
+      width,
+      height,
+    });
+  }
 };
 
 const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {

+ 86 - 30
src/tests/flip.test.tsx

@@ -79,6 +79,8 @@ const createAndReturnOneDraw = (angle: number = 0) => {
   });
 };
 
+const FLIP_PRECISION_DECIMALS = 7;
+
 // Rectangle element
 
 it("flips an unrotated rectangle horizontally correctly", () => {
@@ -408,9 +410,15 @@ it("flips an unrotated arrow horizontally correctly", () => {
   h.app.actionManager.executeAction(actionFlipHorizontal);
 
   // Check if width and height did not change
-  expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
-
-  expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
+  expect(API.getSelectedElements()[0].width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
+
+  expect(API.getSelectedElements()[0].height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
 });
 
 it("flips an unrotated arrow vertically correctly", () => {
@@ -422,9 +430,15 @@ it("flips an unrotated arrow vertically correctly", () => {
   h.app.actionManager.executeAction(actionFlipVertical);
 
   // Check if width and height did not change
-  expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
-
-  expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
+  expect(API.getSelectedElements()[0].width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
+
+  expect(API.getSelectedElements()[0].height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
 });
 
 //@TODO fix the tests with rotation
@@ -439,10 +453,15 @@ it.skip("flips a rotated arrow horizontally correctly", () => {
   h.app.actionManager.executeAction(actionFlipHorizontal);
 
   // Check if width and height did not change
-  expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
-
-  expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
-
+  expect(API.getSelectedElements()[0].width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
+
+  expect(API.getSelectedElements()[0].height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
   // Check angle
   expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
 });
@@ -458,9 +477,15 @@ it.skip("flips a rotated arrow vertically correctly", () => {
   h.app.actionManager.executeAction(actionFlipVertical);
 
   // Check if width and height did not change
-  expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
+  expect(API.getSelectedElements()[0].width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
 
-  expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
+  expect(API.getSelectedElements()[0].height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
 
   // Check angle
   expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
@@ -477,9 +502,15 @@ it("flips an unrotated line horizontally correctly", () => {
   h.app.actionManager.executeAction(actionFlipHorizontal);
 
   // Check if width and height did not change
-  expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
-
-  expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
+  expect(API.getSelectedElements()[0].width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
+
+  expect(API.getSelectedElements()[0].height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
 });
 
 it("flips an unrotated line vertically correctly", () => {
@@ -491,9 +522,15 @@ it("flips an unrotated line vertically correctly", () => {
   h.app.actionManager.executeAction(actionFlipVertical);
 
   // Check if width and height did not change
-  expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
-
-  expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
+  expect(API.getSelectedElements()[0].width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
+
+  expect(API.getSelectedElements()[0].height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
 });
 
 it.skip("flips a rotated line horizontally correctly", () => {
@@ -508,9 +545,15 @@ it.skip("flips a rotated line horizontally correctly", () => {
   h.app.actionManager.executeAction(actionFlipHorizontal);
 
   // Check if width and height did not change
-  expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
+  expect(API.getSelectedElements()[0].width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
 
-  expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
+  expect(API.getSelectedElements()[0].height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
 
   // Check angle
   expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
@@ -528,9 +571,15 @@ it.skip("flips a rotated line vertically correctly", () => {
   h.app.actionManager.executeAction(actionFlipVertical);
 
   // Check if width and height did not change
-  expect(API.getSelectedElements()[0].width).toEqual(originalWidth);
+  expect(API.getSelectedElements()[0].width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
 
-  expect(API.getSelectedElements()[0].height).toEqual(originalHeight);
+  expect(API.getSelectedElements()[0].height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
 
   // Check angle
   expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle);
@@ -549,9 +598,9 @@ it("flips an unrotated drawing horizontally correctly", () => {
   h.app.actionManager.executeAction(actionFlipHorizontal);
 
   // Check if width and height did not change
-  expect(draw.width).toEqual(originalWidth);
+  expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS);
 
-  expect(draw.height).toEqual(originalHeight);
+  expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS);
 });
 
 it("flips an unrotated drawing vertically correctly", () => {
@@ -565,9 +614,9 @@ it("flips an unrotated drawing vertically correctly", () => {
   h.app.actionManager.executeAction(actionFlipVertical);
 
   // Check if width and height did not change
-  expect(draw.width).toEqual(originalWidth);
+  expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS);
 
-  expect(draw.height).toEqual(originalHeight);
+  expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS);
 });
 
 it("flips a rotated drawing horizontally correctly", () => {
@@ -584,9 +633,9 @@ it("flips a rotated drawing horizontally correctly", () => {
   h.app.actionManager.executeAction(actionFlipHorizontal);
 
   // Check if width and height did not change
-  expect(draw.width).toEqual(originalWidth);
+  expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS);
 
-  expect(draw.height).toEqual(originalHeight);
+  expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS);
 
   // Check angle
   expect(draw.angle).toBeCloseTo(expectedAngle);
@@ -606,9 +655,16 @@ it("flips a rotated drawing vertically correctly", () => {
   h.app.actionManager.executeAction(actionFlipVertical);
 
   // Check if width and height did not change
-  expect(API.getSelectedElement().width).toEqual(originalWidth);
 
-  expect(API.getSelectedElement().height).toEqual(originalHeight);
+  expect(API.getSelectedElement().width).toBeCloseTo(
+    originalWidth,
+    FLIP_PRECISION_DECIMALS,
+  );
+
+  expect(API.getSelectedElement().height).toBeCloseTo(
+    originalHeight,
+    FLIP_PRECISION_DECIMALS,
+  );
 
   // Check angle
   expect(API.getSelectedElement().angle).toBeCloseTo(expectedAngle);