diff --git a/src/components/App.tsx b/src/components/App.tsx index faca963c..d680034c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3587,9 +3587,6 @@ class App extends React.Component { transformElements( pointerDownState, transformHandleType, - (newTransformHandle) => { - pointerDownState.resize.handleType = newTransformHandle; - }, selectedElements, pointerDownState.resize.arrowDirection, getRotateWithDiscreteAngleKey(event), diff --git a/src/element/index.ts b/src/element/index.ts index e49bc633..63fdcfff 100644 --- a/src/element/index.ts +++ b/src/element/index.ts @@ -34,7 +34,6 @@ export { export { resizeTest, getCursorForResizingElement, - normalizeTransformHandleType, getElementWithTransformHandleType, getTransformHandleTypeFromCoords, } from "./resizeTest"; diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index 199e3a48..96c216b0 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -4,7 +4,6 @@ import { rescalePoints } from "../points"; import { rotate, adjustXYWithRotation, - getFlipAdjustment, centerPoint, rotatePoint, } from "../math"; @@ -13,21 +12,16 @@ import { ExcalidrawTextElement, NonDeletedExcalidrawElement, NonDeleted, - ExcalidrawGenericElement, - ExcalidrawElement, } from "./types"; import { getElementAbsoluteCoords, getCommonBounds, getResizedElementAbsoluteCoords, } from "./bounds"; -import { isGenericElement, isLinearElement, isTextElement } from "./typeChecks"; +import { isLinearElement, isTextElement } from "./typeChecks"; import { mutateElement } from "./mutateElement"; import { getPerfectElementSize } from "./sizeHelpers"; -import { - getCursorForResizingElement, - normalizeTransformHandleType, -} from "./resizeTest"; +import { getCursorForResizingElement } from "./resizeTest"; import { measureText, getFontString } from "../utils"; import { updateBoundElements } from "./binding"; import { @@ -49,7 +43,6 @@ const normalizeAngle = (angle: number): number => { export const transformElements = ( pointerDownState: PointerDownState, transformHandleType: MaybeTransformHandleType, - setTransformHandle: (nextTransformHandle: MaybeTransformHandleType) => void, selectedElements: readonly NonDeletedExcalidrawElement[], resizeArrowDirection: "origin" | "end", isRotateWithDiscreteAngle: boolean, @@ -101,36 +94,15 @@ export const transformElements = ( ); updateBoundElements(element); } else if (transformHandleType) { - if (isGenericElement(element)) { - resizeSingleGenericElement( - pointerDownState.originalElements.get(element.id) as typeof element, - shouldKeepSidesRatio, - element, - transformHandleType, - isResizeCenterPoint, - pointerX, - 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 }); - } - } + resizeSingleElement( + pointerDownState.originalElements.get(element.id) as typeof element, + shouldKeepSidesRatio, + element, + transformHandleType, + isResizeCenterPoint, + pointerX, + pointerY, + ); } // update cursor @@ -414,8 +386,8 @@ const resizeSingleTextElement = ( } }; -const resizeSingleGenericElement = ( - stateAtResizeStart: NonDeleted, +const resizeSingleElement = ( + stateAtResizeStart: NonDeletedExcalidrawElement, shouldKeepSidesRatio: boolean, element: NonDeletedExcalidrawElement, transformHandleDirection: TransformHandleDirection, @@ -423,251 +395,184 @@ const resizeSingleGenericElement = ( pointerX: 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 startBottomRight: Point = [x2, y2]; const startCenter: Point = centerPoint(startTopLeft, startBottomRight); // Calculate new dimensions based on cursor position - let newWidth = stateAtResizeStart.width; - let newHeight = stateAtResizeStart.height; const rotatedPointer = rotatePoint( [pointerX, pointerY], startCenter, -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")) { - newWidth = rotatedPointer[0] - startTopLeft[0]; + scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth; } if (transformHandleDirection.includes("s")) { - newHeight = rotatedPointer[1] - startTopLeft[1]; + scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight; } if (transformHandleDirection.includes("w")) { - newWidth = startBottomRight[0] - rotatedPointer[0]; + scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth; } 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 if (isResizeFromCenter) { - newWidth = 2 * newWidth - stateAtResizeStart.width; - newHeight = 2 * newHeight - stateAtResizeStart.height; + eleNewWidth = 2 * eleNewWidth - eleInitialWidth; + eleNewHeight = 2 * eleNewHeight - eleInitialHeight; } // adjust dimensions to keep sides ratio if (shouldKeepSidesRatio) { - const widthRatio = Math.abs(newWidth) / stateAtResizeStart.width; - const heightRatio = Math.abs(newHeight) / stateAtResizeStart.height; + const widthRatio = Math.abs(eleNewWidth) / eleInitialWidth; + const heightRatio = Math.abs(eleNewHeight) / eleInitialHeight; if (transformHandleDirection.length === 1) { - newHeight *= widthRatio; - newWidth *= heightRatio; + eleNewHeight *= widthRatio; + eleNewWidth *= heightRatio; } if (transformHandleDirection.length === 2) { const ratio = Math.max(widthRatio, heightRatio); - newWidth = stateAtResizeStart.width * ratio * Math.sign(newWidth); - newHeight = stateAtResizeStart.height * ratio * Math.sign(newHeight); + eleNewWidth = eleInitialWidth * ratio * Math.sign(eleNewWidth); + 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 - let newTopLeft = startTopLeft as [number, number]; + let newTopLeft = [...startTopLeft] as [number, number]; if (["n", "w", "nw"].includes(transformHandleDirection)) { newTopLeft = [ - startBottomRight[0] - Math.abs(newWidth), - startBottomRight[1] - Math.abs(newHeight), + startBottomRight[0] - Math.abs(newBoundsWidth), + startBottomRight[1] - Math.abs(newBoundsHeight), ]; } if (transformHandleDirection === "ne") { - const bottomLeft = [ - stateAtResizeStart.x, - stateAtResizeStart.y + stateAtResizeStart.height, - ]; - newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newHeight)]; + const bottomLeft = [startTopLeft[0], startBottomRight[1]]; + newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)]; } if (transformHandleDirection === "sw") { - const topRight = [ - stateAtResizeStart.x + stateAtResizeStart.width, - stateAtResizeStart.y, - ]; - newTopLeft = [topRight[0] - Math.abs(newWidth), topRight[1]]; + const topRight = [startBottomRight[0], startTopLeft[1]]; + newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]]; } // Keeps opposite handle fixed during resize if (shouldKeepSidesRatio) { if (["s", "n"].includes(transformHandleDirection)) { - newTopLeft[0] = startCenter[0] - newWidth / 2; + newTopLeft[0] = startCenter[0] - newBoundsWidth / 2; } if (["e", "w"].includes(transformHandleDirection)) { - newTopLeft[1] = startCenter[1] - newHeight / 2; + newTopLeft[1] = startCenter[1] - newBoundsHeight / 2; } } // Flip horizontally - if (newWidth < 0) { + if (eleNewWidth < 0) { if (transformHandleDirection.includes("e")) { - newTopLeft[0] -= Math.abs(newWidth); + newTopLeft[0] -= Math.abs(newBoundsWidth); } if (transformHandleDirection.includes("w")) { - newTopLeft[0] += Math.abs(newWidth); + newTopLeft[0] += Math.abs(newBoundsWidth); } } // Flip vertically - if (newHeight < 0) { + if (eleNewHeight < 0) { if (transformHandleDirection.includes("s")) { - newTopLeft[1] -= Math.abs(newHeight); + newTopLeft[1] -= Math.abs(newBoundsHeight); } if (transformHandleDirection.includes("n")) { - newTopLeft[1] += Math.abs(newHeight); + newTopLeft[1] += Math.abs(newBoundsHeight); } } if (isResizeFromCenter) { - newTopLeft[0] = startCenter[0] - Math.abs(newWidth) / 2; - newTopLeft[1] = startCenter[1] - Math.abs(newHeight) / 2; + newTopLeft[0] = startCenter[0] - Math.abs(newBoundsWidth) / 2; + newTopLeft[1] = startCenter[1] - Math.abs(newBoundsHeight) / 2; } // adjust topLeft to new rotation point const angle = stateAtResizeStart.angle; const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle); const newCenter: Point = [ - newTopLeft[0] + Math.abs(newWidth) / 2, - newTopLeft[1] + Math.abs(newHeight) / 2, + newTopLeft[0] + Math.abs(newBoundsWidth) / 2, + newTopLeft[1] + Math.abs(newBoundsHeight) / 2, ]; const rotatedNewCenter = rotatePoint(newCenter, startCenter, 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 = { - width: Math.abs(newWidth), - height: Math.abs(newHeight), - x: newTopLeft[0], - y: newTopLeft[1], + width: Math.abs(eleNewWidth), + height: Math.abs(eleNewHeight), + x: newOrigin[0], + y: newOrigin[1], + ...rescaledPoints, }; - updateBoundElements(element, { - newSize: { width: resizedElement.width, height: resizedElement.height }, - }); - mutateElement(element, resizedElement); -}; - -const resizeSingleNonGenericElement = ( - element: NonDeleted>, - 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) + resizedElement.width !== 0 && + resizedElement.height !== 0 && + Number.isFinite(resizedElement.x) && + Number.isFinite(resizedElement.y) ) { - mutateElement(element, { - width: nextWidth, - height: nextHeight, - x: nextElementX, - y: nextElementY, - ...rescaledPoints, + updateBoundElements(element, { + newSize: { width: resizedElement.width, height: resizedElement.height }, }); + mutateElement(element, resizedElement); } }; diff --git a/src/element/resizeTest.ts b/src/element/resizeTest.ts index 4471244d..3a794e2c 100644 --- a/src/element/resizeTest.ts +++ b/src/element/resizeTest.ts @@ -173,57 +173,3 @@ export const getCursorForResizingElement = (resizingElement: { 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; -}; diff --git a/src/math.ts b/src/math.ts index dadc6ccc..15a8aa6e 100644 --- a/src/math.ts +++ b/src/math.ts @@ -70,64 +70,6 @@ export const adjustXYWithRotation = ( 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[]) => { const [px, py] = point; const [start, ...other] = path; diff --git a/src/tests/resize.test.tsx b/src/tests/resize.test.tsx index 8d207c08..70c79fe6 100644 --- a/src/tests/resize.test.tsx +++ b/src/tests/resize.test.tsx @@ -41,7 +41,7 @@ describe("resize rectangle ellipses and diamond elements", () => { ${"s"} | ${[_, 39]} | ${[100, 139]} | ${[elemData.x, elemData.x]} ${"e"} | ${[-20, _]} | ${[80, 100]} | ${[elemData.x, 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]} ${"nw"} | ${[-300, -200]} | ${[400, 300]} | ${[-300, -200]} ${"sw"} | ${[40, -20]} | ${[60, 80]} | ${[40, 0]}