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

This commit is contained in:
Antonio Della Fortuna 2022-10-19 00:03:58 +02:00 committed by GitHub
parent 941b2d7042
commit 55110bf1b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 145 additions and 47 deletions

View File

@ -6,10 +6,14 @@ import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements"; import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
import { AppState } from "../types"; import { AppState } from "../types";
import { getTransformHandles } from "../element/transformHandles"; import { getTransformHandles } from "../element/transformHandles";
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding"; import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import {
getElementAbsoluteCoords,
getElementPointsCoords,
} from "../element/bounds";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
const enableActionFlipHorizontal = ( const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@ -118,13 +122,6 @@ const flipElement = (
const height = element.height; const height = element.height;
const originalAngle = normalizeAngle(element.angle); 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 // Rotate back to zero, if necessary
mutateElement(element, { mutateElement(element, {
angle: normalizeAngle(0), angle: normalizeAngle(0),
@ -132,7 +129,6 @@ const flipElement = (
// Flip unrotated by pulling TransformHandle to opposite side // Flip unrotated by pulling TransformHandle to opposite side
const transformHandles = getTransformHandles(element, appState.zoom); const transformHandles = getTransformHandles(element, appState.zoom);
let usingNWHandle = true; let usingNWHandle = true;
let newNCoordsX = 0;
let nHandle = transformHandles.nw; let nHandle = transformHandles.nw;
if (!nHandle) { if (!nHandle) {
// Use ne handle instead // 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)) { 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++) { for (let index = 1; index < element.points.length; index++) {
LinearElementEditor.movePoints(element, [ 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); LinearElementEditor.normalizePoints(element);
} else { } else {
// calculate new x-coord for transformation const elWidth = initialPointsCoords
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width; ? initialPointsCoords[2] - initialPointsCoords[0]
: initialElementAbsoluteCoords[2] - initialElementAbsoluteCoords[0];
const startPoint = initialPointsCoords
? [initialPointsCoords[0], initialPointsCoords[1]]
: [initialElementAbsoluteCoords[0], initialElementAbsoluteCoords[1]];
resizeSingleElement( resizeSingleElement(
new Map().set(element.id, element), new Map().set(element.id, element),
true, false,
element, element,
usingNWHandle ? "nw" : "ne", usingNWHandle ? "nw" : "ne",
false, true,
newNCoordsX, usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth,
nHandle[1], startPoint[1],
); );
// fix the size to account for handle sizes
mutateElement(element, {
width,
height,
});
} }
// Rotate by (360 degrees - original angle) // Rotate by (360 degrees - original angle)
@ -186,9 +203,34 @@ const flipElement = (
mutateElement(element, { mutateElement(element, {
x: originalX + finalOffsetX, x: originalX + finalOffsetX,
y: originalY, y: originalY,
width,
height,
}); });
updateBoundElements(element); 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) => { const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {

View File

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