2020-04-07 17:49:59 +09:00
|
|
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
|
|
|
import { rescalePoints } from "../points";
|
2020-04-10 01:10:35 +09:00
|
|
|
|
2020-12-06 22:39:31 +00:00
|
|
|
import {
|
|
|
|
rotate,
|
|
|
|
adjustXYWithRotation,
|
|
|
|
centerPoint,
|
|
|
|
rotatePoint,
|
|
|
|
} from "../math";
|
2020-04-08 09:49:52 -07:00
|
|
|
import {
|
|
|
|
ExcalidrawLinearElement,
|
2020-05-28 07:17:15 +09:00
|
|
|
ExcalidrawTextElement,
|
2020-04-08 09:49:52 -07:00
|
|
|
NonDeletedExcalidrawElement,
|
|
|
|
NonDeleted,
|
|
|
|
} from "./types";
|
2020-05-05 00:25:40 +09:00
|
|
|
import {
|
|
|
|
getElementAbsoluteCoords,
|
|
|
|
getCommonBounds,
|
|
|
|
getResizedElementAbsoluteCoords,
|
|
|
|
} from "./bounds";
|
2021-05-09 16:42:10 +01:00
|
|
|
import {
|
|
|
|
isFreeDrawElement,
|
|
|
|
isLinearElement,
|
|
|
|
isTextElement,
|
|
|
|
} from "./typeChecks";
|
2020-04-07 17:49:59 +09:00
|
|
|
import { mutateElement } from "./mutateElement";
|
2020-05-05 00:25:40 +09:00
|
|
|
import { getPerfectElementSize } from "./sizeHelpers";
|
2020-05-28 07:17:15 +09:00
|
|
|
import { measureText, getFontString } from "../utils";
|
2020-08-08 21:04:15 -07:00
|
|
|
import { updateBoundElements } from "./binding";
|
2020-08-10 14:16:39 +02:00
|
|
|
import {
|
|
|
|
TransformHandleType,
|
|
|
|
MaybeTransformHandleType,
|
2020-12-06 22:39:31 +00:00
|
|
|
TransformHandleDirection,
|
2020-08-10 14:16:39 +02:00
|
|
|
} from "./transformHandles";
|
2021-06-01 23:52:13 +05:30
|
|
|
import { Point, PointerDownState } from "../types";
|
2020-04-07 17:49:59 +09:00
|
|
|
|
2021-03-26 11:45:08 -04:00
|
|
|
export const normalizeAngle = (angle: number): number => {
|
2020-07-26 19:21:38 +09:00
|
|
|
if (angle >= 2 * Math.PI) {
|
|
|
|
return angle - 2 * Math.PI;
|
|
|
|
}
|
|
|
|
return angle;
|
|
|
|
};
|
|
|
|
|
2020-08-28 17:20:06 +09:00
|
|
|
// Returns true when transform (resizing/rotation) happened
|
|
|
|
export const transformElements = (
|
2020-09-11 17:22:40 +02:00
|
|
|
pointerDownState: PointerDownState,
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType: MaybeTransformHandleType,
|
2020-07-26 19:21:38 +09:00
|
|
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
2020-05-11 00:41:36 +09:00
|
|
|
resizeArrowDirection: "origin" | "end",
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldRotateWithDiscreteAngle: boolean,
|
|
|
|
shouldResizeFromCenter: boolean,
|
|
|
|
shouldMaintainAspectRatio: boolean,
|
2020-05-09 17:57:00 +09:00
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
2020-07-26 19:21:38 +09:00
|
|
|
centerX: number,
|
|
|
|
centerY: number,
|
2020-05-05 00:25:40 +09:00
|
|
|
) => {
|
|
|
|
if (selectedElements.length === 1) {
|
|
|
|
const [element] = selectedElements;
|
2020-08-10 14:16:39 +02:00
|
|
|
if (transformHandleType === "rotation") {
|
2020-06-24 00:24:52 +09:00
|
|
|
rotateSingleElement(
|
|
|
|
element,
|
|
|
|
pointerX,
|
|
|
|
pointerY,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldRotateWithDiscreteAngle,
|
2020-06-24 00:24:52 +09:00
|
|
|
);
|
2020-08-08 21:04:15 -07:00
|
|
|
updateBoundElements(element);
|
2020-05-05 00:25:40 +09:00
|
|
|
} else if (
|
|
|
|
isLinearElement(element) &&
|
|
|
|
element.points.length === 2 &&
|
2020-08-10 14:16:39 +02:00
|
|
|
(transformHandleType === "nw" ||
|
|
|
|
transformHandleType === "ne" ||
|
|
|
|
transformHandleType === "sw" ||
|
|
|
|
transformHandleType === "se")
|
2020-05-05 00:25:40 +09:00
|
|
|
) {
|
2020-08-28 17:20:06 +09:00
|
|
|
reshapeSingleTwoPointElement(
|
2020-05-05 00:25:40 +09:00
|
|
|
element,
|
2020-05-11 00:41:36 +09:00
|
|
|
resizeArrowDirection,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldRotateWithDiscreteAngle,
|
2020-05-09 17:57:00 +09:00
|
|
|
pointerX,
|
|
|
|
pointerY,
|
2020-05-05 00:25:40 +09:00
|
|
|
);
|
2020-05-28 07:17:15 +09:00
|
|
|
} else if (
|
2020-12-06 22:39:31 +00:00
|
|
|
isTextElement(element) &&
|
2020-08-10 14:16:39 +02:00
|
|
|
(transformHandleType === "nw" ||
|
|
|
|
transformHandleType === "ne" ||
|
|
|
|
transformHandleType === "sw" ||
|
|
|
|
transformHandleType === "se")
|
2020-05-28 07:17:15 +09:00
|
|
|
) {
|
|
|
|
resizeSingleTextElement(
|
|
|
|
element,
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldResizeFromCenter,
|
2020-05-28 07:17:15 +09:00
|
|
|
pointerX,
|
|
|
|
pointerY,
|
|
|
|
);
|
2020-09-08 12:03:49 -04:00
|
|
|
updateBoundElements(element);
|
2020-08-10 14:16:39 +02:00
|
|
|
} else if (transformHandleType) {
|
2021-01-16 18:49:13 +00:00
|
|
|
resizeSingleElement(
|
|
|
|
pointerDownState.originalElements.get(element.id) as typeof element,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldMaintainAspectRatio,
|
2021-01-16 18:49:13 +00:00
|
|
|
element,
|
|
|
|
transformHandleType,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldResizeFromCenter,
|
2021-01-16 18:49:13 +00:00
|
|
|
pointerX,
|
|
|
|
pointerY,
|
|
|
|
);
|
2020-05-05 00:25:40 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2020-07-26 19:21:38 +09:00
|
|
|
} else if (selectedElements.length > 1) {
|
2020-08-10 14:16:39 +02:00
|
|
|
if (transformHandleType === "rotation") {
|
2020-07-26 19:21:38 +09:00
|
|
|
rotateMultipleElements(
|
2020-09-11 17:22:40 +02:00
|
|
|
pointerDownState,
|
2020-07-26 19:21:38 +09:00
|
|
|
selectedElements,
|
|
|
|
pointerX,
|
|
|
|
pointerY,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldRotateWithDiscreteAngle,
|
2020-07-26 19:21:38 +09:00
|
|
|
centerX,
|
|
|
|
centerY,
|
|
|
|
);
|
|
|
|
return true;
|
|
|
|
} else if (
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType === "nw" ||
|
|
|
|
transformHandleType === "ne" ||
|
|
|
|
transformHandleType === "sw" ||
|
|
|
|
transformHandleType === "se"
|
2020-07-26 19:21:38 +09:00
|
|
|
) {
|
|
|
|
resizeMultipleElements(
|
|
|
|
selectedElements,
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType,
|
2020-07-26 19:21:38 +09:00
|
|
|
pointerX,
|
|
|
|
pointerY,
|
|
|
|
);
|
|
|
|
return true;
|
|
|
|
}
|
2020-05-05 00:25:40 +09:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
const rotateSingleElement = (
|
|
|
|
element: NonDeletedExcalidrawElement,
|
2020-05-09 17:57:00 +09:00
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldRotateWithDiscreteAngle: boolean,
|
2020-05-05 00:25:40 +09:00
|
|
|
) => {
|
|
|
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
|
|
|
const cx = (x1 + x2) / 2;
|
|
|
|
const cy = (y1 + y2) / 2;
|
2020-05-09 17:57:00 +09:00
|
|
|
let angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldRotateWithDiscreteAngle) {
|
2020-05-05 00:25:40 +09:00
|
|
|
angle += SHIFT_LOCKING_ANGLE / 2;
|
|
|
|
angle -= angle % SHIFT_LOCKING_ANGLE;
|
|
|
|
}
|
2020-07-26 19:21:38 +09:00
|
|
|
angle = normalizeAngle(angle);
|
2020-05-05 00:25:40 +09:00
|
|
|
mutateElement(element, { angle });
|
|
|
|
};
|
|
|
|
|
2020-08-28 17:20:06 +09:00
|
|
|
// 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: string,
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2021-03-26 11:45:08 -04:00
|
|
|
export const reshapeSingleTwoPointElement = (
|
2020-05-05 00:25:40 +09:00
|
|
|
element: NonDeleted<ExcalidrawLinearElement>,
|
2020-05-11 00:41:36 +09:00
|
|
|
resizeArrowDirection: "origin" | "end",
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldRotateWithDiscreteAngle: boolean,
|
2020-05-09 17:57:00 +09:00
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
2020-05-05 00:25:40 +09:00
|
|
|
) => {
|
2020-08-28 17:20:06 +09:00
|
|
|
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,
|
|
|
|
];
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldRotateWithDiscreteAngle) {
|
2020-08-28 17:20:06 +09:00
|
|
|
[width, height] = getPerfectElementSizeWithRotation(
|
|
|
|
element.type,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
element.angle,
|
|
|
|
);
|
2020-04-09 16:14:32 +01:00
|
|
|
}
|
2020-08-28 17:20:06 +09:00
|
|
|
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],
|
|
|
|
],
|
|
|
|
});
|
2020-04-09 16:14:32 +01:00
|
|
|
};
|
|
|
|
|
2020-05-14 23:56:14 +09:00
|
|
|
const rescalePointsInElement = (
|
|
|
|
element: NonDeletedExcalidrawElement,
|
|
|
|
width: number,
|
|
|
|
height: number,
|
|
|
|
) =>
|
2021-05-09 16:42:10 +01:00
|
|
|
isLinearElement(element) || isFreeDrawElement(element)
|
2020-05-14 23:56:14 +09:00
|
|
|
? {
|
|
|
|
points: rescalePoints(
|
|
|
|
0,
|
|
|
|
width,
|
|
|
|
rescalePoints(1, height, element.points),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
: {};
|
|
|
|
|
2020-07-09 22:22:10 +09:00
|
|
|
const MIN_FONT_SIZE = 1;
|
|
|
|
|
2020-06-08 18:25:20 +09:00
|
|
|
const measureFontSizeFromWH = (
|
|
|
|
element: NonDeleted<ExcalidrawTextElement>,
|
|
|
|
nextWidth: number,
|
|
|
|
nextHeight: number,
|
|
|
|
): { size: number; baseline: number } | null => {
|
2020-07-09 22:22:10 +09:00
|
|
|
// We only use width to scale font on resize
|
|
|
|
const nextFontSize = element.fontSize * (nextWidth / element.width);
|
|
|
|
if (nextFontSize < MIN_FONT_SIZE) {
|
|
|
|
return null;
|
2020-06-08 18:25:20 +09:00
|
|
|
}
|
2020-07-09 22:22:10 +09:00
|
|
|
const metrics = measureText(
|
2020-06-24 00:24:52 +09:00
|
|
|
element.text,
|
|
|
|
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
|
|
|
|
);
|
2020-07-09 22:22:10 +09:00
|
|
|
return {
|
|
|
|
size: nextFontSize,
|
|
|
|
baseline: metrics.baseline + (nextHeight - metrics.height),
|
|
|
|
};
|
2020-06-08 18:25:20 +09:00
|
|
|
};
|
|
|
|
|
2020-08-10 14:16:39 +02:00
|
|
|
const getSidesForTransformHandle = (
|
|
|
|
transformHandleType: TransformHandleType,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldResizeFromCenter: boolean,
|
2020-06-25 21:21:27 +02:00
|
|
|
) => {
|
|
|
|
return {
|
|
|
|
n:
|
2020-08-10 14:16:39 +02:00
|
|
|
/^(n|ne|nw)$/.test(transformHandleType) ||
|
2021-10-21 22:05:48 +02:00
|
|
|
(shouldResizeFromCenter && /^(s|se|sw)$/.test(transformHandleType)),
|
2020-06-25 21:21:27 +02:00
|
|
|
s:
|
2020-08-10 14:16:39 +02:00
|
|
|
/^(s|se|sw)$/.test(transformHandleType) ||
|
2021-10-21 22:05:48 +02:00
|
|
|
(shouldResizeFromCenter && /^(n|ne|nw)$/.test(transformHandleType)),
|
2020-06-25 21:21:27 +02:00
|
|
|
w:
|
2020-08-10 14:16:39 +02:00
|
|
|
/^(w|nw|sw)$/.test(transformHandleType) ||
|
2021-10-21 22:05:48 +02:00
|
|
|
(shouldResizeFromCenter && /^(e|ne|se)$/.test(transformHandleType)),
|
2020-06-25 21:21:27 +02:00
|
|
|
e:
|
2020-08-10 14:16:39 +02:00
|
|
|
/^(e|ne|se)$/.test(transformHandleType) ||
|
2021-10-21 22:05:48 +02:00
|
|
|
(shouldResizeFromCenter && /^(w|nw|sw)$/.test(transformHandleType)),
|
2020-06-25 21:21:27 +02:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2020-05-28 07:17:15 +09:00
|
|
|
const resizeSingleTextElement = (
|
|
|
|
element: NonDeleted<ExcalidrawTextElement>,
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType: "nw" | "ne" | "sw" | "se",
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldResizeFromCenter: boolean,
|
2020-05-28 07:17:15 +09:00
|
|
|
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,
|
|
|
|
);
|
2020-11-06 22:06:30 +02:00
|
|
|
let scale: number;
|
2020-08-10 14:16:39 +02:00
|
|
|
switch (transformHandleType) {
|
2020-05-28 07:17:15 +09:00
|
|
|
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) {
|
2020-06-08 18:25:20 +09:00
|
|
|
const nextWidth = element.width * scale;
|
|
|
|
const nextHeight = element.height * scale;
|
|
|
|
const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
|
|
|
|
if (nextFont === null) {
|
2020-05-28 07:17:15 +09:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
|
|
|
element,
|
2020-06-08 18:25:20 +09:00
|
|
|
nextWidth,
|
|
|
|
nextHeight,
|
2020-05-28 07:17:15 +09:00
|
|
|
);
|
|
|
|
const deltaX1 = (x1 - nextX1) / 2;
|
|
|
|
const deltaY1 = (y1 - nextY1) / 2;
|
|
|
|
const deltaX2 = (x2 - nextX2) / 2;
|
|
|
|
const deltaY2 = (y2 - nextY2) / 2;
|
|
|
|
const [nextElementX, nextElementY] = adjustXYWithRotation(
|
2021-10-21 22:05:48 +02:00
|
|
|
getSidesForTransformHandle(transformHandleType, shouldResizeFromCenter),
|
2020-05-28 07:17:15 +09:00
|
|
|
element.x,
|
|
|
|
element.y,
|
|
|
|
element.angle,
|
|
|
|
deltaX1,
|
|
|
|
deltaY1,
|
|
|
|
deltaX2,
|
|
|
|
deltaY2,
|
|
|
|
);
|
|
|
|
mutateElement(element, {
|
2020-06-08 18:25:20 +09:00
|
|
|
fontSize: nextFont.size,
|
|
|
|
width: nextWidth,
|
|
|
|
height: nextHeight,
|
|
|
|
baseline: nextFont.baseline,
|
2020-05-28 07:17:15 +09:00
|
|
|
x: nextElementX,
|
|
|
|
y: nextElementY,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-03-26 11:45:08 -04:00
|
|
|
export const resizeSingleElement = (
|
2021-01-16 18:49:13 +00:00
|
|
|
stateAtResizeStart: NonDeletedExcalidrawElement,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldMaintainAspectRatio: boolean,
|
2020-05-05 00:25:40 +09:00
|
|
|
element: NonDeletedExcalidrawElement,
|
2020-12-06 22:39:31 +00:00
|
|
|
transformHandleDirection: TransformHandleDirection,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldResizeFromCenter: boolean,
|
2020-12-06 22:39:31 +00:00
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
|
|
|
) => {
|
2021-01-16 18:49:13 +00:00
|
|
|
// Gets bounds corners
|
|
|
|
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
|
|
|
|
stateAtResizeStart,
|
|
|
|
stateAtResizeStart.width,
|
|
|
|
stateAtResizeStart.height,
|
|
|
|
);
|
2020-12-06 22:39:31 +00:00
|
|
|
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,
|
|
|
|
);
|
2021-01-16 18:49:13 +00:00
|
|
|
|
2021-05-09 16:42:10 +01:00
|
|
|
// Get bounds corners rendered on screen
|
2021-01-16 18:49:13 +00:00
|
|
|
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;
|
|
|
|
|
2020-12-06 22:39:31 +00:00
|
|
|
if (transformHandleDirection.includes("e")) {
|
2021-01-16 18:49:13 +00:00
|
|
|
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
if (transformHandleDirection.includes("s")) {
|
2021-01-16 18:49:13 +00:00
|
|
|
scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
if (transformHandleDirection.includes("w")) {
|
2021-01-16 18:49:13 +00:00
|
|
|
scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
if (transformHandleDirection.includes("n")) {
|
2021-01-16 18:49:13 +00:00
|
|
|
scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
2021-01-16 18:49:13 +00:00
|
|
|
// 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;
|
2020-12-06 22:39:31 +00:00
|
|
|
|
|
|
|
// adjust dimensions for resizing from center
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldResizeFromCenter) {
|
2021-01-16 18:49:13 +00:00
|
|
|
eleNewWidth = 2 * eleNewWidth - eleInitialWidth;
|
|
|
|
eleNewHeight = 2 * eleNewHeight - eleInitialHeight;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// adjust dimensions to keep sides ratio
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldMaintainAspectRatio) {
|
2021-01-16 18:49:13 +00:00
|
|
|
const widthRatio = Math.abs(eleNewWidth) / eleInitialWidth;
|
|
|
|
const heightRatio = Math.abs(eleNewHeight) / eleInitialHeight;
|
2020-12-06 22:39:31 +00:00
|
|
|
if (transformHandleDirection.length === 1) {
|
2021-01-16 18:49:13 +00:00
|
|
|
eleNewHeight *= widthRatio;
|
|
|
|
eleNewWidth *= heightRatio;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
if (transformHandleDirection.length === 2) {
|
|
|
|
const ratio = Math.max(widthRatio, heightRatio);
|
2021-01-16 18:49:13 +00:00
|
|
|
eleNewWidth = eleInitialWidth * ratio * Math.sign(eleNewWidth);
|
|
|
|
eleNewHeight = eleInitialHeight * ratio * Math.sign(eleNewHeight);
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-01 15:24:05 +02:00
|
|
|
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
|
|
|
getResizedElementAbsoluteCoords(
|
|
|
|
stateAtResizeStart,
|
|
|
|
eleNewWidth,
|
|
|
|
eleNewHeight,
|
|
|
|
);
|
2021-01-16 18:49:13 +00:00
|
|
|
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
|
|
|
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
|
|
|
|
2020-12-06 22:39:31 +00:00
|
|
|
// Calculate new topLeft based on fixed corner during resize
|
2021-01-16 18:49:13 +00:00
|
|
|
let newTopLeft = [...startTopLeft] as [number, number];
|
2020-12-06 22:39:31 +00:00
|
|
|
if (["n", "w", "nw"].includes(transformHandleDirection)) {
|
|
|
|
newTopLeft = [
|
2021-01-16 18:49:13 +00:00
|
|
|
startBottomRight[0] - Math.abs(newBoundsWidth),
|
|
|
|
startBottomRight[1] - Math.abs(newBoundsHeight),
|
2020-12-06 22:39:31 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
if (transformHandleDirection === "ne") {
|
2021-01-16 18:49:13 +00:00
|
|
|
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
|
|
|
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
if (transformHandleDirection === "sw") {
|
2021-01-16 18:49:13 +00:00
|
|
|
const topRight = [startBottomRight[0], startTopLeft[1]];
|
|
|
|
newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Keeps opposite handle fixed during resize
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldMaintainAspectRatio) {
|
2020-12-06 22:39:31 +00:00
|
|
|
if (["s", "n"].includes(transformHandleDirection)) {
|
2021-01-16 18:49:13 +00:00
|
|
|
newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
if (["e", "w"].includes(transformHandleDirection)) {
|
2021-01-16 18:49:13 +00:00
|
|
|
newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Flip horizontally
|
2021-01-16 18:49:13 +00:00
|
|
|
if (eleNewWidth < 0) {
|
2020-12-06 22:39:31 +00:00
|
|
|
if (transformHandleDirection.includes("e")) {
|
2021-01-16 18:49:13 +00:00
|
|
|
newTopLeft[0] -= Math.abs(newBoundsWidth);
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
if (transformHandleDirection.includes("w")) {
|
2021-01-16 18:49:13 +00:00
|
|
|
newTopLeft[0] += Math.abs(newBoundsWidth);
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Flip vertically
|
2021-01-16 18:49:13 +00:00
|
|
|
if (eleNewHeight < 0) {
|
2020-12-06 22:39:31 +00:00
|
|
|
if (transformHandleDirection.includes("s")) {
|
2021-01-16 18:49:13 +00:00
|
|
|
newTopLeft[1] -= Math.abs(newBoundsHeight);
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
if (transformHandleDirection.includes("n")) {
|
2021-01-16 18:49:13 +00:00
|
|
|
newTopLeft[1] += Math.abs(newBoundsHeight);
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldResizeFromCenter) {
|
2021-01-16 18:49:13 +00:00
|
|
|
newTopLeft[0] = startCenter[0] - Math.abs(newBoundsWidth) / 2;
|
|
|
|
newTopLeft[1] = startCenter[1] - Math.abs(newBoundsHeight) / 2;
|
2020-12-06 22:39:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// adjust topLeft to new rotation point
|
|
|
|
const angle = stateAtResizeStart.angle;
|
|
|
|
const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
|
|
|
|
const newCenter: Point = [
|
2021-01-16 18:49:13 +00:00
|
|
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
|
|
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
2020-12-06 22:39:31 +00:00
|
|
|
];
|
|
|
|
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
|
|
|
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
|
|
|
|
2021-01-16 18:49:13 +00:00
|
|
|
// Readjust points for linear elements
|
|
|
|
const rescaledPoints = rescalePointsInElement(
|
|
|
|
stateAtResizeStart,
|
|
|
|
eleNewWidth,
|
|
|
|
eleNewHeight,
|
2020-05-05 00:25:40 +09:00
|
|
|
);
|
2021-01-16 18:49:13 +00:00
|
|
|
// 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;
|
2020-12-06 22:39:31 +00:00
|
|
|
|
2021-01-16 18:49:13 +00:00
|
|
|
const resizedElement = {
|
|
|
|
width: Math.abs(eleNewWidth),
|
|
|
|
height: Math.abs(eleNewHeight),
|
|
|
|
x: newOrigin[0],
|
|
|
|
y: newOrigin[1],
|
|
|
|
...rescaledPoints,
|
|
|
|
};
|
2020-12-06 22:39:31 +00:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
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],
|
|
|
|
],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-05-05 00:25:40 +09:00
|
|
|
if (
|
2021-01-16 18:49:13 +00:00
|
|
|
resizedElement.width !== 0 &&
|
|
|
|
resizedElement.height !== 0 &&
|
|
|
|
Number.isFinite(resizedElement.x) &&
|
|
|
|
Number.isFinite(resizedElement.y)
|
2020-05-05 00:25:40 +09:00
|
|
|
) {
|
2021-01-16 18:49:13 +00:00
|
|
|
updateBoundElements(element, {
|
|
|
|
newSize: { width: resizedElement.width, height: resizedElement.height },
|
2020-05-05 00:25:40 +09:00
|
|
|
});
|
2021-01-16 18:49:13 +00:00
|
|
|
mutateElement(element, resizedElement);
|
2020-05-05 00:25:40 +09:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const resizeMultipleElements = (
|
|
|
|
elements: readonly NonDeletedExcalidrawElement[],
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType: "nw" | "ne" | "sw" | "se",
|
2020-05-09 17:57:00 +09:00
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
2020-05-05 00:25:40 +09:00
|
|
|
) => {
|
|
|
|
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
2020-06-08 18:25:20 +09:00
|
|
|
let scale: number;
|
|
|
|
let getNextXY: (
|
|
|
|
element: NonDeletedExcalidrawElement,
|
|
|
|
origCoords: readonly [number, number, number, number],
|
|
|
|
finalCoords: readonly [number, number, number, number],
|
|
|
|
) => { x: number; y: number };
|
2020-08-10 14:16:39 +02:00
|
|
|
switch (transformHandleType) {
|
2020-06-08 18:25:20 +09:00
|
|
|
case "se":
|
|
|
|
scale = Math.max(
|
2020-05-11 00:41:36 +09:00
|
|
|
(pointerX - x1) / (x2 - x1),
|
|
|
|
(pointerY - y1) / (y2 - y1),
|
2020-04-09 16:14:32 +01:00
|
|
|
);
|
2020-06-08 18:25:20 +09:00
|
|
|
getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
|
|
|
|
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
|
|
|
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
|
|
|
return { x, y };
|
|
|
|
};
|
2020-05-05 00:25:40 +09:00
|
|
|
break;
|
2020-06-08 18:25:20 +09:00
|
|
|
case "nw":
|
|
|
|
scale = Math.max(
|
2020-05-11 00:41:36 +09:00
|
|
|
(x2 - pointerX) / (x2 - x1),
|
|
|
|
(y2 - pointerY) / (y2 - y1),
|
2020-05-05 00:25:40 +09:00
|
|
|
);
|
2020-06-08 18:25:20 +09:00
|
|
|
getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
|
|
|
|
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
|
|
|
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
|
|
|
return { x, y };
|
|
|
|
};
|
2020-05-05 00:25:40 +09:00
|
|
|
break;
|
2020-06-08 18:25:20 +09:00
|
|
|
case "ne":
|
|
|
|
scale = Math.max(
|
2020-05-11 00:41:36 +09:00
|
|
|
(pointerX - x1) / (x2 - x1),
|
|
|
|
(y2 - pointerY) / (y2 - y1),
|
2020-05-05 00:25:40 +09:00
|
|
|
);
|
2020-06-08 18:25:20 +09:00
|
|
|
getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
|
|
|
|
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
|
|
|
|
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
|
|
|
|
return { x, y };
|
|
|
|
};
|
2020-05-05 00:25:40 +09:00
|
|
|
break;
|
2020-06-08 18:25:20 +09:00
|
|
|
case "sw":
|
|
|
|
scale = Math.max(
|
2020-05-11 00:41:36 +09:00
|
|
|
(x2 - pointerX) / (x2 - x1),
|
|
|
|
(pointerY - y1) / (y2 - y1),
|
2020-05-05 00:25:40 +09:00
|
|
|
);
|
2020-06-08 18:25:20 +09:00
|
|
|
getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
|
|
|
|
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
|
|
|
|
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
|
|
|
|
return { x, y };
|
|
|
|
};
|
2020-05-05 00:25:40 +09:00
|
|
|
break;
|
2020-06-08 18:25:20 +09:00
|
|
|
}
|
|
|
|
if (scale > 0) {
|
|
|
|
const updates = elements.reduce(
|
|
|
|
(prev, element) => {
|
|
|
|
if (!prev) {
|
|
|
|
return prev;
|
|
|
|
}
|
|
|
|
const width = element.width * scale;
|
|
|
|
const height = element.height * scale;
|
|
|
|
let font: { fontSize?: number; baseline?: number } = {};
|
|
|
|
if (element.type === "text") {
|
|
|
|
const nextFont = measureFontSizeFromWH(element, width, height);
|
|
|
|
if (nextFont === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
font = { fontSize: nextFont.size, baseline: nextFont.baseline };
|
|
|
|
}
|
|
|
|
const origCoords = getElementAbsoluteCoords(element);
|
2021-05-09 16:42:10 +01:00
|
|
|
|
2020-06-08 18:25:20 +09:00
|
|
|
const rescaledPoints = rescalePointsInElement(element, width, height);
|
2021-05-09 16:42:10 +01:00
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
updateBoundElements(element, {
|
|
|
|
newSize: { width, height },
|
|
|
|
simultaneouslyUpdated: elements,
|
|
|
|
});
|
2021-05-09 16:42:10 +01:00
|
|
|
|
2020-06-08 18:25:20 +09:00
|
|
|
const finalCoords = getResizedElementAbsoluteCoords(
|
|
|
|
{
|
|
|
|
...element,
|
|
|
|
...rescaledPoints,
|
|
|
|
},
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
);
|
2021-05-09 16:42:10 +01:00
|
|
|
|
2020-06-08 18:25:20 +09:00
|
|
|
const { x, y } = getNextXY(element, origCoords, finalCoords);
|
|
|
|
return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
|
|
|
|
},
|
|
|
|
[] as
|
|
|
|
| {
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
points?: (readonly [number, number])[];
|
|
|
|
fontSize?: number;
|
|
|
|
baseline?: number;
|
|
|
|
}[]
|
|
|
|
| null,
|
|
|
|
);
|
|
|
|
if (updates) {
|
|
|
|
elements.forEach((element, index) => {
|
|
|
|
mutateElement(element, updates[index]);
|
|
|
|
});
|
2020-04-07 17:49:59 +09:00
|
|
|
}
|
|
|
|
}
|
2020-04-09 16:14:32 +01:00
|
|
|
};
|
2020-04-07 17:49:59 +09:00
|
|
|
|
2020-07-26 19:21:38 +09:00
|
|
|
const rotateMultipleElements = (
|
2020-09-11 17:22:40 +02:00
|
|
|
pointerDownState: PointerDownState,
|
2020-07-26 19:21:38 +09:00
|
|
|
elements: readonly NonDeletedExcalidrawElement[],
|
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldRotateWithDiscreteAngle: boolean,
|
2020-07-26 19:21:38 +09:00
|
|
|
centerX: number,
|
|
|
|
centerY: number,
|
|
|
|
) => {
|
|
|
|
let centerAngle =
|
|
|
|
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldRotateWithDiscreteAngle) {
|
2020-07-26 19:21:38 +09:00
|
|
|
centerAngle += SHIFT_LOCKING_ANGLE / 2;
|
|
|
|
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
|
|
|
}
|
|
|
|
elements.forEach((element, index) => {
|
2020-08-28 17:20:06 +09:00
|
|
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
|
|
|
const cx = (x1 + x2) / 2;
|
|
|
|
const cy = (y1 + y2) / 2;
|
2020-09-11 17:22:40 +02:00
|
|
|
const origAngle =
|
|
|
|
pointerDownState.originalElements.get(element.id)?.angle ?? element.angle;
|
2020-08-28 17:20:06 +09:00
|
|
|
const [rotatedCX, rotatedCY] = rotate(
|
|
|
|
cx,
|
|
|
|
cy,
|
|
|
|
centerX,
|
|
|
|
centerY,
|
2020-09-11 17:22:40 +02:00
|
|
|
centerAngle + origAngle - element.angle,
|
2020-08-28 17:20:06 +09:00
|
|
|
);
|
|
|
|
mutateElement(element, {
|
|
|
|
x: element.x + (rotatedCX - cx),
|
|
|
|
y: element.y + (rotatedCY - cy),
|
2020-09-11 17:22:40 +02:00
|
|
|
angle: normalizeAngle(centerAngle + origAngle),
|
2020-08-28 17:20:06 +09:00
|
|
|
});
|
2020-07-26 19:21:38 +09:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-05-09 17:57:00 +09:00
|
|
|
export const getResizeOffsetXY = (
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType: MaybeTransformHandleType,
|
2020-05-09 17:57:00 +09:00
|
|
|
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);
|
2020-08-10 14:16:39 +02:00
|
|
|
switch (transformHandleType) {
|
2020-05-09 17:57:00 +09:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
};
|
2020-05-11 00:41:36 +09:00
|
|
|
|
|
|
|
export const getResizeArrowDirection = (
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType: MaybeTransformHandleType,
|
2020-05-11 00:41:36 +09:00
|
|
|
element: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
): "origin" | "end" => {
|
|
|
|
const [, [px, py]] = element.points;
|
|
|
|
const isResizeEnd =
|
2020-08-10 14:16:39 +02:00
|
|
|
(transformHandleType === "nw" && (px < 0 || py < 0)) ||
|
|
|
|
(transformHandleType === "ne" && px >= 0) ||
|
|
|
|
(transformHandleType === "sw" && px <= 0) ||
|
|
|
|
(transformHandleType === "se" && (px > 0 || py > 0));
|
2020-05-11 00:41:36 +09:00
|
|
|
return isResizeEnd ? "end" : "origin";
|
|
|
|
};
|