mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: Elbow arrow fixedpoint flipping now properly flips on inverted resize and flip action (#8324)
* Flipping action now properly mirrors selections with elbow arrows * Flipping action now re-centers the selection to the original center to avoid "walking" selections on repeated flipping
This commit is contained in:
parent
44a1c8d857
commit
f3f0ab7c83
17 changed files with 290 additions and 85 deletions
89
packages/excalidraw/actions/actionFlip.test.tsx
Normal file
89
packages/excalidraw/actions/actionFlip.test.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import React from "react";
|
||||
import { Excalidraw } from "../index";
|
||||
import { render } from "../tests/test-utils";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { point } from "../../math";
|
||||
import { actionFlipHorizontal } from "./actionFlip";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
const testElements = [
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rec1",
|
||||
x: 1046,
|
||||
y: 541,
|
||||
width: 100,
|
||||
height: 100,
|
||||
boundElements: [
|
||||
{
|
||||
id: "arr",
|
||||
type: "arrow",
|
||||
},
|
||||
],
|
||||
}),
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rec2",
|
||||
x: 1169,
|
||||
y: 777,
|
||||
width: 102,
|
||||
height: 115,
|
||||
boundElements: [
|
||||
{
|
||||
id: "arr",
|
||||
type: "arrow",
|
||||
},
|
||||
],
|
||||
}),
|
||||
API.createElement({
|
||||
type: "arrow",
|
||||
id: "arrow",
|
||||
x: 1103.0717787616313,
|
||||
y: 536.8531862198708,
|
||||
width: 159.68539325842903,
|
||||
height: 333.0396003698186,
|
||||
startBinding: {
|
||||
elementId: "rec1",
|
||||
focus: 0.1366906474820229,
|
||||
gap: 5.000000000000057,
|
||||
fixedPoint: [0.5683453237410123, -0.05014327585315258],
|
||||
},
|
||||
endBinding: {
|
||||
elementId: "rec2",
|
||||
focus: 0.0014925373134265828,
|
||||
gap: 5,
|
||||
fixedPoint: [-0.04862325174825108, 0.4992537313432874],
|
||||
},
|
||||
points: [
|
||||
point(0, 0),
|
||||
point(0, -35),
|
||||
point(-97.80898876404626, -35),
|
||||
point(-97.80898876404626, 298.0396003698186),
|
||||
point(61.87640449438277, 298.0396003698186),
|
||||
],
|
||||
elbowed: true,
|
||||
}),
|
||||
];
|
||||
|
||||
describe("flipping action", () => {
|
||||
it("flip re-centers the selection even after multiple flip actions", async () => {
|
||||
await render(<Excalidraw initialData={{ elements: testElements }} />);
|
||||
|
||||
API.setSelectedElements(testElements);
|
||||
|
||||
expect(Object.keys(h.state.selectedElementIds).length).toBe(3);
|
||||
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
|
||||
const rec1 = h.elements.find((el) => el.id === "rec1");
|
||||
expect(rec1?.x).toBeCloseTo(1113.78, 0);
|
||||
expect(rec1?.y).toBeCloseTo(541, 0);
|
||||
|
||||
const rec2 = h.elements.find((el) => el.id === "rec2");
|
||||
expect(rec2?.x).toBeCloseTo(988.72, 0);
|
||||
expect(rec2?.y).toBeCloseTo(777, 0);
|
||||
});
|
||||
});
|
|
@ -2,6 +2,7 @@ import { register } from "./register";
|
|||
import { getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import type {
|
||||
ExcalidrawElbowArrowElement,
|
||||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
NonDeletedSceneElementsMap,
|
||||
|
@ -18,7 +19,9 @@ import {
|
|||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||
import { flipHorizontal, flipVertical } from "../components/icons";
|
||||
import { StoreAction } from "../store";
|
||||
import { isLinearElement } from "../element/typeChecks";
|
||||
import { isElbowArrow, isLinearElement } from "../element/typeChecks";
|
||||
import { mutateElbowArrow } from "../element/routing";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
|
||||
export const actionFlipHorizontal = register({
|
||||
name: "flipHorizontal",
|
||||
|
@ -109,7 +112,8 @@ const flipElements = (
|
|||
flipDirection: "horizontal" | "vertical",
|
||||
app: AppClassProperties,
|
||||
): ExcalidrawElement[] => {
|
||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
|
||||
const { minX, minY, maxX, maxY, midX, midY } =
|
||||
getCommonBoundingBox(selectedElements);
|
||||
|
||||
resizeMultipleElements(
|
||||
elementsMap,
|
||||
|
@ -131,5 +135,48 @@ const flipElements = (
|
|||
[],
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// flipping arrow elements (and potentially other) makes the selection group
|
||||
// "move" across the canvas because of how arrows can bump against the "wall"
|
||||
// of the selection, so we need to center the group back to the original
|
||||
// position so that repeated flips don't accumulate the offset
|
||||
|
||||
const { elbowArrows, otherElements } = selectedElements.reduce(
|
||||
(
|
||||
acc: {
|
||||
elbowArrows: ExcalidrawElbowArrowElement[];
|
||||
otherElements: ExcalidrawElement[];
|
||||
},
|
||||
element,
|
||||
) =>
|
||||
isElbowArrow(element)
|
||||
? { ...acc, elbowArrows: acc.elbowArrows.concat(element) }
|
||||
: { ...acc, otherElements: acc.otherElements.concat(element) },
|
||||
{ elbowArrows: [], otherElements: [] },
|
||||
);
|
||||
|
||||
const { midX: newMidX, midY: newMidY } =
|
||||
getCommonBoundingBox(selectedElements);
|
||||
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||
otherElements.forEach((element) =>
|
||||
mutateElement(element, {
|
||||
x: element.x + diffX,
|
||||
y: element.y + diffY,
|
||||
}),
|
||||
);
|
||||
elbowArrows.forEach((element) =>
|
||||
mutateElbowArrow(
|
||||
element,
|
||||
elementsMap,
|
||||
element.points,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
informMutation: false,
|
||||
},
|
||||
),
|
||||
);
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
return selectedElements;
|
||||
};
|
||||
|
|
|
@ -1685,19 +1685,6 @@ export const actionChangeArrowType = register({
|
|||
: {}),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
mutateElement(
|
||||
newElement,
|
||||
{
|
||||
startBinding: newElement.startBinding
|
||||
? { ...newElement.startBinding, fixedPoint: null }
|
||||
: null,
|
||||
endBinding: newElement.endBinding
|
||||
? { ...newElement.endBinding, fixedPoint: null }
|
||||
: null,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return newElement;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue