feat: add flipping multiple elements with a command

This commit is contained in:
Alex Kim 2022-12-19 19:50:23 +03:00
parent b9a185d098
commit c7667cc7d7
No known key found for this signature in database
GPG key ID: CEE74CFA44D238D7
2 changed files with 20 additions and 165 deletions

View file

@ -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,
});
};

View file

@ -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,