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-05-05 00:25:40 +09:00
|
|
|
import { rotate, adjustXYWithRotation, getFlipAdjustment } 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";
|
2020-04-07 17:49:59 +09:00
|
|
|
import { isLinearElement } from "./typeChecks";
|
|
|
|
import { mutateElement } from "./mutateElement";
|
2020-05-05 00:25:40 +09:00
|
|
|
import { getPerfectElementSize } from "./sizeHelpers";
|
2020-04-07 17:49:59 +09:00
|
|
|
import {
|
|
|
|
resizeTest,
|
|
|
|
getCursorForResizingElement,
|
|
|
|
normalizeResizeHandle,
|
|
|
|
} from "./resizeTest";
|
2020-05-28 07:17:15 +09:00
|
|
|
import { measureText, getFontString } from "../utils";
|
2020-04-07 17:49:59 +09:00
|
|
|
|
2020-07-26 19:21:38 +09:00
|
|
|
const normalizeAngle = (angle: number): number => {
|
|
|
|
if (angle >= 2 * Math.PI) {
|
|
|
|
return angle - 2 * Math.PI;
|
|
|
|
}
|
|
|
|
return angle;
|
|
|
|
};
|
|
|
|
|
2020-04-07 17:49:59 +09:00
|
|
|
type ResizeTestType = ReturnType<typeof resizeTest>;
|
|
|
|
|
2020-05-05 00:25:40 +09:00
|
|
|
export const resizeElements = (
|
|
|
|
resizeHandle: ResizeTestType,
|
|
|
|
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
|
2020-07-26 19:21:38 +09:00
|
|
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
2020-05-11 00:41:36 +09:00
|
|
|
resizeArrowDirection: "origin" | "end",
|
2020-06-24 00:24:52 +09:00
|
|
|
isRotateWithDiscreteAngle: boolean,
|
|
|
|
isResizeWithSidesSameLength: boolean,
|
|
|
|
isResizeCenterPoint: 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,
|
|
|
|
originalElements: readonly NonDeletedExcalidrawElement[],
|
2020-05-05 00:25:40 +09:00
|
|
|
) => {
|
|
|
|
if (selectedElements.length === 1) {
|
|
|
|
const [element] = selectedElements;
|
|
|
|
if (resizeHandle === "rotation") {
|
2020-06-24 00:24:52 +09:00
|
|
|
rotateSingleElement(
|
|
|
|
element,
|
|
|
|
pointerX,
|
|
|
|
pointerY,
|
|
|
|
isRotateWithDiscreteAngle,
|
|
|
|
);
|
2020-05-05 00:25:40 +09:00
|
|
|
} else if (
|
|
|
|
isLinearElement(element) &&
|
|
|
|
element.points.length === 2 &&
|
|
|
|
(resizeHandle === "nw" ||
|
|
|
|
resizeHandle === "ne" ||
|
|
|
|
resizeHandle === "sw" ||
|
|
|
|
resizeHandle === "se")
|
|
|
|
) {
|
|
|
|
resizeSingleTwoPointElement(
|
|
|
|
element,
|
2020-05-11 00:41:36 +09:00
|
|
|
resizeArrowDirection,
|
2020-06-24 00:24:52 +09:00
|
|
|
isRotateWithDiscreteAngle,
|
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 (
|
|
|
|
element.type === "text" &&
|
|
|
|
(resizeHandle === "nw" ||
|
|
|
|
resizeHandle === "ne" ||
|
|
|
|
resizeHandle === "sw" ||
|
|
|
|
resizeHandle === "se")
|
|
|
|
) {
|
|
|
|
resizeSingleTextElement(
|
|
|
|
element,
|
|
|
|
resizeHandle,
|
2020-06-24 00:24:52 +09:00
|
|
|
isResizeCenterPoint,
|
2020-05-28 07:17:15 +09:00
|
|
|
pointerX,
|
|
|
|
pointerY,
|
|
|
|
);
|
2020-05-05 00:25:40 +09:00
|
|
|
} else if (resizeHandle) {
|
|
|
|
resizeSingleElement(
|
|
|
|
element,
|
|
|
|
resizeHandle,
|
2020-06-24 00:24:52 +09:00
|
|
|
isResizeWithSidesSameLength,
|
|
|
|
isResizeCenterPoint,
|
2020-05-09 17:57:00 +09:00
|
|
|
pointerX,
|
|
|
|
pointerY,
|
2020-05-05 00:25:40 +09:00
|
|
|
);
|
|
|
|
setResizeHandle(normalizeResizeHandle(element, resizeHandle));
|
|
|
|
if (element.width < 0) {
|
|
|
|
mutateElement(element, { width: -element.width });
|
|
|
|
}
|
|
|
|
if (element.height < 0) {
|
|
|
|
mutateElement(element, { height: -element.height });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-09 17:57:00 +09:00
|
|
|
// update cursor
|
|
|
|
// FIXME it is not very nice to have this here
|
2020-05-05 00:25:40 +09:00
|
|
|
document.documentElement.style.cursor = getCursorForResizingElement({
|
|
|
|
element,
|
|
|
|
resizeHandle,
|
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
2020-07-26 19:21:38 +09:00
|
|
|
} else if (selectedElements.length > 1) {
|
|
|
|
if (resizeHandle === "rotation") {
|
|
|
|
rotateMultipleElements(
|
|
|
|
selectedElements,
|
|
|
|
pointerX,
|
|
|
|
pointerY,
|
|
|
|
isRotateWithDiscreteAngle,
|
|
|
|
centerX,
|
|
|
|
centerY,
|
|
|
|
originalElements,
|
|
|
|
);
|
|
|
|
return true;
|
|
|
|
} else if (
|
|
|
|
resizeHandle === "nw" ||
|
2020-05-05 00:25:40 +09:00
|
|
|
resizeHandle === "ne" ||
|
|
|
|
resizeHandle === "sw" ||
|
2020-07-26 19:21:38 +09:00
|
|
|
resizeHandle === "se"
|
|
|
|
) {
|
|
|
|
resizeMultipleElements(
|
|
|
|
selectedElements,
|
|
|
|
resizeHandle,
|
|
|
|
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,
|
2020-06-24 00:24:52 +09:00
|
|
|
isRotateWithDiscreteAngle: 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);
|
2020-06-24 00:24:52 +09:00
|
|
|
if (isRotateWithDiscreteAngle) {
|
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 });
|
|
|
|
};
|
|
|
|
|
|
|
|
const resizeSingleTwoPointElement = (
|
|
|
|
element: NonDeleted<ExcalidrawLinearElement>,
|
2020-05-11 00:41:36 +09:00
|
|
|
resizeArrowDirection: "origin" | "end",
|
2020-06-24 00:24:52 +09:00
|
|
|
isRotateWithDiscreteAngle: boolean,
|
2020-05-09 17:57:00 +09:00
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
2020-05-05 00:25:40 +09:00
|
|
|
) => {
|
2020-05-11 00:41:36 +09:00
|
|
|
const pointOrigin = element.points[0]; // can assume always [0, 0]?
|
|
|
|
const pointEnd = element.points[1];
|
|
|
|
if (resizeArrowDirection === "end") {
|
2020-06-24 00:24:52 +09:00
|
|
|
if (isRotateWithDiscreteAngle) {
|
2020-05-11 00:41:36 +09:00
|
|
|
const { width, height } = getPerfectElementSize(
|
|
|
|
element.type,
|
|
|
|
pointerX - element.x,
|
|
|
|
pointerY - element.y,
|
|
|
|
);
|
|
|
|
mutateElement(element, {
|
|
|
|
points: [pointOrigin, [width, height]],
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
mutateElement(element, {
|
|
|
|
points: [
|
|
|
|
pointOrigin,
|
|
|
|
[
|
|
|
|
pointerX - pointOrigin[0] - element.x,
|
|
|
|
pointerY - pointOrigin[1] - element.y,
|
|
|
|
],
|
|
|
|
],
|
|
|
|
});
|
|
|
|
}
|
2020-04-07 17:49:59 +09:00
|
|
|
} else {
|
2020-05-11 00:41:36 +09:00
|
|
|
// resizeArrowDirection === "origin"
|
2020-06-24 00:24:52 +09:00
|
|
|
if (isRotateWithDiscreteAngle) {
|
2020-05-11 00:41:36 +09:00
|
|
|
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]],
|
|
|
|
});
|
2020-04-09 16:14:32 +01:00
|
|
|
} else {
|
2020-05-11 00:41:36 +09:00
|
|
|
mutateElement(element, {
|
|
|
|
x: pointerX,
|
|
|
|
y: pointerY,
|
|
|
|
points: [
|
|
|
|
pointOrigin,
|
|
|
|
[
|
|
|
|
pointEnd[0] - (pointerX - pointOrigin[0] - element.x),
|
|
|
|
pointEnd[1] - (pointerY - pointOrigin[1] - element.y),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
});
|
2020-04-09 16:14:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-05-14 23:56:14 +09:00
|
|
|
const rescalePointsInElement = (
|
|
|
|
element: NonDeletedExcalidrawElement,
|
|
|
|
width: number,
|
|
|
|
height: number,
|
|
|
|
) =>
|
|
|
|
isLinearElement(element)
|
|
|
|
? {
|
|
|
|
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-06-25 21:21:27 +02:00
|
|
|
const getSidesForResizeHandle = (
|
|
|
|
resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
|
|
|
isResizeFromCenter: boolean,
|
|
|
|
) => {
|
|
|
|
return {
|
|
|
|
n:
|
|
|
|
/^(n|ne|nw)$/.test(resizeHandle) ||
|
|
|
|
(isResizeFromCenter && /^(s|se|sw)$/.test(resizeHandle)),
|
|
|
|
s:
|
|
|
|
/^(s|se|sw)$/.test(resizeHandle) ||
|
|
|
|
(isResizeFromCenter && /^(n|ne|nw)$/.test(resizeHandle)),
|
|
|
|
w:
|
|
|
|
/^(w|nw|sw)$/.test(resizeHandle) ||
|
|
|
|
(isResizeFromCenter && /^(e|ne|se)$/.test(resizeHandle)),
|
|
|
|
e:
|
|
|
|
/^(e|ne|se)$/.test(resizeHandle) ||
|
|
|
|
(isResizeFromCenter && /^(w|nw|sw)$/.test(resizeHandle)),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2020-05-28 07:17:15 +09:00
|
|
|
const resizeSingleTextElement = (
|
|
|
|
element: NonDeleted<ExcalidrawTextElement>,
|
|
|
|
resizeHandle: "nw" | "ne" | "sw" | "se",
|
|
|
|
isResizeFromCenter: 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;
|
|
|
|
switch (resizeHandle) {
|
|
|
|
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(
|
2020-06-25 21:21:27 +02:00
|
|
|
getSidesForResizeHandle(resizeHandle, isResizeFromCenter),
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-05-05 00:25:40 +09:00
|
|
|
const resizeSingleElement = (
|
|
|
|
element: NonDeletedExcalidrawElement,
|
|
|
|
resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
|
|
|
sidesWithSameLength: boolean,
|
|
|
|
isResizeFromCenter: boolean,
|
2020-05-09 17:57:00 +09:00
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
2020-04-09 16:14:32 +01:00
|
|
|
) => {
|
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;
|
|
|
|
// rotation pointer with reverse angle
|
|
|
|
const [rotatedX, rotatedY] = rotate(
|
2020-05-11 00:41:36 +09:00
|
|
|
pointerX,
|
|
|
|
pointerY,
|
2020-05-05 00:25:40 +09:00
|
|
|
cx,
|
|
|
|
cy,
|
|
|
|
-element.angle,
|
2020-04-07 17:49:59 +09:00
|
|
|
);
|
2020-05-05 00:25:40 +09:00
|
|
|
let scaleX = 1;
|
|
|
|
let scaleY = 1;
|
|
|
|
if (resizeHandle === "e" || resizeHandle === "ne" || resizeHandle === "se") {
|
2020-05-09 17:57:00 +09:00
|
|
|
scaleX = (rotatedX - x1) / (x2 - x1);
|
2020-05-05 00:25:40 +09:00
|
|
|
}
|
|
|
|
if (resizeHandle === "s" || resizeHandle === "sw" || resizeHandle === "se") {
|
2020-05-09 17:57:00 +09:00
|
|
|
scaleY = (rotatedY - y1) / (y2 - y1);
|
2020-05-05 00:25:40 +09:00
|
|
|
}
|
|
|
|
if (resizeHandle === "w" || resizeHandle === "nw" || resizeHandle === "sw") {
|
2020-05-09 17:57:00 +09:00
|
|
|
scaleX = (x2 - rotatedX) / (x2 - x1);
|
2020-05-05 00:25:40 +09:00
|
|
|
}
|
|
|
|
if (resizeHandle === "n" || resizeHandle === "nw" || resizeHandle === "ne") {
|
2020-05-09 17:57:00 +09:00
|
|
|
scaleY = (y2 - rotatedY) / (y2 - y1);
|
2020-05-05 00:25:40 +09:00
|
|
|
}
|
|
|
|
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;
|
2020-05-14 23:56:14 +09:00
|
|
|
const rescaledPoints = rescalePointsInElement(element, nextWidth, nextHeight);
|
2020-05-05 00:25:40 +09:00
|
|
|
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(
|
2020-06-25 21:21:27 +02:00
|
|
|
getSidesForResizeHandle(resizeHandle, isResizeFromCenter),
|
2020-05-05 00:25:40 +09:00
|
|
|
element.x - flipDiffX,
|
|
|
|
element.y - flipDiffY,
|
|
|
|
element.angle,
|
|
|
|
deltaX1,
|
|
|
|
deltaY1,
|
|
|
|
deltaX2,
|
|
|
|
deltaY2,
|
|
|
|
);
|
|
|
|
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",
|
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-05-05 00:25:40 +09:00
|
|
|
switch (resizeHandle) {
|
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);
|
|
|
|
const rescaledPoints = rescalePointsInElement(element, width, height);
|
|
|
|
const finalCoords = getResizedElementAbsoluteCoords(
|
|
|
|
{
|
|
|
|
...element,
|
|
|
|
...rescaledPoints,
|
|
|
|
},
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
);
|
|
|
|
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 = (
|
|
|
|
elements: readonly NonDeletedExcalidrawElement[],
|
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
|
|
|
isRotateWithDiscreteAngle: boolean,
|
|
|
|
centerX: number,
|
|
|
|
centerY: number,
|
|
|
|
originalElements: readonly NonDeletedExcalidrawElement[],
|
|
|
|
) => {
|
|
|
|
let centerAngle =
|
|
|
|
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
|
|
|
if (isRotateWithDiscreteAngle) {
|
|
|
|
centerAngle += SHIFT_LOCKING_ANGLE / 2;
|
|
|
|
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
|
|
|
}
|
|
|
|
elements.forEach((element, index) => {
|
|
|
|
if (isLinearElement(element) && element.points.length === 2) {
|
|
|
|
// FIXME this is a bit tricky (how can we make this more readable?)
|
|
|
|
const originalElement = originalElements[index];
|
|
|
|
if (
|
|
|
|
!isLinearElement(originalElement) ||
|
|
|
|
originalElement.points.length !== 2
|
|
|
|
) {
|
|
|
|
throw new Error("original element not compatible"); // should not happen
|
|
|
|
}
|
|
|
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
|
|
|
|
const cx = (x1 + x2) / 2;
|
|
|
|
const cy = (y1 + y2) / 2;
|
|
|
|
const [rotatedCX, rotatedCY] = rotate(
|
|
|
|
cx,
|
|
|
|
cy,
|
|
|
|
centerX,
|
|
|
|
centerY,
|
|
|
|
centerAngle,
|
|
|
|
);
|
|
|
|
const { points } = originalElement;
|
|
|
|
const [rotatedX, rotatedY] = rotate(
|
|
|
|
points[1][0],
|
|
|
|
points[1][1],
|
|
|
|
points[0][0],
|
|
|
|
points[0][1],
|
|
|
|
centerAngle,
|
|
|
|
);
|
|
|
|
mutateElement(element, {
|
|
|
|
x:
|
|
|
|
originalElement.x +
|
|
|
|
(rotatedCX - cx) +
|
|
|
|
((originalElement.points[0][0] + originalElement.points[1][0]) / 2 -
|
|
|
|
(points[0][0] + rotatedX) / 2),
|
|
|
|
y:
|
|
|
|
originalElement.y +
|
|
|
|
(rotatedCY - cy) +
|
|
|
|
((originalElement.points[0][1] + originalElement.points[1][1]) / 2 -
|
|
|
|
(points[0][1] + rotatedY) / 2),
|
|
|
|
points: [
|
|
|
|
[points[0][0], points[0][1]],
|
|
|
|
[rotatedX, rotatedY],
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
|
|
|
const cx = (x1 + x2) / 2;
|
|
|
|
const cy = (y1 + y2) / 2;
|
|
|
|
const [rotatedCX, rotatedCY] = rotate(
|
|
|
|
cx,
|
|
|
|
cy,
|
|
|
|
centerX,
|
|
|
|
centerY,
|
|
|
|
centerAngle + originalElements[index].angle - element.angle,
|
|
|
|
);
|
|
|
|
mutateElement(element, {
|
|
|
|
x: element.x + (rotatedCX - cx),
|
|
|
|
y: element.y + (rotatedCY - cy),
|
|
|
|
angle: normalizeAngle(centerAngle + originalElements[index].angle),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-05-09 17:57:00 +09:00
|
|
|
export const getResizeOffsetXY = (
|
|
|
|
resizeHandle: ResizeTestType,
|
|
|
|
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 (resizeHandle) {
|
|
|
|
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 = (
|
|
|
|
resizeHandle: ResizeTestType,
|
|
|
|
element: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
): "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";
|
|
|
|
};
|