123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886 |
- import { BOUND_TEXT_PADDING, SHIFT_LOCKING_ANGLE } from "../constants";
- import { rescalePoints } from "../points";
- import {
- rotate,
- adjustXYWithRotation,
- centerPoint,
- rotatePoint,
- } from "../math";
- import {
- ExcalidrawLinearElement,
- ExcalidrawTextElement,
- NonDeletedExcalidrawElement,
- NonDeleted,
- ExcalidrawElement,
- } from "./types";
- import {
- getElementAbsoluteCoords,
- getCommonBounds,
- getResizedElementAbsoluteCoords,
- getCommonBoundingBox,
- } from "./bounds";
- import {
- isFreeDrawElement,
- isLinearElement,
- isTextElement,
- } from "./typeChecks";
- import { mutateElement } from "./mutateElement";
- import { getPerfectElementSize } from "./sizeHelpers";
- import { getFontString } from "../utils";
- import { updateBoundElements } from "./binding";
- import {
- TransformHandleType,
- MaybeTransformHandleType,
- TransformHandleDirection,
- } from "./transformHandles";
- import { Point, PointerDownState } from "../types";
- import Scene from "../scene/Scene";
- import {
- getApproxMinLineHeight,
- getApproxMinLineWidth,
- getBoundTextElement,
- getBoundTextElementId,
- handleBindTextResize,
- measureText,
- } from "./textElement";
- export const normalizeAngle = (angle: number): number => {
- if (angle >= 2 * Math.PI) {
- return angle - 2 * Math.PI;
- }
- return angle;
- };
- // Returns true when transform (resizing/rotation) happened
- export const transformElements = (
- pointerDownState: PointerDownState,
- transformHandleType: MaybeTransformHandleType,
- selectedElements: readonly NonDeletedExcalidrawElement[],
- resizeArrowDirection: "origin" | "end",
- shouldRotateWithDiscreteAngle: boolean,
- shouldResizeFromCenter: boolean,
- shouldMaintainAspectRatio: boolean,
- pointerX: number,
- pointerY: number,
- centerX: number,
- centerY: number,
- ) => {
- if (selectedElements.length === 1) {
- const [element] = selectedElements;
- if (transformHandleType === "rotation") {
- rotateSingleElement(
- element,
- pointerX,
- pointerY,
- shouldRotateWithDiscreteAngle,
- );
- updateBoundElements(element);
- } else if (
- isLinearElement(element) &&
- element.points.length === 2 &&
- (transformHandleType === "nw" ||
- transformHandleType === "ne" ||
- transformHandleType === "sw" ||
- transformHandleType === "se")
- ) {
- reshapeSingleTwoPointElement(
- element,
- resizeArrowDirection,
- shouldRotateWithDiscreteAngle,
- pointerX,
- pointerY,
- );
- } else if (
- isTextElement(element) &&
- (transformHandleType === "nw" ||
- transformHandleType === "ne" ||
- transformHandleType === "sw" ||
- transformHandleType === "se")
- ) {
- resizeSingleTextElement(
- element,
- transformHandleType,
- shouldResizeFromCenter,
- pointerX,
- pointerY,
- );
- updateBoundElements(element);
- } else if (transformHandleType) {
- resizeSingleElement(
- pointerDownState.originalElements,
- shouldMaintainAspectRatio,
- element,
- transformHandleType,
- shouldResizeFromCenter,
- pointerX,
- pointerY,
- );
- }
- return true;
- } else if (selectedElements.length > 1) {
- if (transformHandleType === "rotation") {
- rotateMultipleElements(
- pointerDownState,
- selectedElements,
- pointerX,
- pointerY,
- shouldRotateWithDiscreteAngle,
- centerX,
- centerY,
- );
- return true;
- } else if (
- transformHandleType === "nw" ||
- transformHandleType === "ne" ||
- transformHandleType === "sw" ||
- transformHandleType === "se"
- ) {
- resizeMultipleElements(
- pointerDownState,
- selectedElements,
- transformHandleType,
- shouldResizeFromCenter,
- pointerX,
- pointerY,
- );
- return true;
- }
- }
- return false;
- };
- const rotateSingleElement = (
- element: NonDeletedExcalidrawElement,
- pointerX: number,
- pointerY: number,
- shouldRotateWithDiscreteAngle: 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(pointerY - cy, pointerX - cx);
- if (shouldRotateWithDiscreteAngle) {
- angle += SHIFT_LOCKING_ANGLE / 2;
- angle -= angle % SHIFT_LOCKING_ANGLE;
- }
- angle = normalizeAngle(angle);
- mutateElement(element, { angle });
- const boundTextElementId = getBoundTextElementId(element);
- if (boundTextElementId) {
- const textElement = Scene.getScene(element)!.getElement(boundTextElementId);
- mutateElement(textElement!, { angle });
- }
- };
- // used in DEV only
- const validateTwoPointElementNormalized = (
- element: NonDeleted<ExcalidrawLinearElement>,
- ) => {
- if (
- element.points.length !== 2 ||
- element.points[0][0] !== 0 ||
- element.points[0][1] !== 0 ||
- Math.abs(element.points[1][0]) !== element.width ||
- Math.abs(element.points[1][1]) !== element.height
- ) {
- throw new Error("Two-point element is not normalized");
- }
- };
- const getPerfectElementSizeWithRotation = (
- elementType: ExcalidrawElement["type"],
- width: number,
- height: number,
- angle: number,
- ): [number, number] => {
- const size = getPerfectElementSize(
- elementType,
- ...rotate(width, height, 0, 0, angle),
- );
- return rotate(size.width, size.height, 0, 0, -angle);
- };
- export const reshapeSingleTwoPointElement = (
- element: NonDeleted<ExcalidrawLinearElement>,
- resizeArrowDirection: "origin" | "end",
- shouldRotateWithDiscreteAngle: boolean,
- pointerX: number,
- pointerY: number,
- ) => {
- if (process.env.NODE_ENV !== "production") {
- validateTwoPointElementNormalized(element);
- }
- 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 [width, height] =
- resizeArrowDirection === "end"
- ? [rotatedX - element.x, rotatedY - element.y]
- : [
- element.x + element.points[1][0] - rotatedX,
- element.y + element.points[1][1] - rotatedY,
- ];
- if (shouldRotateWithDiscreteAngle) {
- [width, height] = getPerfectElementSizeWithRotation(
- element.type,
- width,
- height,
- element.angle,
- );
- }
- const [nextElementX, nextElementY] = adjustXYWithRotation(
- resizeArrowDirection === "end"
- ? { s: true, e: true }
- : { n: true, w: true },
- element.x,
- element.y,
- element.angle,
- 0,
- 0,
- (element.points[1][0] - width) / 2,
- (element.points[1][1] - height) / 2,
- );
- mutateElement(element, {
- x: nextElementX,
- y: nextElementY,
- points: [
- [0, 0],
- [width, height],
- ],
- });
- };
- const rescalePointsInElement = (
- element: NonDeletedExcalidrawElement,
- width: number,
- height: number,
- normalizePoints: boolean,
- ) =>
- isLinearElement(element) || isFreeDrawElement(element)
- ? {
- points: rescalePoints(
- 0,
- width,
- rescalePoints(1, height, element.points, normalizePoints),
- normalizePoints,
- ),
- }
- : {};
- const MIN_FONT_SIZE = 1;
- const measureFontSizeFromWH = (
- element: NonDeleted<ExcalidrawTextElement>,
- nextWidth: number,
- nextHeight: number,
- ): { size: number; baseline: number } | null => {
- // We only use width to scale font on resize
- const nextFontSize = element.fontSize * (nextWidth / element.width);
- if (nextFontSize < MIN_FONT_SIZE) {
- return null;
- }
- const metrics = measureText(
- element.text,
- getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
- element.containerId ? element.width : null,
- );
- return {
- size: nextFontSize,
- baseline: metrics.baseline + (nextHeight - metrics.height),
- };
- };
- const getSidesForTransformHandle = (
- transformHandleType: TransformHandleType,
- shouldResizeFromCenter: boolean,
- ) => {
- return {
- n:
- /^(n|ne|nw)$/.test(transformHandleType) ||
- (shouldResizeFromCenter && /^(s|se|sw)$/.test(transformHandleType)),
- s:
- /^(s|se|sw)$/.test(transformHandleType) ||
- (shouldResizeFromCenter && /^(n|ne|nw)$/.test(transformHandleType)),
- w:
- /^(w|nw|sw)$/.test(transformHandleType) ||
- (shouldResizeFromCenter && /^(e|ne|se)$/.test(transformHandleType)),
- e:
- /^(e|ne|se)$/.test(transformHandleType) ||
- (shouldResizeFromCenter && /^(w|nw|sw)$/.test(transformHandleType)),
- };
- };
- const resizeSingleTextElement = (
- element: NonDeleted<ExcalidrawTextElement>,
- transformHandleType: "nw" | "ne" | "sw" | "se",
- shouldResizeFromCenter: 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 scale: number;
- switch (transformHandleType) {
- case "se":
- scale = Math.max(
- (rotatedX - x1) / (x2 - x1),
- (rotatedY - y1) / (y2 - y1),
- );
- break;
- case "nw":
- scale = Math.max(
- (x2 - rotatedX) / (x2 - x1),
- (y2 - rotatedY) / (y2 - y1),
- );
- break;
- case "ne":
- scale = Math.max(
- (rotatedX - x1) / (x2 - x1),
- (y2 - rotatedY) / (y2 - y1),
- );
- break;
- case "sw":
- scale = Math.max(
- (x2 - rotatedX) / (x2 - x1),
- (rotatedY - y1) / (y2 - y1),
- );
- break;
- }
- if (scale > 0) {
- const nextWidth = element.width * scale;
- const nextHeight = element.height * scale;
- const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
- if (nextFont === null) {
- return;
- }
- const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
- element,
- nextWidth,
- nextHeight,
- false,
- );
- const deltaX1 = (x1 - nextX1) / 2;
- const deltaY1 = (y1 - nextY1) / 2;
- const deltaX2 = (x2 - nextX2) / 2;
- const deltaY2 = (y2 - nextY2) / 2;
- const [nextElementX, nextElementY] = adjustXYWithRotation(
- getSidesForTransformHandle(transformHandleType, shouldResizeFromCenter),
- element.x,
- element.y,
- element.angle,
- deltaX1,
- deltaY1,
- deltaX2,
- deltaY2,
- );
- mutateElement(element, {
- fontSize: nextFont.size,
- width: nextWidth,
- height: nextHeight,
- baseline: nextFont.baseline,
- x: nextElementX,
- y: nextElementY,
- });
- }
- };
- export const resizeSingleElement = (
- originalElements: PointerDownState["originalElements"],
- shouldMaintainAspectRatio: boolean,
- element: NonDeletedExcalidrawElement,
- transformHandleDirection: TransformHandleDirection,
- shouldResizeFromCenter: boolean,
- pointerX: number,
- pointerY: number,
- ) => {
- const stateAtResizeStart = originalElements.get(element.id)!;
- // Gets bounds corners
- const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
- stateAtResizeStart,
- stateAtResizeStart.width,
- stateAtResizeStart.height,
- true,
- );
- const startTopLeft: Point = [x1, y1];
- const startBottomRight: Point = [x2, y2];
- const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
- // Calculate new dimensions based on cursor position
- 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,
- true,
- );
- 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;
- let boundTextFont: { fontSize?: number; baseline?: number } = {};
- const boundTextElement = getBoundTextElement(element);
- if (transformHandleDirection.includes("e")) {
- scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
- }
- if (transformHandleDirection.includes("s")) {
- scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight;
- }
- if (transformHandleDirection.includes("w")) {
- scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
- }
- if (transformHandleDirection.includes("n")) {
- 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 (shouldResizeFromCenter) {
- eleNewWidth = 2 * eleNewWidth - eleInitialWidth;
- eleNewHeight = 2 * eleNewHeight - eleInitialHeight;
- }
- // adjust dimensions to keep sides ratio
- if (shouldMaintainAspectRatio) {
- const widthRatio = Math.abs(eleNewWidth) / eleInitialWidth;
- const heightRatio = Math.abs(eleNewHeight) / eleInitialHeight;
- if (transformHandleDirection.length === 1) {
- eleNewHeight *= widthRatio;
- eleNewWidth *= heightRatio;
- }
- if (transformHandleDirection.length === 2) {
- const ratio = Math.max(widthRatio, heightRatio);
- eleNewWidth = eleInitialWidth * ratio * Math.sign(eleNewWidth);
- eleNewHeight = eleInitialHeight * ratio * Math.sign(eleNewHeight);
- }
- }
- if (boundTextElement) {
- const stateOfBoundTextElementAtResize = originalElements.get(
- boundTextElement.id,
- ) as typeof boundTextElement | undefined;
- if (stateOfBoundTextElementAtResize) {
- boundTextFont = {
- fontSize: stateOfBoundTextElementAtResize.fontSize,
- baseline: stateOfBoundTextElementAtResize.baseline,
- };
- }
- if (shouldMaintainAspectRatio) {
- const nextFont = measureFontSizeFromWH(
- boundTextElement,
- eleNewWidth - BOUND_TEXT_PADDING * 2,
- eleNewHeight - BOUND_TEXT_PADDING * 2,
- );
- if (nextFont === null) {
- return;
- }
- boundTextFont = {
- fontSize: nextFont.size,
- baseline: nextFont.baseline,
- };
- } else {
- const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
- const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
- eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
- eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
- }
- }
- const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
- getResizedElementAbsoluteCoords(
- stateAtResizeStart,
- eleNewWidth,
- eleNewHeight,
- true,
- );
- const newBoundsWidth = newBoundsX2 - newBoundsX1;
- const newBoundsHeight = newBoundsY2 - newBoundsY1;
- // Calculate new topLeft based on fixed corner during resize
- let newTopLeft = [...startTopLeft] as [number, number];
- if (["n", "w", "nw"].includes(transformHandleDirection)) {
- newTopLeft = [
- startBottomRight[0] - Math.abs(newBoundsWidth),
- startBottomRight[1] - Math.abs(newBoundsHeight),
- ];
- }
- if (transformHandleDirection === "ne") {
- const bottomLeft = [startTopLeft[0], startBottomRight[1]];
- newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
- }
- if (transformHandleDirection === "sw") {
- const topRight = [startBottomRight[0], startTopLeft[1]];
- newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
- }
- // Keeps opposite handle fixed during resize
- if (shouldMaintainAspectRatio) {
- if (["s", "n"].includes(transformHandleDirection)) {
- newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
- }
- if (["e", "w"].includes(transformHandleDirection)) {
- newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
- }
- }
- // Flip horizontally
- if (eleNewWidth < 0) {
- if (transformHandleDirection.includes("e")) {
- newTopLeft[0] -= Math.abs(newBoundsWidth);
- }
- if (transformHandleDirection.includes("w")) {
- newTopLeft[0] += Math.abs(newBoundsWidth);
- }
- }
- // Flip vertically
- if (eleNewHeight < 0) {
- if (transformHandleDirection.includes("s")) {
- newTopLeft[1] -= Math.abs(newBoundsHeight);
- }
- if (transformHandleDirection.includes("n")) {
- newTopLeft[1] += Math.abs(newBoundsHeight);
- }
- }
- if (shouldResizeFromCenter) {
- 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(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,
- true,
- );
- // 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(eleNewWidth),
- height: Math.abs(eleNewHeight),
- x: newOrigin[0],
- y: newOrigin[1],
- ...rescaledPoints,
- };
- if ("scale" in element && "scale" in stateAtResizeStart) {
- mutateElement(element, {
- scale: [
- // defaulting because scaleX/Y can be 0/-0
- (Math.sign(scaleX) || stateAtResizeStart.scale[0]) *
- stateAtResizeStart.scale[0],
- (Math.sign(scaleY) || stateAtResizeStart.scale[1]) *
- stateAtResizeStart.scale[1],
- ],
- });
- }
- if (
- resizedElement.width !== 0 &&
- resizedElement.height !== 0 &&
- Number.isFinite(resizedElement.x) &&
- Number.isFinite(resizedElement.y)
- ) {
- updateBoundElements(element, {
- newSize: { width: resizedElement.width, height: resizedElement.height },
- });
- mutateElement(element, resizedElement);
- if (boundTextElement && boundTextFont) {
- mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
- }
- handleBindTextResize(element, transformHandleDirection);
- }
- };
- const resizeMultipleElements = (
- pointerDownState: PointerDownState,
- selectedElements: readonly NonDeletedExcalidrawElement[],
- transformHandleType: "nw" | "ne" | "sw" | "se",
- shouldResizeFromCenter: boolean,
- pointerX: number,
- pointerY: number,
- ) => {
- // map selected elements to the original elements. While it never should
- // happen that pointerDownState.originalElements won't contain the selected
- // elements during resize, this coupling isn't guaranteed, so to ensure
- // type safety we need to transform only those elements we filter.
- const targetElements = selectedElements.reduce(
- (
- acc: {
- /** element at resize start */
- orig: NonDeletedExcalidrawElement;
- /** latest element */
- latest: NonDeletedExcalidrawElement;
- }[],
- element,
- ) => {
- const origElement = pointerDownState.originalElements.get(element.id);
- if (origElement) {
- acc.push({ orig: origElement, latest: element });
- }
- return acc;
- },
- [],
- );
- const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
- targetElements.map(({ orig }) => orig),
- );
- const direction = transformHandleType;
- const mapDirectionsToAnchors: Record<typeof direction, Point> = {
- ne: [minX, maxY],
- se: [minX, minY],
- sw: [maxX, minY],
- nw: [maxX, maxY],
- };
- // anchor point must be on the opposite side of the dragged selection handle
- // or be the center of the selection if alt is pressed
- const [anchorX, anchorY]: Point = shouldResizeFromCenter
- ? [midX, midY]
- : mapDirectionsToAnchors[direction];
- const mapDirectionsToPointerSides: Record<
- typeof direction,
- [x: boolean, y: boolean]
- > = {
- ne: [pointerX >= anchorX, pointerY <= anchorY],
- se: [pointerX >= anchorX, pointerY >= anchorY],
- sw: [pointerX <= anchorX, pointerY >= anchorY],
- nw: [pointerX <= anchorX, pointerY <= anchorY],
- };
- // pointer side relative to anchor
- const [pointerSideX, pointerSideY] = mapDirectionsToPointerSides[
- direction
- ].map((condition) => (condition ? 1 : -1));
- // stop resizing if a pointer is on the other side of selection
- if (pointerSideX < 0 && pointerSideY < 0) {
- return;
- }
- const scale =
- Math.max(
- (pointerSideX * Math.abs(pointerX - anchorX)) / (maxX - minX),
- (pointerSideY * Math.abs(pointerY - anchorY)) / (maxY - minY),
- ) * (shouldResizeFromCenter ? 2 : 1);
- if (scale === 1) {
- return;
- }
- targetElements.forEach((element) => {
- const width = element.orig.width * scale;
- const height = element.orig.height * scale;
- const x = anchorX + (element.orig.x - anchorX) * scale;
- const y = anchorY + (element.orig.y - anchorY) * scale;
- // readjust points for linear & free draw elements
- const rescaledPoints = rescalePointsInElement(
- element.orig,
- width,
- height,
- false,
- );
- const update: {
- width: number;
- height: number;
- x: number;
- y: number;
- points?: Point[];
- fontSize?: number;
- baseline?: number;
- } = {
- width,
- height,
- x,
- y,
- ...rescaledPoints,
- };
- let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
- const boundTextElement = getBoundTextElement(element.latest);
- if (boundTextElement || isTextElement(element.orig)) {
- const optionalPadding = boundTextElement ? BOUND_TEXT_PADDING * 2 : 0;
- const textMeasurements = measureFontSizeFromWH(
- boundTextElement ?? (element.orig as ExcalidrawTextElement),
- width - optionalPadding,
- height - optionalPadding,
- );
- if (textMeasurements) {
- if (isTextElement(element.orig)) {
- update.fontSize = textMeasurements.size;
- update.baseline = textMeasurements.baseline;
- }
- if (boundTextElement) {
- boundTextUpdates = {
- fontSize: textMeasurements.size,
- baseline: textMeasurements.baseline,
- };
- }
- }
- }
- mutateElement(element.latest, update);
- if (boundTextElement && boundTextUpdates) {
- mutateElement(boundTextElement, boundTextUpdates);
- handleBindTextResize(element.latest, transformHandleType);
- }
- });
- };
- const rotateMultipleElements = (
- pointerDownState: PointerDownState,
- elements: readonly NonDeletedExcalidrawElement[],
- pointerX: number,
- pointerY: number,
- shouldRotateWithDiscreteAngle: boolean,
- centerX: number,
- centerY: number,
- ) => {
- let centerAngle =
- (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
- if (shouldRotateWithDiscreteAngle) {
- centerAngle += SHIFT_LOCKING_ANGLE / 2;
- centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
- }
- elements.forEach((element, index) => {
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
- const cx = (x1 + x2) / 2;
- const cy = (y1 + y2) / 2;
- const origAngle =
- pointerDownState.originalElements.get(element.id)?.angle ?? element.angle;
- const [rotatedCX, rotatedCY] = rotate(
- cx,
- cy,
- centerX,
- centerY,
- centerAngle + origAngle - element.angle,
- );
- mutateElement(element, {
- x: element.x + (rotatedCX - cx),
- y: element.y + (rotatedCY - cy),
- angle: normalizeAngle(centerAngle + origAngle),
- });
- const boundTextElementId = getBoundTextElementId(element);
- if (boundTextElementId) {
- const textElement =
- Scene.getScene(element)!.getElement(boundTextElementId)!;
- mutateElement(textElement, {
- x: textElement.x + (rotatedCX - cx),
- y: textElement.y + (rotatedCY - cy),
- angle: normalizeAngle(centerAngle + origAngle),
- });
- }
- });
- };
- export const getResizeOffsetXY = (
- transformHandleType: MaybeTransformHandleType,
- selectedElements: NonDeletedExcalidrawElement[],
- x: number,
- y: number,
- ): [number, number] => {
- const [x1, y1, x2, y2] =
- selectedElements.length === 1
- ? getElementAbsoluteCoords(selectedElements[0])
- : getCommonBounds(selectedElements);
- const cx = (x1 + x2) / 2;
- const cy = (y1 + y2) / 2;
- const angle = selectedElements.length === 1 ? selectedElements[0].angle : 0;
- [x, y] = rotate(x, y, cx, cy, -angle);
- switch (transformHandleType) {
- case "n":
- return rotate(x - (x1 + x2) / 2, y - y1, 0, 0, angle);
- case "s":
- return rotate(x - (x1 + x2) / 2, y - y2, 0, 0, angle);
- case "w":
- return rotate(x - x1, y - (y1 + y2) / 2, 0, 0, angle);
- case "e":
- return rotate(x - x2, y - (y1 + y2) / 2, 0, 0, angle);
- case "nw":
- return rotate(x - x1, y - y1, 0, 0, angle);
- case "ne":
- return rotate(x - x2, y - y1, 0, 0, angle);
- case "sw":
- return rotate(x - x1, y - y2, 0, 0, angle);
- case "se":
- return rotate(x - x2, y - y2, 0, 0, angle);
- default:
- return [0, 0];
- }
- };
- export const getResizeArrowDirection = (
- transformHandleType: MaybeTransformHandleType,
- element: NonDeleted<ExcalidrawLinearElement>,
- ): "origin" | "end" => {
- const [, [px, py]] = element.points;
- const isResizeEnd =
- (transformHandleType === "nw" && (px < 0 || py < 0)) ||
- (transformHandleType === "ne" && px >= 0) ||
- (transformHandleType === "sw" && px <= 0) ||
- (transformHandleType === "se" && (px > 0 || py > 0));
- return isResizeEnd ? "end" : "origin";
- };
|