From 8efe0b7d0534b947f75626bdac3678867cdb20df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Quinto?= Date: Thu, 9 Apr 2020 16:14:32 +0100 Subject: [PATCH] Shift loses pointer fixing #1296 (#1330) * change resize math to absolute instead of delta * typings * small change for width on rotation * apply absolute resize to other sides * revert&change math.ts * polish, polish, polish * refactor with offset * eliminate nextX * rename to offsetPointer * fix curved lines * prefer arrow function * remove unused variables/comments for now Co-authored-by: daishi --- src/element/resizeElements.ts | 356 +++++++++++----------------------- src/math.test.ts | 15 ++ src/math.ts | 88 +++++++-- 3 files changed, 205 insertions(+), 254 deletions(-) create mode 100644 src/math.test.ts diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index a1970e72..287d58ec 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -2,7 +2,7 @@ import { AppState } from "../types"; import { SHIFT_LOCKING_ANGLE } from "../constants"; import { getSelectedElements, globalSceneState } from "../scene"; import { rescalePoints } from "../points"; -import { rotate, adjustXYWithRotation } from "../math"; +import { rotate, resizeXYWidthHightWithRotation } from "../math"; import { ExcalidrawLinearElement, NonDeletedExcalidrawElement, @@ -27,7 +27,7 @@ export type ResizeArrowFnType = ( deltaY: number, pointerX: number, pointerY: number, - perfect: boolean, + sidesWithSameLength: boolean, ) => void; const arrowResizeOrigin: ResizeArrowFnType = ( @@ -37,7 +37,7 @@ const arrowResizeOrigin: ResizeArrowFnType = ( deltaY, pointerX, pointerY, - perfect, + sidesWithSameLength, ) => { const [px, py] = element.points[pointIndex]; let x = element.x + deltaX; @@ -45,7 +45,7 @@ const arrowResizeOrigin: ResizeArrowFnType = ( let pointX = px - deltaX; let pointY = py - deltaY; - if (perfect) { + if (sidesWithSameLength) { const { width, height } = getPerfectElementSize( element.type, px + element.x - pointerX, @@ -73,10 +73,10 @@ const arrowResizeEnd: ResizeArrowFnType = ( deltaY, pointerX, pointerY, - perfect, + sidesWithSameLength, ) => { const [px, py] = element.points[pointIndex]; - if (perfect) { + if (sidesWithSameLength) { const { width, height } = getPerfectElementSize( element.type, pointerX - element.x, @@ -96,7 +96,31 @@ const arrowResizeEnd: ResizeArrowFnType = ( } }; -export function resizeElements( +const applyResizeArrowFn = ( + element: NonDeleted, + resizeArrowFn: ResizeArrowFnType | null, + setResizeArrowFn: (fn: ResizeArrowFnType) => void, + isResizeEnd: boolean, + sidesWithSameLength: boolean, + x: number, + y: number, + lastX: number, + lastY: number, +) => { + const angle = element.angle; + const [deltaX, deltaY] = rotate(x - lastX, y - lastY, 0, 0, -angle); + if (!resizeArrowFn) { + if (isResizeEnd) { + resizeArrowFn = arrowResizeEnd; + } else { + resizeArrowFn = arrowResizeOrigin; + } + } + resizeArrowFn(element, 1, deltaX, deltaY, x, y, sidesWithSameLength); + setResizeArrowFn(resizeArrowFn); +}; + +export const resizeElements = ( resizeHandle: ResizeTestType, setResizeHandle: (nextResizeHandle: ResizeTestType) => void, appState: AppState, @@ -104,11 +128,11 @@ export function resizeElements( resizeArrowFn: ResizeArrowFnType | null, setResizeArrowFn: (fn: ResizeArrowFnType) => void, event: PointerEvent, - x: number, - y: number, + xPointer: number, + yPointer: number, lastX: number, lastY: number, -) { +) => { setAppState({ isResizing: resizeHandle !== "rotation", isRotating: resizeHandle === "rotation", @@ -117,224 +141,79 @@ export function resizeElements( globalSceneState.getElements(), appState, ); + const handleOffset = 4 / appState.zoom; // XXX import constant + const dashedLinePadding = 4 / appState.zoom; // XXX import constant + const offsetPointer = handleOffset + dashedLinePadding; + const minSize = handleOffset * 4; if (selectedElements.length === 1) { - const element = selectedElements[0]; - const angle = element.angle; - // reverse rotate delta - const [deltaX, deltaY] = rotate(x - lastX, y - lastY, 0, 0, -angle); - switch (resizeHandle) { - case "nw": - if (isLinearElement(element) && element.points.length === 2) { - const [, p1] = element.points; - - if (!resizeArrowFn) { - if (p1[0] < 0 || p1[1] < 0) { - resizeArrowFn = arrowResizeEnd; - } else { - resizeArrowFn = arrowResizeOrigin; - } - } - resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey); - setResizeArrowFn(resizeArrowFn); - } else { - const width = element.width - deltaX; - const height = event.shiftKey ? width : element.height - deltaY; - const dY = element.height - height; - mutateElement(element, { - width, - height, - ...adjustXYWithRotation("nw", element, deltaX, dY, angle), - ...(isLinearElement(element) && width !== 0 && height !== 0 - ? { - points: rescalePoints( - 0, - width, - rescalePoints(1, height, element.points), - ), - } - : {}), - }); - } - break; - case "ne": - if (isLinearElement(element) && element.points.length === 2) { - const [, p1] = element.points; - if (!resizeArrowFn) { - if (p1[0] >= 0) { - resizeArrowFn = arrowResizeEnd; - } else { - resizeArrowFn = arrowResizeOrigin; - } - } - resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey); - setResizeArrowFn(resizeArrowFn); - } else { - const width = element.width + deltaX; - const height = event.shiftKey ? width : element.height - deltaY; - const dY = element.height - height; - mutateElement(element, { - width, - height, - ...adjustXYWithRotation("ne", element, deltaX, dY, angle), - ...(isLinearElement(element) && width !== 0 && height !== 0 - ? { - points: rescalePoints( - 0, - width, - rescalePoints(1, height, element.points), - ), - } - : {}), - }); - } - break; - case "sw": - if (isLinearElement(element) && element.points.length === 2) { - const [, p1] = element.points; - if (!resizeArrowFn) { - if (p1[0] <= 0) { - resizeArrowFn = arrowResizeEnd; - } else { - resizeArrowFn = arrowResizeOrigin; - } - } - resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey); - setResizeArrowFn(resizeArrowFn); - } else { - const width = element.width - deltaX; - const height = event.shiftKey ? width : element.height + deltaY; - const dY = height - element.height; - mutateElement(element, { - width, - height, - ...adjustXYWithRotation("sw", element, deltaX, dY, angle), - ...(isLinearElement(element) && width !== 0 && height !== 0 - ? { - points: rescalePoints( - 0, - width, - rescalePoints(1, height, element.points), - ), - } - : {}), - }); - } - break; - case "se": - if (isLinearElement(element) && element.points.length === 2) { - const [, p1] = element.points; - if (!resizeArrowFn) { - if (p1[0] > 0 || p1[1] > 0) { - resizeArrowFn = arrowResizeEnd; - } else { - resizeArrowFn = arrowResizeOrigin; - } - } - resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey); - setResizeArrowFn(resizeArrowFn); - } else { - const width = element.width + deltaX; - const height = event.shiftKey ? width : element.height + deltaY; - const dY = height - element.height; - mutateElement(element, { - width, - height, - ...adjustXYWithRotation("se", element, deltaX, dY, angle), - ...(isLinearElement(element) && width !== 0 && height !== 0 - ? { - points: rescalePoints( - 0, - width, - rescalePoints(1, height, element.points), - ), - } - : {}), - }); - } - break; - case "n": { - const height = element.height - deltaY; - - if (isLinearElement(element) && height !== 0) { - mutateElement(element, { - height, - ...adjustXYWithRotation("n", element, 0, deltaY, angle), - points: rescalePoints(1, height, element.points), - }); - } else { - mutateElement(element, { - height, - ...adjustXYWithRotation("n", element, 0, deltaY, angle), - }); - } - - break; + const [element] = selectedElements; + if (resizeHandle === "rotation") { + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); + const cx = (x1 + x2) / 2; + const cy = (y1 + y2) / 2; + let angle = (5 * Math.PI) / 2 + Math.atan2(yPointer - cy, xPointer - cx); + if (event.shiftKey) { + angle += SHIFT_LOCKING_ANGLE / 2; + angle -= angle % SHIFT_LOCKING_ANGLE; } - case "w": { - const width = element.width - deltaX; - - if (isLinearElement(element) && width !== 0) { - mutateElement(element, { - width, - ...adjustXYWithRotation("w", element, deltaX, 0, angle), - points: rescalePoints(0, width, element.points), - }); - } else { - mutateElement(element, { - width, - ...adjustXYWithRotation("w", element, deltaX, 0, angle), - }); - } - break; + if (angle >= 2 * Math.PI) { + angle -= 2 * Math.PI; } - case "s": { - const height = element.height + deltaY; - - if (isLinearElement(element) && height !== 0) { - mutateElement(element, { - height, - ...adjustXYWithRotation("s", element, 0, deltaY, angle), - points: rescalePoints(1, height, element.points), - }); - } else { - mutateElement(element, { - height, - ...adjustXYWithRotation("s", element, 0, deltaY, angle), - }); - } - break; - } - case "e": { - const width = element.width + deltaX; - - if (isLinearElement(element) && width !== 0) { - mutateElement(element, { - width, - ...adjustXYWithRotation("e", element, deltaX, 0, angle), - points: rescalePoints(0, width, element.points), - }); - } else { - mutateElement(element, { - width, - ...adjustXYWithRotation("e", element, deltaX, 0, angle), - }); - } - break; - } - case "rotation": { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); - const cx = (x1 + x2) / 2; - const cy = (y1 + y2) / 2; - let angle = (5 * Math.PI) / 2 + Math.atan2(y - cy, x - cx); - if (event.shiftKey) { - angle += SHIFT_LOCKING_ANGLE / 2; - angle -= angle % SHIFT_LOCKING_ANGLE; - } - if (angle >= 2 * Math.PI) { - angle -= 2 * Math.PI; - } - mutateElement(element, { angle }); - break; + mutateElement(element, { angle }); + } else if ( + isLinearElement(element) && + element.points.length === 2 && + (resizeHandle === "nw" || + resizeHandle === "ne" || + resizeHandle === "sw" || + resizeHandle === "se") + ) { + const [, [px, py]] = element.points; + const isResizeEnd = + (resizeHandle === "nw" && (px < 0 || py < 0)) || + (resizeHandle === "ne" && px >= 0) || + (resizeHandle === "sw" && px <= 0) || + (resizeHandle === "se" && (px > 0 || py > 0)); + applyResizeArrowFn( + element, + resizeArrowFn, + setResizeArrowFn, + isResizeEnd, + event.shiftKey, + xPointer, + yPointer, + lastX, + lastY, + ); + } else if (resizeHandle) { + const [x1, y1] = getElementAbsoluteCoords(element); + const resized = resizeXYWidthHightWithRotation( + resizeHandle, + x1, + y1, + element.width, + element.height, + x1 - element.x, + y1 - element.y, + element.angle, + xPointer, + yPointer, + offsetPointer, + event.shiftKey, + ); + if (resized.width !== 0 && resized.height !== 0) { + mutateElement(element, { + ...resized, + ...(isLinearElement(element) + ? { + points: rescalePoints( + 0, + resized.width, + rescalePoints(1, resized.height, element.points), + ), + } + : {}), + }); } } @@ -359,15 +238,12 @@ export function resizeElements( return true; } else if (selectedElements.length > 1) { const [x1, y1, x2, y2] = getCommonBounds(selectedElements); - const handleOffset = 4 / appState.zoom; // XXX import constant - const dashedLinePadding = 4 / appState.zoom; // XXX import constant - const minSize = handleOffset * 4; const minScale = Math.max(minSize / (x2 - x1), minSize / (y2 - y1)); switch (resizeHandle) { case "se": { const scale = Math.max( - (x - handleOffset - dashedLinePadding - x1) / (x2 - x1), - (y - handleOffset - dashedLinePadding - y1) / (y2 - y1), + (xPointer - offsetPointer - x1) / (x2 - x1), + (yPointer - offsetPointer - y1) / (y2 - y1), ); if (scale > minScale) { selectedElements.forEach((element) => { @@ -382,8 +258,8 @@ export function resizeElements( } case "nw": { const scale = Math.max( - (x2 - handleOffset - dashedLinePadding - x) / (x2 - x1), - (y2 - handleOffset - dashedLinePadding - y) / (y2 - y1), + (x2 - offsetPointer - xPointer) / (x2 - x1), + (y2 - offsetPointer - yPointer) / (y2 - y1), ); if (scale > minScale) { selectedElements.forEach((element) => { @@ -398,8 +274,8 @@ export function resizeElements( } case "ne": { const scale = Math.max( - (x - handleOffset - dashedLinePadding - x1) / (x2 - x1), - (y2 - handleOffset - dashedLinePadding - y) / (y2 - y1), + (xPointer - offsetPointer - x1) / (x2 - x1), + (y2 - offsetPointer - yPointer) / (y2 - y1), ); if (scale > minScale) { selectedElements.forEach((element) => { @@ -414,8 +290,8 @@ export function resizeElements( } case "sw": { const scale = Math.max( - (x2 - handleOffset - dashedLinePadding - x) / (x2 - x1), - (y - handleOffset - dashedLinePadding - y1) / (y2 - y1), + (x2 - offsetPointer - xPointer) / (x2 - x1), + (yPointer - offsetPointer - y1) / (y2 - y1), ); if (scale > minScale) { selectedElements.forEach((element) => { @@ -431,12 +307,12 @@ export function resizeElements( } } return false; -} +}; -export function canResizeMutlipleElements( +export const canResizeMutlipleElements = ( elements: readonly NonDeletedExcalidrawElement[], -) { +) => { return elements.every((element) => ["rectangle", "diamond", "ellipse"].includes(element.type), ); -} +}; diff --git a/src/math.test.ts b/src/math.test.ts new file mode 100644 index 00000000..60ab5618 --- /dev/null +++ b/src/math.test.ts @@ -0,0 +1,15 @@ +import { rotate } from "./math"; + +describe("rotate", () => { + it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => { + const x1 = 10; + const y1 = 20; + const x2 = 20; + const y2 = 30; + const angle = Math.PI / 2; + const [rotatedX, rotatedY] = rotate(x1, y1, x2, y2, angle); + expect([rotatedX, rotatedY]).toEqual([30, 20]); + const res2 = rotate(rotatedX, rotatedY, x2, y2, -angle); + expect(res2).toEqual([x1, x2]); + }); +}); diff --git a/src/math.ts b/src/math.ts index 2a22ac31..6e4a36a4 100644 --- a/src/math.ts +++ b/src/math.ts @@ -56,32 +56,92 @@ export function rotate( ]; } -export function adjustXYWithRotation( +const adjustXYWithRotation = ( side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se", - position: { x: number; y: number }, + x: number, + y: number, + angle: number, deltaX: number, deltaY: number, - angle: number, -) { - let { x, y } = position; +) => { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + deltaX /= 2; + deltaY /= 2; if (side === "e" || side === "ne" || side === "se") { - x -= (deltaX / 2) * (1 - Math.cos(angle)); - y -= (deltaX / 2) * -Math.sin(angle); + x += deltaX * (1 - cos); + y += deltaX * -sin; } if (side === "s" || side === "sw" || side === "se") { - x -= (deltaY / 2) * Math.sin(angle); - y -= (deltaY / 2) * (1 - Math.cos(angle)); + x += deltaY * sin; + y += deltaY * (1 - cos); } if (side === "w" || side === "nw" || side === "sw") { - x += (deltaX / 2) * (1 + Math.cos(angle)); - y += (deltaX / 2) * Math.sin(angle); + x += deltaX * (1 + cos); + y += deltaX * sin; } if (side === "n" || side === "nw" || side === "ne") { - x += (deltaY / 2) * -Math.sin(angle); - y += (deltaY / 2) * (1 + Math.cos(angle)); + x += deltaY * -sin; + y += deltaY * (1 + cos); } return { x, y }; -} +}; + +export const resizeXYWidthHightWithRotation = ( + side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se", + x: number, + y: number, + width: number, + height: number, + offsetX: number, + offsetY: number, + angle: number, + xPointer: number, + yPointer: number, + offsetPointer: number, + sidesWithSameLength: boolean, +) => { + // center point for rotation + const cx = x + width / 2; + const cy = y + height / 2; + + // rotation with current angle + const [rotatedX, rotatedY] = rotate(xPointer, yPointer, cx, cy, -angle); + + let scaleX = 1; + let scaleY = 1; + if (side === "e" || side === "ne" || side === "se") { + scaleX = (rotatedX - offsetPointer - x) / width; + } + if (side === "s" || side === "sw" || side === "se") { + scaleY = (rotatedY - offsetPointer - y) / height; + } + if (side === "w" || side === "nw" || side === "sw") { + scaleX = (x + width - offsetPointer - rotatedX) / width; + } + if (side === "n" || side === "nw" || side === "ne") { + scaleY = (y + height - offsetPointer - rotatedY) / height; + } + + let nextWidth = width * scaleX; + let nextHeight = height * scaleY; + if (sidesWithSameLength) { + nextWidth = nextHeight = Math.max(nextWidth, nextHeight); + } + + return { + width: nextWidth, + height: nextHeight, + ...adjustXYWithRotation( + side, + x - offsetX, + y - offsetY, + angle, + width - nextWidth, + height - nextHeight, + ), + }; +}; export const getPointOnAPath = (point: Point, path: Point[]) => { const [px, py] = point;