mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
feat: add flipping multiple elements with a command
This commit is contained in:
parent
b9a185d098
commit
c7667cc7d7
2 changed files with 20 additions and 165 deletions
|
@ -1,41 +1,26 @@
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
|
||||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||||
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
import { resizeMultipleElements } from "../element/resizeElements";
|
||||||
import { AppState } from "../types";
|
import { AppState, PointerDownState } from "../types";
|
||||||
import { getTransformHandles } from "../element/transformHandles";
|
|
||||||
import { updateBoundElements } from "../element/binding";
|
import { updateBoundElements } from "../element/binding";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import {
|
|
||||||
getElementAbsoluteCoords,
|
|
||||||
getElementPointsCoords,
|
|
||||||
} from "../element/bounds";
|
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
|
|
||||||
const enableActionFlipHorizontal = (
|
const enableActionFlipHorizontal = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const eligibleElements = getSelectedElements(
|
return true;
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableActionFlipVertical = (
|
const enableActionFlipVertical = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const eligibleElements = getSelectedElements(
|
return true;
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return eligibleElements.length === 1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
|
@ -81,11 +66,6 @@ const flipSelectedElements = (
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
// remove once we allow for groups of elements to be flipped
|
|
||||||
if (selectedElements.length > 1) {
|
|
||||||
return elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedElements = flipElements(
|
const updatedElements = flipElements(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
appState,
|
appState,
|
||||||
|
@ -104,144 +84,18 @@ const flipElements = (
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
elements.forEach((element) => {
|
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
|
||||||
flipElement(element, appState);
|
|
||||||
// If vertical flip, rotate an extra 180
|
resizeMultipleElements(
|
||||||
if (flipDirection === "vertical") {
|
{ originalElements: arrayToMap(elements) } as PointerDownState,
|
||||||
rotateElement(element, Math.PI);
|
elements,
|
||||||
}
|
"nw",
|
||||||
});
|
true,
|
||||||
|
flipDirection === "horizontal" ? maxX : minX,
|
||||||
|
flipDirection === "horizontal" ? minY : maxY,
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.forEach((element) => updateBoundElements(element));
|
||||||
|
|
||||||
return elements;
|
return elements;
|
||||||
};
|
};
|
||||||
|
|
||||||
const flipElement = (
|
|
||||||
element: NonDeleted<ExcalidrawElement>,
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const originalX = element.x;
|
|
||||||
const originalY = element.y;
|
|
||||||
const width = element.width;
|
|
||||||
const height = element.height;
|
|
||||||
const originalAngle = normalizeAngle(element.angle);
|
|
||||||
|
|
||||||
// Rotate back to zero, if necessary
|
|
||||||
mutateElement(element, {
|
|
||||||
angle: normalizeAngle(0),
|
|
||||||
});
|
|
||||||
// Flip unrotated by pulling TransformHandle to opposite side
|
|
||||||
const transformHandles = getTransformHandles(element, appState.zoom);
|
|
||||||
let usingNWHandle = true;
|
|
||||||
let nHandle = transformHandles.nw;
|
|
||||||
if (!nHandle) {
|
|
||||||
// Use ne handle instead
|
|
||||||
usingNWHandle = false;
|
|
||||||
nHandle = transformHandles.ne;
|
|
||||||
if (!nHandle) {
|
|
||||||
mutateElement(element, {
|
|
||||||
angle: originalAngle,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
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]],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
LinearElementEditor.normalizePoints(element);
|
|
||||||
} else {
|
|
||||||
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),
|
|
||||||
false,
|
|
||||||
element,
|
|
||||||
usingNWHandle ? "nw" : "ne",
|
|
||||||
true,
|
|
||||||
usingNWHandle ? startPoint[0] + elWidth : startPoint[0] - elWidth,
|
|
||||||
startPoint[1],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotate by (360 degrees - original angle)
|
|
||||||
let angle = normalizeAngle(2 * Math.PI - originalAngle);
|
|
||||||
if (angle < 0) {
|
|
||||||
// check, probably unnecessary
|
|
||||||
angle = normalizeAngle(angle + 2 * Math.PI);
|
|
||||||
}
|
|
||||||
mutateElement(element, {
|
|
||||||
angle,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Move back to original spot to appear "flipped in place"
|
|
||||||
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);
|
|
||||||
|
|
||||||
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 originalX = element.x;
|
|
||||||
const originalY = element.y;
|
|
||||||
let angle = normalizeAngle(element.angle + rotationAngle);
|
|
||||||
if (angle < 0) {
|
|
||||||
// check, probably unnecessary
|
|
||||||
angle = normalizeAngle(2 * Math.PI + angle);
|
|
||||||
}
|
|
||||||
mutateElement(element, {
|
|
||||||
angle,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Move back to original spot
|
|
||||||
mutateElement(element, {
|
|
||||||
x: originalX,
|
|
||||||
y: originalY,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -587,7 +587,7 @@ export const resizeSingleElement = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resizeMultipleElements = (
|
export const resizeMultipleElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||||
|
@ -684,6 +684,7 @@ const resizeMultipleElements = (
|
||||||
const x = anchorX + flipFactorX * (offsetX * scale + flipAdjustX);
|
const x = anchorX + flipFactorX * (offsetX * scale + flipAdjustX);
|
||||||
const y = anchorY + flipFactorY * (offsetY * scale + flipAdjustY);
|
const y = anchorY + flipFactorY * (offsetY * scale + flipAdjustY);
|
||||||
|
|
||||||
|
// TODO curved lines adjustment
|
||||||
// readjust points for linear & free draw elements
|
// readjust points for linear & free draw elements
|
||||||
const rescaledPoints = rescalePointsInElement(
|
const rescaledPoints = rescalePointsInElement(
|
||||||
element.orig,
|
element.orig,
|
||||||
|
|
Loading…
Add table
Reference in a new issue