mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix resizing lines with abs coords bigger than element w/h (#1427)
This commit is contained in:
parent
686af31d9d
commit
73d8c5b7c1
4 changed files with 498 additions and 352 deletions
|
@ -3,17 +3,21 @@ import { SHIFT_LOCKING_ANGLE } from "../constants";
|
|||
import { getSelectedElements, globalSceneState } from "../scene";
|
||||
import { rescalePoints } from "../points";
|
||||
|
||||
import { rotate, resizeXYWidthHightWithRotation } from "../math";
|
||||
import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
|
||||
import {
|
||||
ExcalidrawLinearElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
ResizeArrowFnType,
|
||||
} from "./types";
|
||||
import { getElementAbsoluteCoords, getCommonBounds } from "./bounds";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getCommonBounds,
|
||||
getResizedElementAbsoluteCoords,
|
||||
} from "./bounds";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getPerfectElementSize, normalizeDimensions } from "./sizeHelpers";
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import {
|
||||
resizeTest,
|
||||
getCursorForResizingElement,
|
||||
|
@ -26,6 +30,155 @@ import {
|
|||
|
||||
type ResizeTestType = ReturnType<typeof resizeTest>;
|
||||
|
||||
export const resizeElements = (
|
||||
resizeHandle: ResizeTestType,
|
||||
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
|
||||
appState: AppState,
|
||||
setAppState: (obj: any) => void,
|
||||
resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
|
||||
setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
|
||||
event: PointerEvent, // XXX we want to make it independent?
|
||||
xPointer: number,
|
||||
yPointer: number,
|
||||
lastX: number, // XXX eliminate in #1339
|
||||
lastY: number, // XXX eliminate in #1339
|
||||
) => {
|
||||
setAppState({
|
||||
isResizing: resizeHandle && resizeHandle !== "rotation",
|
||||
isRotating: resizeHandle === "rotation",
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getElements(),
|
||||
appState,
|
||||
);
|
||||
const handleOffset = 4 / appState.zoom; // XXX import constant
|
||||
const dashedLinePadding = 4 / appState.zoom; // XXX import constant
|
||||
const offsetPointer = handleOffset + dashedLinePadding;
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
if (resizeHandle === "rotation") {
|
||||
rotateSingleElement(element, xPointer, yPointer, event.shiftKey);
|
||||
} else if (
|
||||
isLinearElement(element) &&
|
||||
element.points.length === 2 &&
|
||||
(resizeHandle === "nw" ||
|
||||
resizeHandle === "ne" ||
|
||||
resizeHandle === "sw" ||
|
||||
resizeHandle === "se")
|
||||
) {
|
||||
resizeSingleTwoPointElement(
|
||||
element,
|
||||
resizeHandle,
|
||||
resizeArrowFn,
|
||||
setResizeArrowFn,
|
||||
event.shiftKey,
|
||||
xPointer,
|
||||
yPointer,
|
||||
lastX,
|
||||
lastY,
|
||||
);
|
||||
} else if (resizeHandle) {
|
||||
resizeSingleElement(
|
||||
element,
|
||||
resizeHandle,
|
||||
getResizeWithSidesSameLengthKey(event),
|
||||
getResizeCenterPointKey(event),
|
||||
xPointer,
|
||||
yPointer,
|
||||
offsetPointer,
|
||||
);
|
||||
setResizeHandle(normalizeResizeHandle(element, resizeHandle));
|
||||
if (element.width < 0) {
|
||||
mutateElement(element, { width: -element.width });
|
||||
}
|
||||
if (element.height < 0) {
|
||||
mutateElement(element, { height: -element.height });
|
||||
}
|
||||
}
|
||||
|
||||
// XXX do we need this?
|
||||
document.documentElement.style.cursor = getCursorForResizingElement({
|
||||
element,
|
||||
resizeHandle,
|
||||
});
|
||||
// XXX why do we need this?
|
||||
if (appState.resizingElement) {
|
||||
mutateElement(appState.resizingElement, {
|
||||
x: element.x,
|
||||
y: element.y,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (
|
||||
selectedElements.length > 1 &&
|
||||
(resizeHandle === "nw" ||
|
||||
resizeHandle === "ne" ||
|
||||
resizeHandle === "sw" ||
|
||||
resizeHandle === "se")
|
||||
) {
|
||||
resizeMultipleElements(
|
||||
selectedElements,
|
||||
resizeHandle,
|
||||
xPointer,
|
||||
yPointer,
|
||||
offsetPointer,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const rotateSingleElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
xPointer: number,
|
||||
yPointer: number,
|
||||
isAngleLocking: boolean,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
let angle = (5 * Math.PI) / 2 + Math.atan2(yPointer - cy, xPointer - cx);
|
||||
if (isAngleLocking) {
|
||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
||||
}
|
||||
if (angle >= 2 * Math.PI) {
|
||||
angle -= 2 * Math.PI;
|
||||
}
|
||||
mutateElement(element, { angle });
|
||||
};
|
||||
|
||||
const resizeSingleTwoPointElement = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
resizeHandle: "nw" | "ne" | "sw" | "se",
|
||||
resizeArrowFn: ResizeArrowFnType | null,
|
||||
setResizeArrowFn: (fn: ResizeArrowFnType) => void,
|
||||
sidesWithSameLength: boolean,
|
||||
xPointer: number,
|
||||
yPointer: number,
|
||||
lastX: number,
|
||||
lastY: number,
|
||||
) => {
|
||||
const [, [px, py]] = element.points;
|
||||
const isResizeEnd =
|
||||
(resizeHandle === "nw" && (px < 0 || py < 0)) ||
|
||||
(resizeHandle === "ne" && px >= 0) ||
|
||||
(resizeHandle === "sw" && px <= 0) ||
|
||||
(resizeHandle === "se" && (px > 0 || py > 0));
|
||||
applyResizeArrowFn(
|
||||
element,
|
||||
resizeArrowFn,
|
||||
setResizeArrowFn,
|
||||
isResizeEnd,
|
||||
sidesWithSameLength,
|
||||
xPointer,
|
||||
yPointer,
|
||||
lastX,
|
||||
lastY,
|
||||
);
|
||||
};
|
||||
|
||||
const arrowResizeOrigin: ResizeArrowFnType = (
|
||||
element,
|
||||
pointIndex,
|
||||
|
@ -116,199 +269,196 @@ const applyResizeArrowFn = (
|
|||
setResizeArrowFn(resizeArrowFn);
|
||||
};
|
||||
|
||||
export const resizeElements = (
|
||||
resizeHandle: ResizeTestType,
|
||||
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
|
||||
appState: AppState,
|
||||
setAppState: (obj: any) => void,
|
||||
resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
|
||||
setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
|
||||
event: PointerEvent, // XXX we want to make it independent?
|
||||
const resizeSingleElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
||||
sidesWithSameLength: boolean,
|
||||
isResizeFromCenter: boolean,
|
||||
xPointer: number,
|
||||
yPointer: number,
|
||||
lastX: number, // XXX eliminate in #1339
|
||||
lastY: number, // XXX eliminate in #1339
|
||||
offsetPointer: number,
|
||||
) => {
|
||||
setAppState({
|
||||
isResizing: resizeHandle !== "rotation",
|
||||
isRotating: resizeHandle === "rotation",
|
||||
});
|
||||
const selectedElements = getSelectedElements(
|
||||
globalSceneState.getElements(),
|
||||
appState,
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
// rotation pointer with reverse angle
|
||||
const [rotatedX, rotatedY] = rotate(
|
||||
xPointer,
|
||||
yPointer,
|
||||
cx,
|
||||
cy,
|
||||
-element.angle,
|
||||
);
|
||||
const handleOffset = 4 / appState.zoom; // XXX import constant
|
||||
const dashedLinePadding = 4 / appState.zoom; // XXX import constant
|
||||
const offsetPointer = handleOffset + dashedLinePadding;
|
||||
const minSize = 0;
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
if (resizeHandle === "rotation") {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
let angle = (5 * Math.PI) / 2 + Math.atan2(yPointer - cy, xPointer - cx);
|
||||
if (event.shiftKey) {
|
||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
||||
// XXX this might be slow with closure
|
||||
const adjustWithOffsetPointer = (wh: number) => {
|
||||
if (wh > offsetPointer) {
|
||||
return wh - offsetPointer;
|
||||
} else if (wh < -offsetPointer) {
|
||||
return wh + offsetPointer;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
let scaleX = 1;
|
||||
let scaleY = 1;
|
||||
if (resizeHandle === "e" || resizeHandle === "ne" || resizeHandle === "se") {
|
||||
scaleX = adjustWithOffsetPointer(rotatedX - x1) / (x2 - x1);
|
||||
}
|
||||
if (resizeHandle === "s" || resizeHandle === "sw" || resizeHandle === "se") {
|
||||
scaleY = adjustWithOffsetPointer(rotatedY - y1) / (y2 - y1);
|
||||
}
|
||||
if (resizeHandle === "w" || resizeHandle === "nw" || resizeHandle === "sw") {
|
||||
scaleX = adjustWithOffsetPointer(x2 - rotatedX) / (x2 - x1);
|
||||
}
|
||||
if (resizeHandle === "n" || resizeHandle === "nw" || resizeHandle === "ne") {
|
||||
scaleY = adjustWithOffsetPointer(y2 - rotatedY) / (y2 - y1);
|
||||
}
|
||||
let nextWidth = element.width * scaleX;
|
||||
let nextHeight = element.height * scaleY;
|
||||
if (sidesWithSameLength) {
|
||||
nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
|
||||
}
|
||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||
element,
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
);
|
||||
const deltaX1 = (x1 - nextX1) / 2;
|
||||
const deltaY1 = (y1 - nextY1) / 2;
|
||||
const deltaX2 = (x2 - nextX2) / 2;
|
||||
const deltaY2 = (y2 - nextY2) / 2;
|
||||
const rescaledPoints = isLinearElement(element)
|
||||
? {
|
||||
points: rescalePoints(
|
||||
0,
|
||||
nextWidth,
|
||||
rescalePoints(1, nextHeight, element.points),
|
||||
),
|
||||
}
|
||||
if (angle >= 2 * Math.PI) {
|
||||
angle -= 2 * Math.PI;
|
||||
}
|
||||
mutateElement(element, { angle });
|
||||
} else if (
|
||||
isLinearElement(element) &&
|
||||
element.points.length === 2 &&
|
||||
(resizeHandle === "nw" ||
|
||||
resizeHandle === "ne" ||
|
||||
resizeHandle === "sw" ||
|
||||
resizeHandle === "se")
|
||||
) {
|
||||
const [, [px, py]] = element.points;
|
||||
const isResizeEnd =
|
||||
(resizeHandle === "nw" && (px < 0 || py < 0)) ||
|
||||
(resizeHandle === "ne" && px >= 0) ||
|
||||
(resizeHandle === "sw" && px <= 0) ||
|
||||
(resizeHandle === "se" && (px > 0 || py > 0));
|
||||
applyResizeArrowFn(
|
||||
element,
|
||||
resizeArrowFn,
|
||||
setResizeArrowFn,
|
||||
isResizeEnd,
|
||||
event.shiftKey,
|
||||
xPointer,
|
||||
yPointer,
|
||||
lastX,
|
||||
lastY,
|
||||
: {};
|
||||
const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords(
|
||||
{
|
||||
...element,
|
||||
...rescaledPoints,
|
||||
},
|
||||
Math.abs(nextWidth),
|
||||
Math.abs(nextHeight),
|
||||
);
|
||||
const [flipDiffX, flipDiffY] = getFlipAdjustment(
|
||||
resizeHandle,
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
nextX1,
|
||||
nextY1,
|
||||
nextX2,
|
||||
nextY2,
|
||||
finalX1,
|
||||
finalY1,
|
||||
finalX2,
|
||||
finalY2,
|
||||
isLinearElement(element),
|
||||
element.angle,
|
||||
);
|
||||
const [nextElementX, nextElementY] = adjustXYWithRotation(
|
||||
resizeHandle,
|
||||
element.x - flipDiffX,
|
||||
element.y - flipDiffY,
|
||||
element.angle,
|
||||
deltaX1,
|
||||
deltaY1,
|
||||
deltaX2,
|
||||
deltaY2,
|
||||
isResizeFromCenter,
|
||||
);
|
||||
if (
|
||||
nextWidth !== 0 &&
|
||||
nextHeight !== 0 &&
|
||||
Number.isFinite(nextElementX) &&
|
||||
Number.isFinite(nextElementY)
|
||||
) {
|
||||
mutateElement(element, {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
x: nextElementX,
|
||||
y: nextElementY,
|
||||
...rescaledPoints,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resizeMultipleElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
resizeHandle: "nw" | "ne" | "sw" | "se",
|
||||
xPointer: number,
|
||||
yPointer: number,
|
||||
offsetPointer: number,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||
switch (resizeHandle) {
|
||||
case "se": {
|
||||
const scale = Math.max(
|
||||
(xPointer - offsetPointer - x1) / (x2 - x1),
|
||||
(yPointer - offsetPointer - y1) / (y2 - y1),
|
||||
);
|
||||
} else if (resizeHandle) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const resized = resizeXYWidthHightWithRotation(
|
||||
resizeHandle,
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
element.width,
|
||||
element.height,
|
||||
element.x,
|
||||
element.y,
|
||||
element.angle,
|
||||
xPointer,
|
||||
yPointer,
|
||||
offsetPointer,
|
||||
getResizeWithSidesSameLengthKey(event),
|
||||
getResizeCenterPointKey(event),
|
||||
);
|
||||
if (
|
||||
Math.abs(resized.width) > minSize &&
|
||||
Math.abs(resized.height) > minSize
|
||||
) {
|
||||
mutateElement(element, {
|
||||
...resized,
|
||||
...(isLinearElement(element)
|
||||
? {
|
||||
points: rescalePoints(
|
||||
0,
|
||||
resized.width,
|
||||
rescalePoints(1, resized.height, element.points),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
if (scale > 0) {
|
||||
elements.forEach((element) => {
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const x = element.x + (element.x - x1) * (scale - 1);
|
||||
const y = element.y + (element.y - y1) * (scale - 1);
|
||||
mutateElement(element, { width, height, x, y });
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (resizeHandle) {
|
||||
setResizeHandle(normalizeResizeHandle(element, resizeHandle));
|
||||
case "nw": {
|
||||
const scale = Math.max(
|
||||
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
||||
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
||||
);
|
||||
if (scale > 0) {
|
||||
elements.forEach((element) => {
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const x = element.x - (x2 - element.x) * (scale - 1);
|
||||
const y = element.y - (y2 - element.y) * (scale - 1);
|
||||
mutateElement(element, { width, height, x, y });
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
normalizeDimensions(element);
|
||||
|
||||
// do we need this?
|
||||
document.documentElement.style.cursor = getCursorForResizingElement({
|
||||
element,
|
||||
resizeHandle,
|
||||
});
|
||||
// why do we need this?
|
||||
if (appState.resizingElement) {
|
||||
mutateElement(appState.resizingElement, {
|
||||
x: element.x,
|
||||
y: element.y,
|
||||
});
|
||||
case "ne": {
|
||||
const scale = Math.max(
|
||||
(xPointer - offsetPointer - x1) / (x2 - x1),
|
||||
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
||||
);
|
||||
if (scale > 0) {
|
||||
elements.forEach((element) => {
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const x = element.x + (element.x - x1) * (scale - 1);
|
||||
const y = element.y - (y2 - element.y) * (scale - 1);
|
||||
mutateElement(element, { width, height, x, y });
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (selectedElements.length > 1) {
|
||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||
const minScale = Math.max(minSize / (x2 - x1), minSize / (y2 - y1));
|
||||
switch (resizeHandle) {
|
||||
case "se": {
|
||||
const scale = Math.max(
|
||||
(xPointer - offsetPointer - x1) / (x2 - x1),
|
||||
(yPointer - offsetPointer - y1) / (y2 - y1),
|
||||
);
|
||||
if (scale > minScale) {
|
||||
selectedElements.forEach((element) => {
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const x = element.x + (element.x - x1) * (scale - 1);
|
||||
const y = element.y + (element.y - y1) * (scale - 1);
|
||||
mutateElement(element, { width, height, x, y });
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "nw": {
|
||||
const scale = Math.max(
|
||||
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
||||
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
||||
);
|
||||
if (scale > minScale) {
|
||||
selectedElements.forEach((element) => {
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const x = element.x - (x2 - element.x) * (scale - 1);
|
||||
const y = element.y - (y2 - element.y) * (scale - 1);
|
||||
mutateElement(element, { width, height, x, y });
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "ne": {
|
||||
const scale = Math.max(
|
||||
(xPointer - offsetPointer - x1) / (x2 - x1),
|
||||
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
||||
);
|
||||
if (scale > minScale) {
|
||||
selectedElements.forEach((element) => {
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const x = element.x + (element.x - x1) * (scale - 1);
|
||||
const y = element.y - (y2 - element.y) * (scale - 1);
|
||||
mutateElement(element, { width, height, x, y });
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "sw": {
|
||||
const scale = Math.max(
|
||||
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
||||
(yPointer - offsetPointer - y1) / (y2 - y1),
|
||||
);
|
||||
if (scale > minScale) {
|
||||
selectedElements.forEach((element) => {
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const x = element.x - (x2 - element.x) * (scale - 1);
|
||||
const y = element.y + (element.y - y1) * (scale - 1);
|
||||
mutateElement(element, { width, height, x, y });
|
||||
});
|
||||
}
|
||||
return true;
|
||||
case "sw": {
|
||||
const scale = Math.max(
|
||||
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
||||
(yPointer - offsetPointer - y1) / (y2 - y1),
|
||||
);
|
||||
if (scale > 0) {
|
||||
elements.forEach((element) => {
|
||||
const width = element.width * scale;
|
||||
const height = element.height * scale;
|
||||
const x = element.x - (x2 - element.x) * (scale - 1);
|
||||
const y = element.y + (element.y - y1) * (scale - 1);
|
||||
mutateElement(element, { width, height, x, y });
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canResizeMutlipleElements = (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue