diff --git a/src/actions/actionFlip.ts b/src/actions/actionFlip.ts index 671c0dce..f88b8b26 100644 --- a/src/actions/actionFlip.ts +++ b/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) => { diff --git a/src/tests/flip.test.tsx b/src/tests/flip.test.tsx index 3f3288d8..a388ad4e 100644 --- a/src/tests/flip.test.tsx +++ b/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].width).toBeCloseTo( + originalWidth, + FLIP_PRECISION_DECIMALS, + ); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); + 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].width).toBeCloseTo( + originalWidth, + FLIP_PRECISION_DECIMALS, + ); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); + 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].width).toBeCloseTo( + originalWidth, + FLIP_PRECISION_DECIMALS, + ); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); + 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].width).toBeCloseTo( + originalWidth, + FLIP_PRECISION_DECIMALS, + ); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); + 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);