improvement: Enhance resize for non generic elements (#2720)

This commit is contained in:
João Forja 2021-01-16 18:49:13 +00:00 committed by GitHub
parent c799b28a0e
commit e26f374ca6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 112 additions and 323 deletions

View File

@ -3587,9 +3587,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
transformElements( transformElements(
pointerDownState, pointerDownState,
transformHandleType, transformHandleType,
(newTransformHandle) => {
pointerDownState.resize.handleType = newTransformHandle;
},
selectedElements, selectedElements,
pointerDownState.resize.arrowDirection, pointerDownState.resize.arrowDirection,
getRotateWithDiscreteAngleKey(event), getRotateWithDiscreteAngleKey(event),

View File

@ -34,7 +34,6 @@ export {
export { export {
resizeTest, resizeTest,
getCursorForResizingElement, getCursorForResizingElement,
normalizeTransformHandleType,
getElementWithTransformHandleType, getElementWithTransformHandleType,
getTransformHandleTypeFromCoords, getTransformHandleTypeFromCoords,
} from "./resizeTest"; } from "./resizeTest";

View File

@ -4,7 +4,6 @@ import { rescalePoints } from "../points";
import { import {
rotate, rotate,
adjustXYWithRotation, adjustXYWithRotation,
getFlipAdjustment,
centerPoint, centerPoint,
rotatePoint, rotatePoint,
} from "../math"; } from "../math";
@ -13,21 +12,16 @@ import {
ExcalidrawTextElement, ExcalidrawTextElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
NonDeleted, NonDeleted,
ExcalidrawGenericElement,
ExcalidrawElement,
} from "./types"; } from "./types";
import { import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
getCommonBounds, getCommonBounds,
getResizedElementAbsoluteCoords, getResizedElementAbsoluteCoords,
} from "./bounds"; } from "./bounds";
import { isGenericElement, isLinearElement, isTextElement } from "./typeChecks"; import { isLinearElement, isTextElement } from "./typeChecks";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers"; import { getPerfectElementSize } from "./sizeHelpers";
import { import { getCursorForResizingElement } from "./resizeTest";
getCursorForResizingElement,
normalizeTransformHandleType,
} from "./resizeTest";
import { measureText, getFontString } from "../utils"; import { measureText, getFontString } from "../utils";
import { updateBoundElements } from "./binding"; import { updateBoundElements } from "./binding";
import { import {
@ -49,7 +43,6 @@ const normalizeAngle = (angle: number): number => {
export const transformElements = ( export const transformElements = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
setTransformHandle: (nextTransformHandle: MaybeTransformHandleType) => void,
selectedElements: readonly NonDeletedExcalidrawElement[], selectedElements: readonly NonDeletedExcalidrawElement[],
resizeArrowDirection: "origin" | "end", resizeArrowDirection: "origin" | "end",
isRotateWithDiscreteAngle: boolean, isRotateWithDiscreteAngle: boolean,
@ -101,8 +94,7 @@ export const transformElements = (
); );
updateBoundElements(element); updateBoundElements(element);
} else if (transformHandleType) { } else if (transformHandleType) {
if (isGenericElement(element)) { resizeSingleElement(
resizeSingleGenericElement(
pointerDownState.originalElements.get(element.id) as typeof element, pointerDownState.originalElements.get(element.id) as typeof element,
shouldKeepSidesRatio, shouldKeepSidesRatio,
element, element,
@ -111,26 +103,6 @@ export const transformElements = (
pointerX, pointerX,
pointerY, pointerY,
); );
} else {
const keepSquareAspectRatio = shouldKeepSidesRatio;
resizeSingleNonGenericElement(
element,
transformHandleType,
isResizeCenterPoint,
keepSquareAspectRatio,
pointerX,
pointerY,
);
setTransformHandle(
normalizeTransformHandleType(element, transformHandleType),
);
if (element.width < 0) {
mutateElement(element, { width: -element.width });
}
if (element.height < 0) {
mutateElement(element, { height: -element.height });
}
}
} }
// update cursor // update cursor
@ -414,8 +386,8 @@ const resizeSingleTextElement = (
} }
}; };
const resizeSingleGenericElement = ( const resizeSingleElement = (
stateAtResizeStart: NonDeleted<ExcalidrawGenericElement>, stateAtResizeStart: NonDeletedExcalidrawElement,
shouldKeepSidesRatio: boolean, shouldKeepSidesRatio: boolean,
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
transformHandleDirection: TransformHandleDirection, transformHandleDirection: TransformHandleDirection,
@ -423,251 +395,184 @@ const resizeSingleGenericElement = (
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(stateAtResizeStart); // Gets bounds corners
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
stateAtResizeStart,
stateAtResizeStart.width,
stateAtResizeStart.height,
);
const startTopLeft: Point = [x1, y1]; const startTopLeft: Point = [x1, y1];
const startBottomRight: Point = [x2, y2]; const startBottomRight: Point = [x2, y2];
const startCenter: Point = centerPoint(startTopLeft, startBottomRight); const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
// Calculate new dimensions based on cursor position // Calculate new dimensions based on cursor position
let newWidth = stateAtResizeStart.width;
let newHeight = stateAtResizeStart.height;
const rotatedPointer = rotatePoint( const rotatedPointer = rotatePoint(
[pointerX, pointerY], [pointerX, pointerY],
startCenter, startCenter,
-stateAtResizeStart.angle, -stateAtResizeStart.angle,
); );
//Get bounds corners rendered on screen
const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
element,
element.width,
element.height,
);
const boundsCurrentWidth = esx2 - esx1;
const boundsCurrentHeight = esy2 - esy1;
// It's important we set the initial scale value based on the width and height at resize start,
// otherwise previous dimensions affected by modifiers will be taken into account.
const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
const atStartBoundsHeight = startBottomRight[1] - startTopLeft[1];
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
if (transformHandleDirection.includes("e")) { if (transformHandleDirection.includes("e")) {
newWidth = rotatedPointer[0] - startTopLeft[0]; scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
} }
if (transformHandleDirection.includes("s")) { if (transformHandleDirection.includes("s")) {
newHeight = rotatedPointer[1] - startTopLeft[1]; scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight;
} }
if (transformHandleDirection.includes("w")) { if (transformHandleDirection.includes("w")) {
newWidth = startBottomRight[0] - rotatedPointer[0]; scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
} }
if (transformHandleDirection.includes("n")) { if (transformHandleDirection.includes("n")) {
newHeight = startBottomRight[1] - rotatedPointer[1]; scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
} }
// Linear elements dimensions differ from bounds dimensions
const eleInitialWidth = stateAtResizeStart.width;
const eleInitialHeight = stateAtResizeStart.height;
// We have to use dimensions of element on screen, otherwise the scaling of the
// dimensions won't match the cursor for linear elements.
let eleNewWidth = element.width * scaleX;
let eleNewHeight = element.height * scaleY;
// adjust dimensions for resizing from center // adjust dimensions for resizing from center
if (isResizeFromCenter) { if (isResizeFromCenter) {
newWidth = 2 * newWidth - stateAtResizeStart.width; eleNewWidth = 2 * eleNewWidth - eleInitialWidth;
newHeight = 2 * newHeight - stateAtResizeStart.height; eleNewHeight = 2 * eleNewHeight - eleInitialHeight;
} }
// adjust dimensions to keep sides ratio // adjust dimensions to keep sides ratio
if (shouldKeepSidesRatio) { if (shouldKeepSidesRatio) {
const widthRatio = Math.abs(newWidth) / stateAtResizeStart.width; const widthRatio = Math.abs(eleNewWidth) / eleInitialWidth;
const heightRatio = Math.abs(newHeight) / stateAtResizeStart.height; const heightRatio = Math.abs(eleNewHeight) / eleInitialHeight;
if (transformHandleDirection.length === 1) { if (transformHandleDirection.length === 1) {
newHeight *= widthRatio; eleNewHeight *= widthRatio;
newWidth *= heightRatio; eleNewWidth *= heightRatio;
} }
if (transformHandleDirection.length === 2) { if (transformHandleDirection.length === 2) {
const ratio = Math.max(widthRatio, heightRatio); const ratio = Math.max(widthRatio, heightRatio);
newWidth = stateAtResizeStart.width * ratio * Math.sign(newWidth); eleNewWidth = eleInitialWidth * ratio * Math.sign(eleNewWidth);
newHeight = stateAtResizeStart.height * ratio * Math.sign(newHeight); eleNewHeight = eleInitialHeight * ratio * Math.sign(eleNewHeight);
} }
} }
const [
newBoundsX1,
newBoundsY1,
newBoundsX2,
newBoundsY2,
] = getResizedElementAbsoluteCoords(
stateAtResizeStart,
eleNewWidth,
eleNewHeight,
);
const newBoundsWidth = newBoundsX2 - newBoundsX1;
const newBoundsHeight = newBoundsY2 - newBoundsY1;
// Calculate new topLeft based on fixed corner during resize // Calculate new topLeft based on fixed corner during resize
let newTopLeft = startTopLeft as [number, number]; let newTopLeft = [...startTopLeft] as [number, number];
if (["n", "w", "nw"].includes(transformHandleDirection)) { if (["n", "w", "nw"].includes(transformHandleDirection)) {
newTopLeft = [ newTopLeft = [
startBottomRight[0] - Math.abs(newWidth), startBottomRight[0] - Math.abs(newBoundsWidth),
startBottomRight[1] - Math.abs(newHeight), startBottomRight[1] - Math.abs(newBoundsHeight),
]; ];
} }
if (transformHandleDirection === "ne") { if (transformHandleDirection === "ne") {
const bottomLeft = [ const bottomLeft = [startTopLeft[0], startBottomRight[1]];
stateAtResizeStart.x, newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
stateAtResizeStart.y + stateAtResizeStart.height,
];
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newHeight)];
} }
if (transformHandleDirection === "sw") { if (transformHandleDirection === "sw") {
const topRight = [ const topRight = [startBottomRight[0], startTopLeft[1]];
stateAtResizeStart.x + stateAtResizeStart.width, newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
stateAtResizeStart.y,
];
newTopLeft = [topRight[0] - Math.abs(newWidth), topRight[1]];
} }
// Keeps opposite handle fixed during resize // Keeps opposite handle fixed during resize
if (shouldKeepSidesRatio) { if (shouldKeepSidesRatio) {
if (["s", "n"].includes(transformHandleDirection)) { if (["s", "n"].includes(transformHandleDirection)) {
newTopLeft[0] = startCenter[0] - newWidth / 2; newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
} }
if (["e", "w"].includes(transformHandleDirection)) { if (["e", "w"].includes(transformHandleDirection)) {
newTopLeft[1] = startCenter[1] - newHeight / 2; newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
} }
} }
// Flip horizontally // Flip horizontally
if (newWidth < 0) { if (eleNewWidth < 0) {
if (transformHandleDirection.includes("e")) { if (transformHandleDirection.includes("e")) {
newTopLeft[0] -= Math.abs(newWidth); newTopLeft[0] -= Math.abs(newBoundsWidth);
} }
if (transformHandleDirection.includes("w")) { if (transformHandleDirection.includes("w")) {
newTopLeft[0] += Math.abs(newWidth); newTopLeft[0] += Math.abs(newBoundsWidth);
} }
} }
// Flip vertically // Flip vertically
if (newHeight < 0) { if (eleNewHeight < 0) {
if (transformHandleDirection.includes("s")) { if (transformHandleDirection.includes("s")) {
newTopLeft[1] -= Math.abs(newHeight); newTopLeft[1] -= Math.abs(newBoundsHeight);
} }
if (transformHandleDirection.includes("n")) { if (transformHandleDirection.includes("n")) {
newTopLeft[1] += Math.abs(newHeight); newTopLeft[1] += Math.abs(newBoundsHeight);
} }
} }
if (isResizeFromCenter) { if (isResizeFromCenter) {
newTopLeft[0] = startCenter[0] - Math.abs(newWidth) / 2; newTopLeft[0] = startCenter[0] - Math.abs(newBoundsWidth) / 2;
newTopLeft[1] = startCenter[1] - Math.abs(newHeight) / 2; newTopLeft[1] = startCenter[1] - Math.abs(newBoundsHeight) / 2;
} }
// adjust topLeft to new rotation point // adjust topLeft to new rotation point
const angle = stateAtResizeStart.angle; const angle = stateAtResizeStart.angle;
const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle); const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
const newCenter: Point = [ const newCenter: Point = [
newTopLeft[0] + Math.abs(newWidth) / 2, newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
newTopLeft[1] + Math.abs(newHeight) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
]; ];
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle); const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle); newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
// Readjust points for linear elements
const rescaledPoints = rescalePointsInElement(
stateAtResizeStart,
eleNewWidth,
eleNewHeight,
);
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
// So we need to readjust (x,y) to be where the first point should be
const newOrigin = [...newTopLeft];
newOrigin[0] += stateAtResizeStart.x - newBoundsX1;
newOrigin[1] += stateAtResizeStart.y - newBoundsY1;
const resizedElement = { const resizedElement = {
width: Math.abs(newWidth), width: Math.abs(eleNewWidth),
height: Math.abs(newHeight), height: Math.abs(eleNewHeight),
x: newTopLeft[0], x: newOrigin[0],
y: newTopLeft[1], y: newOrigin[1],
...rescaledPoints,
}; };
if (
resizedElement.width !== 0 &&
resizedElement.height !== 0 &&
Number.isFinite(resizedElement.x) &&
Number.isFinite(resizedElement.y)
) {
updateBoundElements(element, { updateBoundElements(element, {
newSize: { width: resizedElement.width, height: resizedElement.height }, newSize: { width: resizedElement.width, height: resizedElement.height },
}); });
mutateElement(element, resizedElement); mutateElement(element, resizedElement);
};
const resizeSingleNonGenericElement = (
element: NonDeleted<Exclude<ExcalidrawElement, ExcalidrawGenericElement>>,
transformHandleType: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
isResizeFromCenter: boolean,
keepSquareAspectRatio: boolean,
pointerX: number,
pointerY: number,
) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
// rotation pointer with reverse angle
const [rotatedX, rotatedY] = rotate(
pointerX,
pointerY,
cx,
cy,
-element.angle,
);
let scaleX = 1;
let scaleY = 1;
if (
transformHandleType === "e" ||
transformHandleType === "ne" ||
transformHandleType === "se"
) {
scaleX = (rotatedX - x1) / (x2 - x1);
}
if (
transformHandleType === "s" ||
transformHandleType === "sw" ||
transformHandleType === "se"
) {
scaleY = (rotatedY - y1) / (y2 - y1);
}
if (
transformHandleType === "w" ||
transformHandleType === "nw" ||
transformHandleType === "sw"
) {
scaleX = (x2 - rotatedX) / (x2 - x1);
}
if (
transformHandleType === "n" ||
transformHandleType === "nw" ||
transformHandleType === "ne"
) {
scaleY = (y2 - rotatedY) / (y2 - y1);
}
let nextWidth = element.width * scaleX;
let nextHeight = element.height * scaleY;
if (keepSquareAspectRatio) {
nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
}
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
element,
nextWidth,
nextHeight,
);
const deltaX1 = (x1 - nextX1) / 2;
const deltaY1 = (y1 - nextY1) / 2;
const deltaX2 = (x2 - nextX2) / 2;
const deltaY2 = (y2 - nextY2) / 2;
const rescaledPoints = rescalePointsInElement(element, nextWidth, nextHeight);
updateBoundElements(element, {
newSize: { width: nextWidth, height: nextHeight },
});
const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords(
{
...element,
...rescaledPoints,
},
Math.abs(nextWidth),
Math.abs(nextHeight),
);
const [flipDiffX, flipDiffY] = getFlipAdjustment(
transformHandleType,
nextWidth,
nextHeight,
nextX1,
nextY1,
nextX2,
nextY2,
finalX1,
finalY1,
finalX2,
finalY2,
isLinearElement(element),
element.angle,
);
const [nextElementX, nextElementY] = adjustXYWithRotation(
getSidesForTransformHandle(transformHandleType, isResizeFromCenter),
element.x - flipDiffX,
element.y - flipDiffY,
element.angle,
deltaX1,
deltaY1,
deltaX2,
deltaY2,
);
if (
nextWidth !== 0 &&
nextHeight !== 0 &&
Number.isFinite(nextElementX) &&
Number.isFinite(nextElementY)
) {
mutateElement(element, {
width: nextWidth,
height: nextHeight,
x: nextElementX,
y: nextElementY,
...rescaledPoints,
});
} }
}; };

