2020-04-07 17:49:59 +09:00
|
|
|
import { AppState } from "../types";
|
|
|
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
|
|
|
import { getSelectedElements, globalSceneState } from "../scene";
|
|
|
|
import { rescalePoints } from "../points";
|
2020-04-10 01:10:35 +09:00
|
|
|
|
2020-04-09 16:14:32 +01:00
|
|
|
import { rotate, resizeXYWidthHightWithRotation } from "../math";
|
2020-04-08 09:49:52 -07:00
|
|
|
import {
|
|
|
|
ExcalidrawLinearElement,
|
|
|
|
NonDeletedExcalidrawElement,
|
|
|
|
NonDeleted,
|
2020-04-10 01:10:35 +09:00
|
|
|
ResizeArrowFnType,
|
2020-04-08 09:49:52 -07:00
|
|
|
} from "./types";
|
2020-04-07 17:49:59 +09:00
|
|
|
import { getElementAbsoluteCoords, getCommonBounds } from "./bounds";
|
|
|
|
import { isLinearElement } from "./typeChecks";
|
|
|
|
import { mutateElement } from "./mutateElement";
|
|
|
|
import { getPerfectElementSize, normalizeDimensions } from "./sizeHelpers";
|
|
|
|
import {
|
|
|
|
resizeTest,
|
|
|
|
getCursorForResizingElement,
|
|
|
|
normalizeResizeHandle,
|
|
|
|
} from "./resizeTest";
|
2020-04-22 16:57:17 +01:00
|
|
|
import {
|
|
|
|
getResizeCenterPointKey,
|
|
|
|
getResizeWithSidesSameLengthKey,
|
|
|
|
} from "../keys";
|
2020-04-07 17:49:59 +09:00
|
|
|
|
|
|
|
type ResizeTestType = ReturnType<typeof resizeTest>;
|
|
|
|
|
|
|
|
const arrowResizeOrigin: ResizeArrowFnType = (
|
2020-04-08 09:49:52 -07:00
|
|
|
element,
|
|
|
|
pointIndex,
|
|
|
|
deltaX,
|
|
|
|
deltaY,
|
|
|
|
pointerX,
|
|
|
|
pointerY,
|
2020-04-09 16:14:32 +01:00
|
|
|
sidesWithSameLength,
|
2020-04-07 17:49:59 +09:00
|
|
|
) => {
|
|
|
|
const [px, py] = element.points[pointIndex];
|
|
|
|
let x = element.x + deltaX;
|
|
|
|
let y = element.y + deltaY;
|
|
|
|
let pointX = px - deltaX;
|
|
|
|
let pointY = py - deltaY;
|
|
|
|
|
2020-04-09 16:14:32 +01:00
|
|
|
if (sidesWithSameLength) {
|
2020-04-07 17:49:59 +09:00
|
|
|
const { width, height } = getPerfectElementSize(
|
|
|
|
element.type,
|
|
|
|
px + element.x - pointerX,
|
|
|
|
py + element.y - pointerY,
|
|
|
|
);
|
|
|
|
x = px + element.x - width;
|
|
|
|
y = py + element.y - height;
|
|
|
|
pointX = width;
|
|
|
|
pointY = height;
|
|
|
|
}
|
|
|
|
|
|
|
|
mutateElement(element, {
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
points: element.points.map((point, i) =>
|
|
|
|
i === pointIndex ? ([pointX, pointY] as const) : point,
|
|
|
|
),
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const arrowResizeEnd: ResizeArrowFnType = (
|
2020-04-08 09:49:52 -07:00
|
|
|
element,
|
|
|
|
pointIndex,
|
|
|
|
deltaX,
|
|
|
|
deltaY,
|
|
|
|
pointerX,
|
|
|
|
pointerY,
|
2020-04-09 16:14:32 +01:00
|
|
|
sidesWithSameLength,
|
2020-04-07 17:49:59 +09:00
|
|
|
) => {
|
|
|
|
const [px, py] = element.points[pointIndex];
|
2020-04-09 16:14:32 +01:00
|
|
|
if (sidesWithSameLength) {
|
2020-04-07 17:49:59 +09:00
|
|
|
const { width, height } = getPerfectElementSize(
|
|
|
|
element.type,
|
|
|
|
pointerX - element.x,
|
|
|
|
pointerY - element.y,
|
|
|
|
);
|
|
|
|
mutateElement(element, {
|
|
|
|
points: element.points.map((point, i) =>
|
|
|
|
i === pointIndex ? ([width, height] as const) : point,
|
|
|
|
),
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
mutateElement(element, {
|
|
|
|
points: element.points.map((point, i) =>
|
|
|
|
i === pointIndex ? ([px + deltaX, py + deltaY] as const) : point,
|
|
|
|
),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-04-09 16:14:32 +01:00
|
|
|
const applyResizeArrowFn = (
|
|
|
|
element: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
resizeArrowFn: ResizeArrowFnType | null,
|
|
|
|
setResizeArrowFn: (fn: ResizeArrowFnType) => void,
|
|
|
|
isResizeEnd: boolean,
|
|
|
|
sidesWithSameLength: boolean,
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
lastX: number,
|
|
|
|
lastY: number,
|
|
|
|
) => {
|
|
|
|
const angle = element.angle;
|
|
|
|
const [deltaX, deltaY] = rotate(x - lastX, y - lastY, 0, 0, -angle);
|
|
|
|
if (!resizeArrowFn) {
|
|
|
|
if (isResizeEnd) {
|
|
|
|
resizeArrowFn = arrowResizeEnd;
|
|
|
|
} else {
|
|
|
|
resizeArrowFn = arrowResizeOrigin;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resizeArrowFn(element, 1, deltaX, deltaY, x, y, sidesWithSameLength);
|
|
|
|
setResizeArrowFn(resizeArrowFn);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const resizeElements = (
|
2020-04-07 17:49:59 +09:00
|
|
|
resizeHandle: ResizeTestType,
|
|
|
|
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
|
|
|
|
appState: AppState,
|
|
|
|
setAppState: (obj: any) => void,
|
2020-04-22 16:57:17 +01:00
|
|
|
resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
|
|
|
|
setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
|
|
|
|
event: PointerEvent, // XXX we want to make it independent?
|
2020-04-09 16:14:32 +01:00
|
|
|
xPointer: number,
|
|
|
|
yPointer: number,
|
2020-04-22 16:57:17 +01:00
|
|
|
lastX: number, // XXX eliminate in #1339
|
|
|
|
lastY: number, // XXX eliminate in #1339
|
2020-04-09 16:14:32 +01:00
|
|
|
) => {
|
2020-04-07 17:49:59 +09:00
|
|
|
setAppState({
|
|
|
|
isResizing: resizeHandle !== "rotation",
|
|
|
|
isRotating: resizeHandle === "rotation",
|
|
|
|
});
|
|
|
|
const selectedElements = getSelectedElements(
|
2020-04-08 09:49:52 -07:00
|
|
|
globalSceneState.getElements(),
|
2020-04-07 17:49:59 +09:00
|
|
|
appState,
|
|
|
|
);
|
2020-04-09 16:14:32 +01:00
|
|
|
const handleOffset = 4 / appState.zoom; // XXX import constant
|
|
|
|
const dashedLinePadding = 4 / appState.zoom; // XXX import constant
|
|
|
|
const offsetPointer = handleOffset + dashedLinePadding;
|
2020-04-27 17:30:19 +09:00
|
|
|
const minSize = 0;
|
2020-04-07 17:49:59 +09:00
|
|
|
if (selectedElements.length === 1) {
|
2020-04-09 16:14:32 +01:00
|
|
|
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;
|
2020-04-07 17:49:59 +09:00
|
|
|
}
|
2020-04-09 16:14:32 +01:00
|
|
|
if (angle >= 2 * Math.PI) {
|
|
|
|
angle -= 2 * Math.PI;
|
2020-04-07 17:49:59 +09:00
|
|
|
}
|
2020-04-09 16:14:32 +01:00
|
|
|
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,
|
|
|
|
);
|
|
|
|
} else if (resizeHandle) {
|
2020-04-27 17:30:19 +09:00
|
|
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
2020-04-09 16:14:32 +01:00
|
|
|
const resized = resizeXYWidthHightWithRotation(
|
|
|
|
resizeHandle,
|
|
|
|
x1,
|
|
|
|
y1,
|
2020-04-27 17:30:19 +09:00
|
|
|
x2,
|
|
|
|
y2,
|
2020-04-09 16:14:32 +01:00
|
|
|
element.width,
|
|
|
|
element.height,
|
2020-04-27 17:30:19 +09:00
|
|
|
element.x,
|
|
|
|
element.y,
|
2020-04-09 16:14:32 +01:00
|
|
|
element.angle,
|
|
|
|
xPointer,
|
|
|
|
yPointer,
|
|
|
|
offsetPointer,
|
2020-04-22 16:57:17 +01:00
|
|
|
getResizeWithSidesSameLengthKey(event),
|
|
|
|
getResizeCenterPointKey(event),
|
2020-04-09 16:14:32 +01:00
|
|
|
);
|
2020-04-27 17:30:19 +09:00
|
|
|
if (
|
|
|
|
Math.abs(resized.width) > minSize &&
|
|
|
|
Math.abs(resized.height) > minSize
|
|
|
|
) {
|
2020-04-09 16:14:32 +01:00
|
|
|
mutateElement(element, {
|
|
|
|
...resized,
|
|
|
|
...(isLinearElement(element)
|
|
|
|
? {
|
|
|
|
points: rescalePoints(
|
|
|
|
0,
|
|
|
|
resized.width,
|
|
|
|
rescalePoints(1, resized.height, element.points),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
: {}),
|
|
|
|
});
|
2020-04-07 17:49:59 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (resizeHandle) {
|
|
|
|
setResizeHandle(normalizeResizeHandle(element, resizeHandle));
|
|
|
|
}
|
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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(
|
2020-04-09 16:14:32 +01:00
|
|
|
(xPointer - offsetPointer - x1) / (x2 - x1),
|
|
|
|
(yPointer - offsetPointer - y1) / (y2 - y1),
|
2020-04-07 17:49:59 +09:00
|
|
|
);
|
|
|
|
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(
|
2020-04-09 16:14:32 +01:00
|
|
|
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
|
|
|
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
2020-04-07 17:49:59 +09:00
|
|
|
);
|
|
|
|
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(
|
2020-04-09 16:14:32 +01:00
|
|
|
(xPointer - offsetPointer - x1) / (x2 - x1),
|
|
|
|
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
2020-04-07 17:49:59 +09:00
|
|
|
);
|
|
|
|
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(
|
2020-04-09 16:14:32 +01:00
|
|
|
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
|
|
|
(yPointer - offsetPointer - y1) / (y2 - y1),
|
2020-04-07 17:49:59 +09:00
|
|
|
);
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
2020-04-09 16:14:32 +01:00
|
|
|
};
|
2020-04-07 17:49:59 +09:00
|
|
|
|
2020-04-09 16:14:32 +01:00
|
|
|
export const canResizeMutlipleElements = (
|
2020-04-08 09:49:52 -07:00
|
|
|
elements: readonly NonDeletedExcalidrawElement[],
|
2020-04-09 16:14:32 +01:00
|
|
|
) => {
|
2020-04-07 17:49:59 +09:00
|
|
|
return elements.every((element) =>
|
|
|
|
["rectangle", "diamond", "ellipse"].includes(element.type),
|
|
|
|
);
|
2020-04-09 16:14:32 +01:00
|
|
|
};
|