From 394237728f445fc393396ee2ae949914e359fc5e Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Mon, 11 May 2020 00:41:36 +0900 Subject: [PATCH] Refactor: resize two-point lines/arrows (#1568) --- src/components/App.tsx | 53 ++++---- src/element/index.ts | 1 + src/element/resizeElements.ts | 221 ++++++++++++---------------------- src/element/types.ts | 10 -- 4 files changed, 100 insertions(+), 185 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 8e42fe1a..49724983 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -25,6 +25,7 @@ import { getElementWithResizeHandler, canResizeMutlipleElements, getResizeOffsetXY, + getResizeArrowDirection, getResizeHandlerFromCoords, isNonDeletedElement, } from "../element"; @@ -53,11 +54,7 @@ import Portal from "./Portal"; import { renderScene } from "../renderer"; import { AppState, GestureEvent, Gesture } from "../types"; -import { - ExcalidrawElement, - ExcalidrawTextElement, - ResizeArrowFnType, -} from "../element/types"; +import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { distance2d, isPathALoop } from "../math"; @@ -1847,6 +1844,7 @@ class App extends React.Component { resizeHandle = nextResizeHandle; }; let resizeOffsetXY: [number, number] = [0, 0]; + let resizeArrowDirection: "origin" | "end" = "origin"; let isResizingElements = false; let draggingOccurred = false; let hitElement: ExcalidrawElement | null = null; @@ -1900,6 +1898,16 @@ class App extends React.Component { x, y, ); + if ( + selectedElements.length === 1 && + isLinearElement(selectedElements[0]) && + selectedElements[0].points.length === 2 + ) { + resizeArrowDirection = getResizeArrowDirection( + resizeHandle, + selectedElements[0], + ); + } } if (!isResizingElements) { hitElement = getElementAtPosition( @@ -2079,11 +2087,6 @@ class App extends React.Component { } } - let resizeArrowFn: ResizeArrowFnType | null = null; - const setResizeArrowFn = (fn: ResizeArrowFnType) => { - resizeArrowFn = fn; - }; - let selectedElementWasDuplicated = false; const onPointerMove = withBatchedUpdates((event: PointerEvent) => { @@ -2142,23 +2145,17 @@ class App extends React.Component { isResizing: resizeHandle && resizeHandle !== "rotation", isRotating: resizeHandle === "rotation", }); - const resized = resizeElements( - resizeHandle, - setResizeHandle, - selectedElements, - resizeArrowFn, - setResizeArrowFn, - event, - x, - y, - resizeOffsetXY[0], - resizeOffsetXY[1], - lastX, - lastY, - ); - if (resized) { - lastX = x; - lastY = y; + if ( + resizeElements( + resizeHandle, + setResizeHandle, + selectedElements, + resizeArrowDirection, + event, + x - resizeOffsetXY[0], + y - resizeOffsetXY[1], + ) + ) { return; } } @@ -2328,8 +2325,6 @@ class App extends React.Component { this.savePointer(childEvent.clientX, childEvent.clientY, "up"); - resizeArrowFn = null; - resizeOffsetXY = [0, 0]; lastPointerUp = null; window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove); diff --git a/src/element/index.ts b/src/element/index.ts index 4d639365..7cecd010 100644 --- a/src/element/index.ts +++ b/src/element/index.ts @@ -36,6 +36,7 @@ export { resizeElements, canResizeMutlipleElements, getResizeOffsetXY, + getResizeArrowDirection, } from "./resizeElements"; export { isTextElement, isExcalidrawElement } from "./typeChecks"; export { textWysiwyg } from "./textWysiwyg"; diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index f19ac9a4..89a1e33d 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -6,7 +6,6 @@ import { ExcalidrawLinearElement, NonDeletedExcalidrawElement, NonDeleted, - ResizeArrowFnType, } from "./types"; import { getElementAbsoluteCoords, @@ -32,15 +31,10 @@ export const resizeElements = ( resizeHandle: ResizeTestType, setResizeHandle: (nextResizeHandle: ResizeTestType) => void, selectedElements: NonDeletedExcalidrawElement[], - resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339 - setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339 + resizeArrowDirection: "origin" | "end", event: PointerEvent, // XXX we want to make it independent? pointerX: number, pointerY: number, - offsetX: number, - offsetY: number, - lastX: number, // XXX eliminate in #1339 - lastY: number, // XXX eliminate in #1339 ) => { if (selectedElements.length === 1) { const [element] = selectedElements; @@ -56,14 +50,10 @@ export const resizeElements = ( ) { resizeSingleTwoPointElement( element, - resizeHandle, - resizeArrowFn, - setResizeArrowFn, + resizeArrowDirection, event.shiftKey, pointerX, pointerY, - lastX, - lastY, ); } else if (resizeHandle) { resizeSingleElement( @@ -73,8 +63,6 @@ export const resizeElements = ( getResizeCenterPointKey(event), pointerX, pointerY, - offsetX, - offsetY, ); setResizeHandle(normalizeResizeHandle(element, resizeHandle)); if (element.width < 0) { @@ -100,14 +88,7 @@ export const resizeElements = ( resizeHandle === "sw" || resizeHandle === "se") ) { - resizeMultipleElements( - selectedElements, - resizeHandle, - pointerX, - pointerY, - offsetX, - offsetY, - ); + resizeMultipleElements(selectedElements, resizeHandle, pointerX, pointerY); return true; } return false; @@ -135,122 +116,61 @@ const rotateSingleElement = ( const resizeSingleTwoPointElement = ( element: NonDeleted, - resizeHandle: "nw" | "ne" | "sw" | "se", - resizeArrowFn: ResizeArrowFnType | null, - setResizeArrowFn: (fn: ResizeArrowFnType) => void, - sidesWithSameLength: boolean, + resizeArrowDirection: "origin" | "end", + isAngleLocking: boolean, pointerX: number, pointerY: number, - lastX: number, - lastY: number, ) => { - 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, - sidesWithSameLength, - pointerX, - pointerY, - lastX, - lastY, - ); -}; - -const arrowResizeOrigin: ResizeArrowFnType = ( - element, - pointIndex, - deltaX, - deltaY, - pointerX, - pointerY, - sidesWithSameLength, -) => { - const [px, py] = element.points[pointIndex]; - let x = element.x + deltaX; - let y = element.y + deltaY; - let pointX = px - deltaX; - let pointY = py - deltaY; - - if (sidesWithSameLength) { - const { width, height } = getPerfectElementSize( - element.type, - px + element.x - pointerX, - py + element.y - pointerY, - ); - x = px + element.x - width; - y = py + element.y - height; - pointX = width; - pointY = height; - } - - mutateElement(element, { - x, - y, - points: element.points.map((point, i) => - i === pointIndex ? ([pointX, pointY] as const) : point, - ), - }); -}; - -const arrowResizeEnd: ResizeArrowFnType = ( - element, - pointIndex, - deltaX, - deltaY, - pointerX, - pointerY, - sidesWithSameLength, -) => { - const [px, py] = element.points[pointIndex]; - if (sidesWithSameLength) { - const { width, height } = getPerfectElementSize( - element.type, - pointerX - element.x, - pointerY - element.y, - ); - mutateElement(element, { - points: element.points.map((point, i) => - i === pointIndex ? ([width, height] as const) : point, - ), - }); - } else { - mutateElement(element, { - points: element.points.map((point, i) => - i === pointIndex ? ([px + deltaX, py + deltaY] as const) : point, - ), - }); - } -}; - -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; + const pointOrigin = element.points[0]; // can assume always [0, 0]? + const pointEnd = element.points[1]; + if (resizeArrowDirection === "end") { + if (isAngleLocking) { + const { width, height } = getPerfectElementSize( + element.type, + pointerX - element.x, + pointerY - element.y, + ); + mutateElement(element, { + points: [pointOrigin, [width, height]], + }); } else { - resizeArrowFn = arrowResizeOrigin; + mutateElement(element, { + points: [ + pointOrigin, + [ + pointerX - pointOrigin[0] - element.x, + pointerY - pointOrigin[1] - element.y, + ], + ], + }); + } + } else { + // resizeArrowDirection === "origin" + if (isAngleLocking) { + const { width, height } = getPerfectElementSize( + element.type, + element.x + pointEnd[0] - pointOrigin[0] - pointerX, + element.y + pointEnd[1] - pointOrigin[1] - pointerY, + ); + mutateElement(element, { + x: element.x + pointEnd[0] - pointOrigin[0] - width, + y: element.y + pointEnd[1] - pointOrigin[1] - height, + points: [pointOrigin, [width, height]], + }); + } else { + mutateElement(element, { + x: pointerX, + y: pointerY, + points: [ + pointOrigin, + [ + pointEnd[0] - (pointerX - pointOrigin[0] - element.x), + pointEnd[1] - (pointerY - pointOrigin[1] - element.y), + ], + ], + }); } } - resizeArrowFn(element, 1, deltaX, deltaY, x, y, sidesWithSameLength); - setResizeArrowFn(resizeArrowFn); }; const resizeSingleElement = ( @@ -260,16 +180,14 @@ const resizeSingleElement = ( isResizeFromCenter: boolean, pointerX: number, pointerY: number, - offsetX: number, - offsetY: 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 - offsetX, - pointerY - offsetY, + pointerX, + pointerY, cx, cy, -element.angle, @@ -366,15 +284,13 @@ const resizeMultipleElements = ( resizeHandle: "nw" | "ne" | "sw" | "se", pointerX: number, pointerY: number, - offsetX: number, - offsetY: number, ) => { const [x1, y1, x2, y2] = getCommonBounds(elements); switch (resizeHandle) { case "se": { const scale = Math.max( - (pointerX - offsetX - x1) / (x2 - x1), - (pointerY - offsetY - y1) / (y2 - y1), + (pointerX - x1) / (x2 - x1), + (pointerY - y1) / (y2 - y1), ); if (scale > 0) { elements.forEach((element) => { @@ -389,8 +305,8 @@ const resizeMultipleElements = ( } case "nw": { const scale = Math.max( - (x2 - (pointerX - offsetX)) / (x2 - x1), - (y2 - (pointerY - offsetY)) / (y2 - y1), + (x2 - pointerX) / (x2 - x1), + (y2 - pointerY) / (y2 - y1), ); if (scale > 0) { elements.forEach((element) => { @@ -405,8 +321,8 @@ const resizeMultipleElements = ( } case "ne": { const scale = Math.max( - (pointerX - offsetX - x1) / (x2 - x1), - (y2 - (pointerY - offsetY)) / (y2 - y1), + (pointerX - x1) / (x2 - x1), + (y2 - pointerY) / (y2 - y1), ); if (scale > 0) { elements.forEach((element) => { @@ -421,8 +337,8 @@ const resizeMultipleElements = ( } case "sw": { const scale = Math.max( - (x2 - (pointerX - offsetX)) / (x2 - x1), - (pointerY - offsetY - y1) / (y2 - y1), + (x2 - pointerX) / (x2 - x1), + (pointerY - y1) / (y2 - y1), ); if (scale > 0) { elements.forEach((element) => { @@ -481,3 +397,16 @@ export const getResizeOffsetXY = ( return [0, 0]; } }; + +export const getResizeArrowDirection = ( + resizeHandle: ResizeTestType, + element: NonDeleted, +): "origin" | "end" => { + 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)); + return isResizeEnd ? "end" : "origin"; +}; diff --git a/src/element/types.ts b/src/element/types.ts index 1b1add6c..b21c16c8 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -58,13 +58,3 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase & export type PointerType = "mouse" | "pen" | "touch"; export type TextAlign = "left" | "center" | "right"; - -export type ResizeArrowFnType = ( - element: NonDeleted, - pointIndex: number, - deltaX: number, - deltaY: number, - pointerX: number, - pointerY: number, - perfect: boolean, -) => void;