View File

@ -173,57 +173,3 @@ export const getCursorForResizingElement = (resizingElement: {
return cursor ? `${cursor}-resize` : ""; return cursor ? `${cursor}-resize` : "";
}; };
export const normalizeTransformHandleType = (
element: ExcalidrawElement,
transformHandleType: TransformHandleType,
): TransformHandleType => {
if (element.width >= 0 && element.height >= 0) {
return transformHandleType;
}
if (element.width < 0 && element.height < 0) {
switch (transformHandleType) {
case "nw":
return "se";
case "ne":
return "sw";
case "se":
return "nw";
case "sw":
return "ne";
}
} else if (element.width < 0) {
switch (transformHandleType) {
case "nw":
return "ne";
case "ne":
return "nw";
case "se":
return "sw";
case "sw":
return "se";
case "e":
return "w";
case "w":
return "e";
}
} else {
switch (transformHandleType) {
case "nw":
return "sw";
case "ne":
return "se";
case "se":
return "ne";
case "sw":
return "nw";
case "n":
return "s";
case "s":
return "n";
}
}
return transformHandleType;
};

View File

@ -70,64 +70,6 @@ export const adjustXYWithRotation = (
return [x, y]; return [x, y];
}; };
export const getFlipAdjustment = (
side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
nextWidth: number,
nextHeight: number,
nextX1: number,
nextY1: number,
nextX2: number,
nextY2: number,
finalX1: number,
finalY1: number,
finalX2: number,
finalY2: number,
needsRotation: boolean,
angle: number,
): [number, number] => {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let flipDiffX = 0;
let flipDiffY = 0;
if (nextWidth < 0) {
if (side === "e" || side === "ne" || side === "se") {
if (needsRotation) {
flipDiffX += (finalX2 - nextX1) * cos;
flipDiffY += (finalX2 - nextX1) * sin;
} else {
flipDiffX += finalX2 - nextX1;
}
}
if (side === "w" || side === "nw" || side === "sw") {
if (needsRotation) {
flipDiffX += (finalX1 - nextX2) * cos;
flipDiffY += (finalX1 - nextX2) * sin;
} else {
flipDiffX += finalX1 - nextX2;
}
}
}
if (nextHeight < 0) {
if (side === "s" || side === "se" || side === "sw") {
if (needsRotation) {
flipDiffY += (finalY2 - nextY1) * cos;
flipDiffX += (finalY2 - nextY1) * -sin;
} else {
flipDiffY += finalY2 - nextY1;
}
}
if (side === "n" || side === "ne" || side === "nw") {
if (needsRotation) {
flipDiffY += (finalY1 - nextY2) * cos;
flipDiffX += (finalY1 - nextY2) * -sin;
} else {
flipDiffY += finalY1 - nextY2;
}
}
}
return [flipDiffX, flipDiffY];
};
export const getPointOnAPath = (point: Point, path: Point[]) => { export const getPointOnAPath = (point: Point, path: Point[]) => {
const [px, py] = point; const [px, py] = point;
const [start, ...other] = path; const [start, ...other] = path;

View File

@ -41,7 +41,7 @@ describe("resize rectangle ellipses and diamond elements", () => {
${"s"} | ${[_, 39]} | ${[100, 139]} | ${[elemData.x, elemData.x]} ${"s"} | ${[_, 39]} | ${[100, 139]} | ${[elemData.x, elemData.x]}
${"e"} | ${[-20, _]} | ${[80, 100]} | ${[elemData.x, elemData.y]} ${"e"} | ${[-20, _]} | ${[80, 100]} | ${[elemData.x, elemData.y]}
${"w"} | ${[-20, _]} | ${[120, 100]} | ${[-20, elemData.y]} ${"w"} | ${[-20, _]} | ${[120, 100]} | ${[-20, elemData.y]}
${"ne"} | ${[10, 55]} | ${[110, 45]} | ${[elemData.x, 55]} ${"ne"} | ${[5, 55]} | ${[105, 45]} | ${[elemData.x, 55]}
${"se"} | ${[-30, -10]} | ${[70, 90]} | ${[elemData.x, elemData.y]} ${"se"} | ${[-30, -10]} | ${[70, 90]} | ${[elemData.x, elemData.y]}
${"nw"} | ${[-300, -200]} | ${[400, 300]} | ${[-300, -200]} ${"nw"} | ${[-300, -200]} | ${[400, 300]} | ${[-300, -200]}
${"sw"} | ${[40, -20]} | ${[60, 80]} | ${[40, 0]} ${"sw"} | ${[40, -20]} | ${[60, 80]} | ${[40, 0]}