diff --git a/src/element/bounds.ts b/src/element/bounds.ts index 6311779a..e5a413b0 100644 --- a/src/element/bounds.ts +++ b/src/element/bounds.ts @@ -1,9 +1,11 @@ import { ExcalidrawElement, ExcalidrawLinearElement } from "./types"; import { rotate } from "../math"; -import { Drawable, Op } from "roughjs/bin/core"; +import rough from "roughjs/bin/rough"; +import { Drawable, Op, Options } from "roughjs/bin/core"; import { Point } from "../types"; import { getShapeForElement } from "../renderer/renderElement"; import { isLinearElement } from "./typeChecks"; +import { rescalePoints } from "../points"; // If the element is created from right to left, the width is going to be negative // This set of functions retrieves the absolute position of the 4 points. @@ -11,7 +13,7 @@ export function getElementAbsoluteCoords( element: ExcalidrawElement, ): [number, number, number, number] { if (isLinearElement(element)) { - return getLinearElementAbsoluteBounds(element); + return getLinearElementAbsoluteCoords(element); } return [ element.x, @@ -45,35 +47,10 @@ export function getCurvePathOps(shape: Drawable): Op[] { return shape.sets[0].ops; } -export function getLinearElementAbsoluteBounds( - element: ExcalidrawLinearElement, -): [number, number, number, number] { - if (element.points.length < 2 || !getShapeForElement(element)) { - const { minX, minY, maxX, maxY } = element.points.reduce( - (limits, [x, y]) => { - limits.minY = Math.min(limits.minY, y); - limits.minX = Math.min(limits.minX, x); - - limits.maxX = Math.max(limits.maxX, x); - limits.maxY = Math.max(limits.maxY, y); - - return limits; - }, - { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - ); - return [ - minX + element.x, - minY + element.y, - maxX + element.x, - maxY + element.y, - ]; - } - - const shape = getShapeForElement(element) as Drawable[]; - - // first element is always the curve - const ops = getCurvePathOps(shape[0]); - +const getMinMaxXYFromCurvePathOps = ( + ops: Op[], + transformXY?: (x: number, y: number) => [number, number], +): [number, number, number, number] => { let currentP: Point = [0, 0]; const { minX, minY, maxX, maxY } = ops.reduce( @@ -104,8 +81,11 @@ export function getLinearElementAbsoluteBounds( let t = 0; while (t <= 1.0) { - const x = equation(t, 0); - const y = equation(t, 1); + let x = equation(t, 0); + let y = equation(t, 1); + if (transformXY) { + [x, y] = transformXY(x, y); + } limits.minY = Math.min(limits.minY, y); limits.minX = Math.min(limits.minX, x); @@ -125,13 +105,47 @@ export function getLinearElementAbsoluteBounds( { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, ); + return [minX, minY, maxX, maxY]; +}; + +const getLinearElementAbsoluteCoords = ( + element: ExcalidrawLinearElement, +): [number, number, number, number] => { + if (element.points.length < 2 || !getShapeForElement(element)) { + const { minX, minY, maxX, maxY } = element.points.reduce( + (limits, [x, y]) => { + limits.minY = Math.min(limits.minY, y); + limits.minX = Math.min(limits.minX, x); + + limits.maxX = Math.max(limits.maxX, x); + limits.maxY = Math.max(limits.maxY, y); + + return limits; + }, + { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, + ); + return [ + minX + element.x, + minY + element.y, + maxX + element.x, + maxY + element.y, + ]; + } + + const shape = getShapeForElement(element) as Drawable[]; + + // first element is always the curve + const ops = getCurvePathOps(shape[0]); + + const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops); + return [ minX + element.x, minY + element.y, maxX + element.x, maxY + element.y, ]; -} +}; export function getArrowPoints( element: ExcalidrawLinearElement, @@ -197,8 +211,6 @@ export function getArrowPoints( return [x2, y2, x3, y3, x4, y4]; } -// this function has some code in common with getLinearElementAbsoluteBounds -// there might be more efficient way const getLinearElementRotatedBounds = ( element: ExcalidrawLinearElement, cx: number, @@ -224,56 +236,9 @@ const getLinearElementRotatedBounds = ( // first element is always the curve const ops = getCurvePathOps(shape[0]); - let currentP: Point = [0, 0]; - - const { minX, minY, maxX, maxY } = ops.reduce( - (limits, { op, data }) => { - // There are only four operation types: - // move, bcurveTo, lineTo, and curveTo - if (op === "move") { - // change starting point - currentP = (data as unknown) as Point; - // move operation does not draw anything; so, it always - // returns false - } else if (op === "bcurveTo") { - // create points from bezier curve - // bezier curve stores data as a flattened array of three positions - // [x1, y1, x2, y2, x3, y3] - const p1 = [data[0], data[1]] as Point; - const p2 = [data[2], data[3]] as Point; - const p3 = [data[4], data[5]] as Point; - - const p0 = currentP; - currentP = p3; - - const equation = (t: number, idx: number) => - Math.pow(1 - t, 3) * p3[idx] + - 3 * t * Math.pow(1 - t, 2) * p2[idx] + - 3 * Math.pow(t, 2) * (1 - t) * p1[idx] + - p0[idx] * Math.pow(t, 3); - - let t = 0; - while (t <= 1.0) { - let x = equation(t, 0); - let y = equation(t, 1); - [x, y] = rotate(element.x + x, element.y + y, cx, cy, element.angle); - limits.minY = Math.min(limits.minY, y); - limits.minX = Math.min(limits.minX, x); - limits.maxX = Math.max(limits.maxX, x); - limits.maxY = Math.max(limits.maxY, y); - t += 0.1; - } - } else if (op === "lineTo") { - // TODO: Implement this - } else if (op === "qcurveTo") { - // TODO: Implement this - } - return limits; - }, - { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - ); - - return [minX, minY, maxX, maxY]; + const transformXY = (x: number, y: number) => + rotate(element.x + x, element.y + y, cx, cy, element.angle); + return getMinMaxXYFromCurvePathOps(ops, transformXY); }; export const getElementBounds = ( @@ -338,3 +303,40 @@ export const getCommonBounds = ( return [minX, minY, maxX, maxY]; }; + +export const getResizedElementAbsoluteCoords = ( + element: ExcalidrawElement, + nextWidth: number, + nextHeight: number, +): [number, number, number, number] => { + if (!isLinearElement(element) || element.points.length <= 2) { + return [ + element.x, + element.y, + element.x + nextWidth, + element.y + nextHeight, + ]; + } + + const points = rescalePoints( + 0, + nextWidth, + rescalePoints(1, nextHeight, element.points), + ); + + const options: Options = { + strokeWidth: element.strokeWidth, + roughness: element.roughness, + seed: element.seed, + }; + const gen = rough.generator(); + const curve = gen.curve(points as [number, number][], options); + const ops = getCurvePathOps(curve); + const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops); + return [ + minX + element.x, + minY + element.y, + maxX + element.x, + maxY + element.y, + ]; +}; diff --git a/src/element/index.ts b/src/element/index.ts index 95b55f0c..b6d4c88c 100644 --- a/src/element/index.ts +++ b/src/element/index.ts @@ -17,7 +17,6 @@ export { getCommonBounds, getDiamondPoints, getArrowPoints, - getLinearElementAbsoluteBounds, } from "./bounds"; export { diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index e05e463e..60ad8a15 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -3,17 +3,21 @@ import { SHIFT_LOCKING_ANGLE } from "../constants"; import { getSelectedElements, globalSceneState } from "../scene"; import { rescalePoints } from "../points"; -import { rotate, resizeXYWidthHightWithRotation } from "../math"; +import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math"; import { ExcalidrawLinearElement, NonDeletedExcalidrawElement, NonDeleted, ResizeArrowFnType, } from "./types"; -import { getElementAbsoluteCoords, getCommonBounds } from "./bounds"; +import { + getElementAbsoluteCoords, + getCommonBounds, + getResizedElementAbsoluteCoords, +} from "./bounds"; import { isLinearElement } from "./typeChecks"; import { mutateElement } from "./mutateElement"; -import { getPerfectElementSize, normalizeDimensions } from "./sizeHelpers"; +import { getPerfectElementSize } from "./sizeHelpers"; import { resizeTest, getCursorForResizingElement, @@ -26,6 +30,155 @@ import { type ResizeTestType = ReturnType; +export const resizeElements = ( + resizeHandle: ResizeTestType, + setResizeHandle: (nextResizeHandle: ResizeTestType) => void, + appState: AppState, + setAppState: (obj: any) => void, + resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339 + setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339 + event: PointerEvent, // XXX we want to make it independent? + xPointer: number, + yPointer: number, + lastX: number, // XXX eliminate in #1339 + lastY: number, // XXX eliminate in #1339 +) => { + setAppState({ + isResizing: resizeHandle && resizeHandle !== "rotation", + isRotating: resizeHandle === "rotation", + }); + const selectedElements = getSelectedElements( + globalSceneState.getElements(), + appState, + ); + const handleOffset = 4 / appState.zoom; // XXX import constant + const dashedLinePadding = 4 / appState.zoom; // XXX import constant + const offsetPointer = handleOffset + dashedLinePadding; + if (selectedElements.length === 1) { + const [element] = selectedElements; + if (resizeHandle === "rotation") { + rotateSingleElement(element, xPointer, yPointer, event.shiftKey); + } else if ( + isLinearElement(element) && + element.points.length === 2 && + (resizeHandle === "nw" || + resizeHandle === "ne" || + resizeHandle === "sw" || + resizeHandle === "se") + ) { + resizeSingleTwoPointElement( + element, + resizeHandle, + resizeArrowFn, + setResizeArrowFn, + event.shiftKey, + xPointer, + yPointer, + lastX, + lastY, + ); + } else if (resizeHandle) { + resizeSingleElement( + element, + resizeHandle, + getResizeWithSidesSameLengthKey(event), + getResizeCenterPointKey(event), + xPointer, + yPointer, + offsetPointer, + ); + setResizeHandle(normalizeResizeHandle(element, resizeHandle)); + if (element.width < 0) { + mutateElement(element, { width: -element.width }); + } + if (element.height < 0) { + mutateElement(element, { height: -element.height }); + } + } + + // XXX do we need this? + document.documentElement.style.cursor = getCursorForResizingElement({ + element, + resizeHandle, + }); + // XXX why do we need this? + if (appState.resizingElement) { + mutateElement(appState.resizingElement, { + x: element.x, + y: element.y, + }); + } + + return true; + } else if ( + selectedElements.length > 1 && + (resizeHandle === "nw" || + resizeHandle === "ne" || + resizeHandle === "sw" || + resizeHandle === "se") + ) { + resizeMultipleElements( + selectedElements, + resizeHandle, + xPointer, + yPointer, + offsetPointer, + ); + return true; + } + return false; +}; + +const rotateSingleElement = ( + element: NonDeletedExcalidrawElement, + xPointer: number, + yPointer: number, + isAngleLocking: boolean, +) => { + 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 (isAngleLocking) { + angle += SHIFT_LOCKING_ANGLE / 2; + angle -= angle % SHIFT_LOCKING_ANGLE; + } + if (angle >= 2 * Math.PI) { + angle -= 2 * Math.PI; + } + mutateElement(element, { angle }); +}; + +const resizeSingleTwoPointElement = ( + element: NonDeleted, + resizeHandle: "nw" | "ne" | "sw" | "se", + resizeArrowFn: ResizeArrowFnType | null, + setResizeArrowFn: (fn: ResizeArrowFnType) => void, + sidesWithSameLength: boolean, + xPointer: number, + yPointer: 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, + xPointer, + yPointer, + lastX, + lastY, + ); +}; + const arrowResizeOrigin: ResizeArrowFnType = ( element, pointIndex, @@ -116,199 +269,196 @@ const applyResizeArrowFn = ( setResizeArrowFn(resizeArrowFn); }; -export const resizeElements = ( - resizeHandle: ResizeTestType, - setResizeHandle: (nextResizeHandle: ResizeTestType) => void, - appState: AppState, - setAppState: (obj: any) => void, - resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339 - setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339 - event: PointerEvent, // XXX we want to make it independent? +const resizeSingleElement = ( + element: NonDeletedExcalidrawElement, + resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se", + sidesWithSameLength: boolean, + isResizeFromCenter: boolean, xPointer: number, yPointer: number, - lastX: number, // XXX eliminate in #1339 - lastY: number, // XXX eliminate in #1339 + offsetPointer: number, ) => { - setAppState({ - isResizing: resizeHandle !== "rotation", - isRotating: resizeHandle === "rotation", - }); - const selectedElements = getSelectedElements( - globalSceneState.getElements(), - appState, + 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( + xPointer, + yPointer, + cx, + cy, + -element.angle, ); - const handleOffset = 4 / appState.zoom; // XXX import constant - const dashedLinePadding = 4 / appState.zoom; // XXX import constant - const offsetPointer = handleOffset + dashedLinePadding; - const minSize = 0; - if (selectedElements.length === 1) { - 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; + // XXX this might be slow with closure + const adjustWithOffsetPointer = (wh: number) => { + if (wh > offsetPointer) { + return wh - offsetPointer; + } else if (wh < -offsetPointer) { + return wh + offsetPointer; + } + return 0; + }; + let scaleX = 1; + let scaleY = 1; + if (resizeHandle === "e" || resizeHandle === "ne" || resizeHandle === "se") { + scaleX = adjustWithOffsetPointer(rotatedX - x1) / (x2 - x1); + } + if (resizeHandle === "s" || resizeHandle === "sw" || resizeHandle === "se") { + scaleY = adjustWithOffsetPointer(rotatedY - y1) / (y2 - y1); + } + if (resizeHandle === "w" || resizeHandle === "nw" || resizeHandle === "sw") { + scaleX = adjustWithOffsetPointer(x2 - rotatedX) / (x2 - x1); + } + if (resizeHandle === "n" || resizeHandle === "nw" || resizeHandle === "ne") { + scaleY = adjustWithOffsetPointer(y2 - rotatedY) / (y2 - y1); + } + let nextWidth = element.width * scaleX; + let nextHeight = element.height * scaleY; + if (sidesWithSameLength) { + 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 = isLinearElement(element) + ? { + points: rescalePoints( + 0, + nextWidth, + rescalePoints(1, nextHeight, element.points), + ), } - if (angle >= 2 * Math.PI) { - angle -= 2 * Math.PI; - } - 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, + : {}; + const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords( + { + ...element, + ...rescaledPoints, + }, + Math.abs(nextWidth), + Math.abs(nextHeight), + ); + const [flipDiffX, flipDiffY] = getFlipAdjustment( + resizeHandle, + nextWidth, + nextHeight, + nextX1, + nextY1, + nextX2, + nextY2, + finalX1, + finalY1, + finalX2, + finalY2, + isLinearElement(element), + element.angle, + ); + const [nextElementX, nextElementY] = adjustXYWithRotation( + resizeHandle, + element.x - flipDiffX, + element.y - flipDiffY, + element.angle, + deltaX1, + deltaY1, + deltaX2, + deltaY2, + isResizeFromCenter, + ); + if ( + nextWidth !== 0 && + nextHeight !== 0 && + Number.isFinite(nextElementX) && + Number.isFinite(nextElementY) + ) { + mutateElement(element, { + width: nextWidth, + height: nextHeight, + x: nextElementX, + y: nextElementY, + ...rescaledPoints, + }); + } +}; + +const resizeMultipleElements = ( + elements: readonly NonDeletedExcalidrawElement[], + resizeHandle: "nw" | "ne" | "sw" | "se", + xPointer: number, + yPointer: number, + offsetPointer: number, +) => { + const [x1, y1, x2, y2] = getCommonBounds(elements); + switch (resizeHandle) { + case "se": { + const scale = Math.max( + (xPointer - offsetPointer - x1) / (x2 - x1), + (yPointer - offsetPointer - y1) / (y2 - y1), ); - } else if (resizeHandle) { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); - const resized = resizeXYWidthHightWithRotation( - resizeHandle, - x1, - y1, - x2, - y2, - element.width, - element.height, - element.x, - element.y, - element.angle, - xPointer, - yPointer, - offsetPointer, - getResizeWithSidesSameLengthKey(event), - getResizeCenterPointKey(event), - ); - if ( - Math.abs(resized.width) > minSize && - Math.abs(resized.height) > minSize - ) { - mutateElement(element, { - ...resized, - ...(isLinearElement(element) - ? { - points: rescalePoints( - 0, - resized.width, - rescalePoints(1, resized.height, element.points), - ), - } - : {}), + if (scale > 0) { + elements.forEach((element) => { + const width = element.width * scale; + const height = element.height * scale; + const x = element.x + (element.x - x1) * (scale - 1); + const y = element.y + (element.y - y1) * (scale - 1); + mutateElement(element, { width, height, x, y }); }); } + break; } - - if (resizeHandle) { - setResizeHandle(normalizeResizeHandle(element, resizeHandle)); + case "nw": { + const scale = Math.max( + (x2 - offsetPointer - xPointer) / (x2 - x1), + (y2 - offsetPointer - yPointer) / (y2 - y1), + ); + if (scale > 0) { + elements.forEach((element) => { + const width = element.width * scale; + const height = element.height * scale; + const x = element.x - (x2 - element.x) * (scale - 1); + const y = element.y - (y2 - element.y) * (scale - 1); + mutateElement(element, { width, height, x, y }); + }); + } + break; } - normalizeDimensions(element); - - // do we need this? - document.documentElement.style.cursor = getCursorForResizingElement({ - element, - resizeHandle, - }); - // why do we need this? - if (appState.resizingElement) { - mutateElement(appState.resizingElement, { - x: element.x, - y: element.y, - }); + case "ne": { + const scale = Math.max( + (xPointer - offsetPointer - x1) / (x2 - x1), + (y2 - offsetPointer - yPointer) / (y2 - y1), + ); + if (scale > 0) { + elements.forEach((element) => { + const width = element.width * scale; + const height = element.height * scale; + const x = element.x + (element.x - x1) * (scale - 1); + const y = element.y - (y2 - element.y) * (scale - 1); + mutateElement(element, { width, height, x, y }); + }); + } + break; } - - return true; - } else if (selectedElements.length > 1) { - const [x1, y1, x2, y2] = getCommonBounds(selectedElements); - const minScale = Math.max(minSize / (x2 - x1), minSize / (y2 - y1)); - switch (resizeHandle) { - case "se": { - const scale = Math.max( - (xPointer - offsetPointer - x1) / (x2 - x1), - (yPointer - offsetPointer - y1) / (y2 - y1), - ); - if (scale > minScale) { - selectedElements.forEach((element) => { - const width = element.width * scale; - const height = element.height * scale; - const x = element.x + (element.x - x1) * (scale - 1); - const y = element.y + (element.y - y1) * (scale - 1); - mutateElement(element, { width, height, x, y }); - }); - } - return true; - } - case "nw": { - const scale = Math.max( - (x2 - offsetPointer - xPointer) / (x2 - x1), - (y2 - offsetPointer - yPointer) / (y2 - y1), - ); - if (scale > minScale) { - selectedElements.forEach((element) => { - const width = element.width * scale; - const height = element.height * scale; - const x = element.x - (x2 - element.x) * (scale - 1); - const y = element.y - (y2 - element.y) * (scale - 1); - mutateElement(element, { width, height, x, y }); - }); - } - return true; - } - case "ne": { - const scale = Math.max( - (xPointer - offsetPointer - x1) / (x2 - x1), - (y2 - offsetPointer - yPointer) / (y2 - y1), - ); - if (scale > minScale) { - selectedElements.forEach((element) => { - const width = element.width * scale; - const height = element.height * scale; - const x = element.x + (element.x - x1) * (scale - 1); - const y = element.y - (y2 - element.y) * (scale - 1); - mutateElement(element, { width, height, x, y }); - }); - } - return true; - } - case "sw": { - const scale = Math.max( - (x2 - offsetPointer - xPointer) / (x2 - x1), - (yPointer - offsetPointer - y1) / (y2 - y1), - ); - if (scale > minScale) { - selectedElements.forEach((element) => { - const width = element.width * scale; - const height = element.height * scale; - const x = element.x - (x2 - element.x) * (scale - 1); - const y = element.y + (element.y - y1) * (scale - 1); - mutateElement(element, { width, height, x, y }); - }); - } - return true; + case "sw": { + const scale = Math.max( + (x2 - offsetPointer - xPointer) / (x2 - x1), + (yPointer - offsetPointer - y1) / (y2 - y1), + ); + if (scale > 0) { + elements.forEach((element) => { + const width = element.width * scale; + const height = element.height * scale; + const x = element.x - (x2 - element.x) * (scale - 1); + const y = element.y + (element.y - y1) * (scale - 1); + mutateElement(element, { width, height, x, y }); + }); } + break; } } - return false; }; export const canResizeMutlipleElements = ( diff --git a/src/math.ts b/src/math.ts index f5a6e1e3..08da7405 100644 --- a/src/math.ts +++ b/src/math.ts @@ -56,123 +56,118 @@ export function rotate( ]; } -const adjustXYWithRotation = ( +export const adjustXYWithRotation = ( side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se", x: number, y: number, angle: number, - deltaX: number, - deltaY: number, + deltaX1: number, + deltaY1: number, + deltaX2: number, + deltaY2: number, isResizeFromCenter: boolean, -) => { +): [number, number] => { const cos = Math.cos(angle); const sin = Math.sin(angle); if (side === "e" || side === "ne" || side === "se") { if (isResizeFromCenter) { - x += deltaX; + x += deltaX1 + deltaX2; } else { - x += deltaX * (1 - cos); - y += deltaX * -sin; + x += deltaX1 * (1 + cos); + y += deltaX1 * sin; + x += deltaX2 * (1 - cos); + y += deltaX2 * -sin; } } if (side === "s" || side === "sw" || side === "se") { if (isResizeFromCenter) { - y += deltaY; + y += deltaY1 + deltaY2; } else { - x += deltaY * sin; - y += deltaY * (1 - cos); + x += deltaY1 * -sin; + y += deltaY1 * (1 + cos); + x += deltaY2 * sin; + y += deltaY2 * (1 - cos); } } if (side === "w" || side === "nw" || side === "sw") { if (isResizeFromCenter) { - x += deltaX; + x += deltaX1 + deltaX2; } else { - x += deltaX * (1 + cos); - y += deltaX * sin; + x += deltaX1 * (1 - cos); + y += deltaX1 * -sin; + x += deltaX2 * (1 + cos); + y += deltaX2 * sin; } } if (side === "n" || side === "nw" || side === "ne") { if (isResizeFromCenter) { - y += deltaY; + y += deltaY1 + deltaY2; } else { - x += deltaY * -sin; - y += deltaY * (1 + cos); + x += deltaY1 * sin; + y += deltaY1 * (1 - cos); + x += deltaY2 * -sin; + y += deltaY2 * (1 + cos); } } - return { x, y }; + return [x, y]; }; -export const resizeXYWidthHightWithRotation = ( +export const getFlipAdjustment = ( side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se", - x1: number, - y1: number, - x2: number, - y2: number, - elementWidth: number, - elementHeight: number, - elementX: number, - elementY: number, + nextWidth: number, + nextHeight: number, + nextX1: number, + nextY1: number, + nextX2: number, + nextY2: number, + finalX1: number, + finalY1: number, + finalX2: number, + finalY2: number, + needsRotation: boolean, angle: number, - xPointer: number, - yPointer: number, - offsetPointer: number, - sidesWithSameLength: boolean, - isResizeFromCenter: boolean, -) => { - // center point for rotation - const cx = (x1 + x2) / 2; - const cy = (y1 + y2) / 2; - - // rotation with current angle - const [rotatedX, rotatedY] = rotate(xPointer, yPointer, cx, cy, -angle); - - // XXX this might be slow with closure - const adjustWithOffsetPointer = (w: number) => { - if (w > offsetPointer) { - return w - offsetPointer; - } else if (w < -offsetPointer) { - return w + offsetPointer; +): [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; + } } - return 0; - }; - - let scaleX = 1; - let scaleY = 1; - if (side === "e" || side === "ne" || side === "se") { - scaleX = adjustWithOffsetPointer(rotatedX - x1) / (x2 - x1); } - if (side === "s" || side === "sw" || side === "se") { - scaleY = adjustWithOffsetPointer(rotatedY - y1) / (y2 - y1); + 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; + } + } } - if (side === "w" || side === "nw" || side === "sw") { - scaleX = adjustWithOffsetPointer(x2 - rotatedX) / (x2 - x1); - } - if (side === "n" || side === "nw" || side === "ne") { - scaleY = adjustWithOffsetPointer(y2 - rotatedY) / (y2 - y1); - } - - let nextWidth = elementWidth * scaleX; - let nextHeight = elementHeight * scaleY; - if (sidesWithSameLength) { - nextWidth = nextHeight = Math.max(nextWidth, nextHeight); - } - - const deltaX = (elementWidth - nextWidth) / 2; - const deltaY = (elementHeight - nextHeight) / 2; - - return { - width: nextWidth, - height: nextHeight, - ...adjustXYWithRotation( - side, - elementX, - elementY, - angle, - deltaX, - deltaY, - isResizeFromCenter, - ), - }; + return [flipDiffX, flipDiffY]; }; export const getPointOnAPath = (point: Point, path: Point[]) => {