mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: add flipping for multiple elements (#5578)
* feat: add flipping when resizing multiple elements * fix: image elements not flipping its content * test: fix accidental resizing in grouping test * fix: angles not flipping vertically when resizing * feat: add flipping multiple elements with a command * revert: image elements not flipping its content This reverts commit cb989a6c66e62a02a8c04ce41f12507806c8d0a0. * fix: add special cases for flipping text & images * fix: a few corner cases for flipping * fix: remove angle flip * fix: bound text scaling when resizing * fix: linear elements drifting away after multiple flips * revert: fix linear elements drifting away after multiple flips This reverts commitbffc33dd3f
. * fix: linear elements unstable bounds * revert: linear elements unstable bounds This reverts commit22ae9b02c4
. * fix: hand-drawn lines shift after flipping * test: fix flipping tests * test: fix the number of context menu items * fix: incorrect scaling due to ignoring bound text when finding selection bounds * fix: bound text coordinates not being updated * fix: lines bound text rotation * fix: incorrect placement of bound lines on flip * remove redundant predicates in actionFlip * update test * refactor resizeElement with some renaming and comments * fix grouped bounded text elements not being flipped correctly * combine mutation for bounded text element * remove incorrect return * fix: linear elements bindings after flipping * revert: remove incorrect return This reverts commite6b205ca90
. * fix: minimum size for all elements in selection --------- Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com>
This commit is contained in:
parent
75bea48b54
commit
6459ccda6a
7 changed files with 240 additions and 282 deletions
|
@ -197,7 +197,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipHorizontal",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -207,7 +206,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipVertical",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -4594,7 +4592,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipHorizontal",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -4604,7 +4601,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipVertical",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -5144,7 +5140,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipHorizontal",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -5154,7 +5149,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipVertical",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -6003,7 +5997,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipHorizontal",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -6013,7 +6006,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipVertical",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -6349,7 +6341,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipHorizontal",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
@ -6359,7 +6350,6 @@ Object {
|
|||
"keyTest": [Function],
|
||||
"name": "flipVertical",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
|
|
|
@ -15332,7 +15332,10 @@ Object {
|
|||
"penMode": false,
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": Object {
|
||||
"id0": true,
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
"id3": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollX": 0,
|
||||
|
@ -15342,7 +15345,6 @@ Object {
|
|||
"id0": true,
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
"id3": true,
|
||||
"id5": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
|
@ -15390,7 +15392,7 @@ Object {
|
|||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1505387817,
|
||||
"versionNonce": 23633383,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
|
@ -15421,7 +15423,7 @@ Object {
|
|||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 23633383,
|
||||
"versionNonce": 493213705,
|
||||
"width": 10,
|
||||
"x": 30,
|
||||
"y": 10,
|
||||
|
@ -15452,7 +15454,7 @@ Object {
|
|||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 493213705,
|
||||
"versionNonce": 915032327,
|
||||
"width": 10,
|
||||
"x": 50,
|
||||
"y": 10,
|
||||
|
@ -15803,7 +15805,6 @@ Object {
|
|||
"id0": true,
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
"id3": true,
|
||||
"id5": true,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
|
@ -15833,7 +15834,7 @@ Object {
|
|||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1505387817,
|
||||
"versionNonce": 23633383,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
|
@ -15861,7 +15862,7 @@ Object {
|
|||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 23633383,
|
||||
"versionNonce": 493213705,
|
||||
"width": 10,
|
||||
"x": 30,
|
||||
"y": 10,
|
||||
|
@ -15889,7 +15890,7 @@ Object {
|
|||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 493213705,
|
||||
"versionNonce": 915032327,
|
||||
"width": 10,
|
||||
"x": 50,
|
||||
"y": 10,
|
||||
|
|
|
@ -207,6 +207,8 @@ describe("contextMenu element", () => {
|
|||
"deleteSelectedElements",
|
||||
"group",
|
||||
"addToLibrary",
|
||||
"flipHorizontal",
|
||||
"flipVertical",
|
||||
"sendBackward",
|
||||
"bringForward",
|
||||
"sendToBack",
|
||||
|
@ -258,6 +260,8 @@ describe("contextMenu element", () => {
|
|||
"deleteSelectedElements",
|
||||
"ungroup",
|
||||
"addToLibrary",
|
||||
"flipHorizontal",
|
||||
"flipVertical",
|
||||
"sendBackward",
|
||||
"bringForward",
|
||||
"sendToBack",
|
||||
|
|
|
@ -195,10 +195,8 @@ const checkElementsBoundingBox = async (
|
|||
debugger;
|
||||
await waitFor(() => {
|
||||
// Check if width and height did not change
|
||||
expect(x1 - toleranceInPx <= x12 && x12 <= x1 + toleranceInPx).toBeTruthy();
|
||||
expect(y1 - toleranceInPx <= y12 && y12 <= y1 + toleranceInPx).toBeTruthy();
|
||||
expect(x2 - toleranceInPx <= x22 && x22 <= x2 + toleranceInPx).toBeTruthy();
|
||||
expect(y2 - toleranceInPx <= y22 && y22 <= y2 + toleranceInPx).toBeTruthy();
|
||||
expect(x2 - x1).toBeCloseTo(x22 - x12, -1);
|
||||
expect(y2 - y1).toBeCloseTo(y22 - y12, -1);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -216,14 +214,22 @@ const checkTwoPointsLineHorizontalFlip = async () => {
|
|||
h.app.actionManager.executeAction(actionFlipHorizontal);
|
||||
const newElement = h.elements[0] as ExcalidrawLinearElement;
|
||||
await waitFor(() => {
|
||||
expect(originalElement.points[0][0]).toEqual(
|
||||
newElement.points[0][0] !== 0 ? -newElement.points[0][0] : 0,
|
||||
expect(originalElement.points[0][0]).toBeCloseTo(
|
||||
-newElement.points[0][0],
|
||||
5,
|
||||
);
|
||||
expect(originalElement.points[0][1]).toEqual(newElement.points[0][1]);
|
||||
expect(originalElement.points[1][0]).toEqual(
|
||||
newElement.points[1][0] !== 0 ? -newElement.points[1][0] : 0,
|
||||
expect(originalElement.points[0][1]).toBeCloseTo(
|
||||
newElement.points[0][1],
|
||||
5,
|
||||
);
|
||||
expect(originalElement.points[1][0]).toBeCloseTo(
|
||||
-newElement.points[1][0],
|
||||
5,
|
||||
);
|
||||
expect(originalElement.points[1][1]).toBeCloseTo(
|
||||
newElement.points[1][1],
|
||||
5,
|
||||
);
|
||||
expect(originalElement.points[1][1]).toEqual(newElement.points[1][1]);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -234,14 +240,22 @@ const checkTwoPointsLineVerticalFlip = async () => {
|
|||
h.app.actionManager.executeAction(actionFlipVertical);
|
||||
const newElement = h.elements[0] as ExcalidrawLinearElement;
|
||||
await waitFor(() => {
|
||||
expect(originalElement.points[0][0]).toEqual(
|
||||
newElement.points[0][0] !== 0 ? -newElement.points[0][0] : 0,
|
||||
expect(originalElement.points[0][0]).toBeCloseTo(
|
||||
newElement.points[0][0],
|
||||
5,
|
||||
);
|
||||
expect(originalElement.points[0][1]).toEqual(newElement.points[0][1]);
|
||||
expect(originalElement.points[1][0]).toEqual(
|
||||
newElement.points[1][0] !== 0 ? -newElement.points[1][0] : 0,
|
||||
expect(originalElement.points[0][1]).toBeCloseTo(
|
||||
-newElement.points[0][1],
|
||||
5,
|
||||
);
|
||||
expect(originalElement.points[1][0]).toBeCloseTo(
|
||||
newElement.points[1][0],
|
||||
5,
|
||||
);
|
||||
expect(originalElement.points[1][1]).toBeCloseTo(
|
||||
-newElement.points[1][1],
|
||||
5,
|
||||
);
|
||||
expect(originalElement.points[1][1]).toEqual(newElement.points[1][1]);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -318,7 +332,7 @@ describe("rectangle", () => {
|
|||
|
||||
it("flips a rotated rectangle vertically correctly", async () => {
|
||||
const originalAngle = (3 * Math.PI) / 4;
|
||||
const expectedAgnle = Math.PI / 4;
|
||||
const expectedAgnle = (5 * Math.PI) / 4;
|
||||
|
||||
createAndSelectOneRectangle(originalAngle);
|
||||
|
||||
|
@ -351,7 +365,7 @@ describe("diamond", () => {
|
|||
|
||||
it("flips a rotated diamond vertically correctly", async () => {
|
||||
const originalAngle = (5 * Math.PI) / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const expectedAngle = (3 * Math.PI) / 4;
|
||||
|
||||
createAndSelectOneDiamond(originalAngle);
|
||||
|
||||
|
@ -384,7 +398,7 @@ describe("ellipse", () => {
|
|||
|
||||
it("flips a rotated ellipse vertically correctly", async () => {
|
||||
const originalAngle = (7 * Math.PI) / 4;
|
||||
const expectedAngle = (5 * Math.PI) / 4;
|
||||
const expectedAngle = Math.PI / 4;
|
||||
|
||||
createAndSelectOneEllipse(originalAngle);
|
||||
|
||||
|
@ -429,7 +443,7 @@ describe("arrow", () => {
|
|||
|
||||
it("flips a rotated arrow vertically with line inside min/max points bounds", async () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (3 * Math.PI) / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||
h.app.scene.replaceAllElements([line]);
|
||||
h.app.state.selectedElementIds[line.id] = true;
|
||||
|
@ -481,7 +495,7 @@ describe("arrow", () => {
|
|||
//TODO: elements with curve outside minMax points have a wrong bounding box!!!
|
||||
it.skip("flips a rotated arrow vertically with line outside min/max points bounds", async () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (3 * Math.PI) / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
|
||||
mutateElement(line, { angle: originalAngle });
|
||||
h.app.scene.replaceAllElements([line]);
|
||||
|
@ -512,7 +526,6 @@ describe("arrow", () => {
|
|||
|
||||
it("flips a two points arrow vertically correctly", async () => {
|
||||
createAndSelectOneArrow();
|
||||
|
||||
await checkTwoPointsLineVerticalFlip();
|
||||
});
|
||||
});
|
||||
|
@ -581,7 +594,7 @@ describe("line", () => {
|
|||
//TODO: elements with curve outside minMax points have a wrong bounding box
|
||||
it.skip("flips a rotated line vertically with line outside min/max points bounds", async () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (3 * Math.PI) / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
|
||||
mutateElement(line, { angle: originalAngle });
|
||||
h.app.scene.replaceAllElements([line]);
|
||||
|
@ -616,7 +629,7 @@ describe("line", () => {
|
|||
|
||||
it("flips a rotated line vertically with line inside min/max points bounds", async () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (3 * Math.PI) / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||
h.app.scene.replaceAllElements([line]);
|
||||
h.app.state.selectedElementIds[line.id] = true;
|
||||
|
@ -670,7 +683,7 @@ describe("freedraw", () => {
|
|||
|
||||
it("flips a rotated drawing vertically correctly", async () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (3 * Math.PI) / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
|
||||
const draw = createAndReturnOneDraw(originalAngle);
|
||||
// select draw, since not done automatically
|
||||
|
@ -718,8 +731,8 @@ describe("image", () => {
|
|||
});
|
||||
|
||||
await checkVerticalFlip();
|
||||
expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]);
|
||||
expect(h.elements[0].angle).toBeCloseTo(Math.PI);
|
||||
expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, -1]);
|
||||
expect(h.elements[0].angle).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
it("flips an rotated image horizontally correctly", async () => {
|
||||
|
@ -742,7 +755,7 @@ describe("image", () => {
|
|||
|
||||
it("flips an rotated image vertically correctly", async () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (3 * Math.PI) / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
//paste image
|
||||
await createImage();
|
||||
await waitFor(() => {
|
||||
|
@ -757,7 +770,7 @@ describe("image", () => {
|
|||
});
|
||||
|
||||
await checkRotatedVerticalFlip(expectedAngle);
|
||||
expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]);
|
||||
expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, -1]);
|
||||
expect(h.elements[0].angle).toBeCloseTo(expectedAngle);
|
||||
});
|
||||
|
||||
|
@ -772,7 +785,7 @@ describe("image", () => {
|
|||
});
|
||||
|
||||
await checkVerticalHorizontalFlip();
|
||||
expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
|
||||
expect(h.elements[0].angle).toBeCloseTo(Math.PI);
|
||||
expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, -1]);
|
||||
expect(h.elements[0].angle).toBeCloseTo(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -542,7 +542,7 @@ describe("regression tests", () => {
|
|||
expect(element.groupIds.length).toBe(1);
|
||||
}
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(-10, -10); // the NW resizing handle is at [0, 0], so moving further
|
||||
mouse.down();
|
||||
mouse.restorePosition(...end);
|
||||
mouse.up();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue