fix resizing: dynamic pointer offset for better UX (#1560)
This commit is contained in:
parent
8c8458ceb0
commit
3b1d6910aa
@ -24,6 +24,7 @@ import {
|
|||||||
resizeElements,
|
resizeElements,
|
||||||
getElementWithResizeHandler,
|
getElementWithResizeHandler,
|
||||||
canResizeMutlipleElements,
|
canResizeMutlipleElements,
|
||||||
|
getResizeOffsetXY,
|
||||||
getResizeHandlerFromCoords,
|
getResizeHandlerFromCoords,
|
||||||
isNonDeletedElement,
|
isNonDeletedElement,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
@ -1845,6 +1846,7 @@ class App extends React.Component<any, AppState> {
|
|||||||
const setResizeHandle = (nextResizeHandle: ResizeTestType) => {
|
const setResizeHandle = (nextResizeHandle: ResizeTestType) => {
|
||||||
resizeHandle = nextResizeHandle;
|
resizeHandle = nextResizeHandle;
|
||||||
};
|
};
|
||||||
|
let resizeOffsetXY: [number, number] = [0, 0];
|
||||||
let isResizingElements = false;
|
let isResizingElements = false;
|
||||||
let draggingOccurred = false;
|
let draggingOccurred = false;
|
||||||
let hitElement: ExcalidrawElement | null = null;
|
let hitElement: ExcalidrawElement | null = null;
|
||||||
@ -1891,6 +1893,14 @@ class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isResizingElements) {
|
||||||
|
resizeOffsetXY = getResizeOffsetXY(
|
||||||
|
resizeHandle,
|
||||||
|
selectedElements,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!isResizingElements) {
|
if (!isResizingElements) {
|
||||||
hitElement = getElementAtPosition(
|
hitElement = getElementAtPosition(
|
||||||
elements,
|
elements,
|
||||||
@ -2123,25 +2133,34 @@ class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resized =
|
if (isResizingElements) {
|
||||||
isResizingElements &&
|
const selectedElements = getSelectedElements(
|
||||||
resizeElements(
|
globalSceneState.getElements(),
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
isResizing: resizeHandle && resizeHandle !== "rotation",
|
||||||
|
isRotating: resizeHandle === "rotation",
|
||||||
|
});
|
||||||
|
const resized = resizeElements(
|
||||||
resizeHandle,
|
resizeHandle,
|
||||||
setResizeHandle,
|
setResizeHandle,
|
||||||
this.state,
|
selectedElements,
|
||||||
this.setAppState,
|
|
||||||
resizeArrowFn,
|
resizeArrowFn,
|
||||||
setResizeArrowFn,
|
setResizeArrowFn,
|
||||||
event,
|
event,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
resizeOffsetXY[0],
|
||||||
|
resizeOffsetXY[1],
|
||||||
lastX,
|
lastX,
|
||||||
lastY,
|
lastY,
|
||||||
);
|
);
|
||||||
if (resized) {
|
if (resized) {
|
||||||
lastX = x;
|
lastX = x;
|
||||||
lastY = y;
|
lastY = y;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hitElement && this.state.selectedElementIds[hitElement.id]) {
|
if (hitElement && this.state.selectedElementIds[hitElement.id]) {
|
||||||
@ -2310,6 +2329,7 @@ class App extends React.Component<any, AppState> {
|
|||||||
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
||||||
|
|
||||||
resizeArrowFn = null;
|
resizeArrowFn = null;
|
||||||
|
resizeOffsetXY = [0, 0];
|
||||||
lastPointerUp = null;
|
lastPointerUp = null;
|
||||||
|
|
||||||
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||||
|
@ -32,7 +32,11 @@ export {
|
|||||||
getElementWithResizeHandler,
|
getElementWithResizeHandler,
|
||||||
getResizeHandlerFromCoords,
|
getResizeHandlerFromCoords,
|
||||||
} from "./resizeTest";
|
} from "./resizeTest";
|
||||||
export { resizeElements, canResizeMutlipleElements } from "./resizeElements";
|
export {
|
||||||
|
resizeElements,
|
||||||
|
canResizeMutlipleElements,
|
||||||
|
getResizeOffsetXY,
|
||||||
|
} from "./resizeElements";
|
||||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
||||||
export { textWysiwyg } from "./textWysiwyg";
|
export { textWysiwyg } from "./textWysiwyg";
|
||||||
export { redrawTextBoundingBox } from "./textElement";
|
export { redrawTextBoundingBox } from "./textElement";
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { AppState } from "../types";
|
|
||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import { getSelectedElements, globalSceneState } from "../scene";
|
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
|
import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
|
||||||
@ -33,31 +31,21 @@ type ResizeTestType = ReturnType<typeof resizeTest>;
|
|||||||
export const resizeElements = (
|
export const resizeElements = (
|
||||||
resizeHandle: ResizeTestType,
|
resizeHandle: ResizeTestType,
|
||||||
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
|
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
|
||||||
appState: AppState,
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
setAppState: (obj: any) => void,
|
|
||||||
resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
|
resizeArrowFn: ResizeArrowFnType | null, // XXX eliminate in #1339
|
||||||
setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
|
setResizeArrowFn: (fn: ResizeArrowFnType) => void, // XXX eliminate in #1339
|
||||||
event: PointerEvent, // XXX we want to make it independent?
|
event: PointerEvent, // XXX we want to make it independent?
|
||||||
xPointer: number,
|
pointerX: number,
|
||||||
yPointer: number,
|
pointerY: number,
|
||||||
|
offsetX: number,
|
||||||
|
offsetY: number,
|
||||||
lastX: number, // XXX eliminate in #1339
|
lastX: number, // XXX eliminate in #1339
|
||||||
lastY: 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) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
if (resizeHandle === "rotation") {
|
if (resizeHandle === "rotation") {
|
||||||
rotateSingleElement(element, xPointer, yPointer, event.shiftKey);
|
rotateSingleElement(element, pointerX, pointerY, event.shiftKey);
|
||||||
} else if (
|
} else if (
|
||||||
isLinearElement(element) &&
|
isLinearElement(element) &&
|
||||||
element.points.length === 2 &&
|
element.points.length === 2 &&
|
||||||
@ -72,8 +60,8 @@ export const resizeElements = (
|
|||||||
resizeArrowFn,
|
resizeArrowFn,
|
||||||
setResizeArrowFn,
|
setResizeArrowFn,
|
||||||
event.shiftKey,
|
event.shiftKey,
|
||||||
xPointer,
|
pointerX,
|
||||||
yPointer,
|
pointerY,
|
||||||
lastX,
|
lastX,
|
||||||
lastY,
|
lastY,
|
||||||
);
|
);
|
||||||
@ -83,9 +71,10 @@ export const resizeElements = (
|
|||||||
resizeHandle,
|
resizeHandle,
|
||||||
getResizeWithSidesSameLengthKey(event),
|
getResizeWithSidesSameLengthKey(event),
|
||||||
getResizeCenterPointKey(event),
|
getResizeCenterPointKey(event),
|
||||||
xPointer,
|
pointerX,
|
||||||
yPointer,
|
pointerY,
|
||||||
offsetPointer,
|
offsetX,
|
||||||
|
offsetY,
|
||||||
);
|
);
|
||||||
setResizeHandle(normalizeResizeHandle(element, resizeHandle));
|
setResizeHandle(normalizeResizeHandle(element, resizeHandle));
|
||||||
if (element.width < 0) {
|
if (element.width < 0) {
|
||||||
@ -96,18 +85,12 @@ export const resizeElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX do we need this?
|
// update cursor
|
||||||
|
// FIXME it is not very nice to have this here
|
||||||
document.documentElement.style.cursor = getCursorForResizingElement({
|
document.documentElement.style.cursor = getCursorForResizingElement({
|
||||||
element,
|
element,
|
||||||
resizeHandle,
|
resizeHandle,
|
||||||
});
|
});
|
||||||
// XXX why do we need this?
|
|
||||||
if (appState.resizingElement) {
|
|
||||||
mutateElement(appState.resizingElement, {
|
|
||||||
x: element.x,
|
|
||||||
y: element.y,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else if (
|
} else if (
|
||||||
@ -120,9 +103,10 @@ export const resizeElements = (
|
|||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
resizeHandle,
|
resizeHandle,
|
||||||
xPointer,
|
pointerX,
|
||||||
yPointer,
|
pointerY,
|
||||||
offsetPointer,
|
offsetX,
|
||||||
|
offsetY,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -131,14 +115,14 @@ export const resizeElements = (
|
|||||||
|
|
||||||
const rotateSingleElement = (
|
const rotateSingleElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
xPointer: number,
|
pointerX: number,
|
||||||
yPointer: number,
|
pointerY: number,
|
||||||
isAngleLocking: boolean,
|
isAngleLocking: boolean,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
let angle = (5 * Math.PI) / 2 + Math.atan2(yPointer - cy, xPointer - cx);
|
let angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
|
||||||
if (isAngleLocking) {
|
if (isAngleLocking) {
|
||||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
angle += SHIFT_LOCKING_ANGLE / 2;
|
||||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
angle -= angle % SHIFT_LOCKING_ANGLE;
|
||||||
@ -155,8 +139,8 @@ const resizeSingleTwoPointElement = (
|
|||||||
resizeArrowFn: ResizeArrowFnType | null,
|
resizeArrowFn: ResizeArrowFnType | null,
|
||||||
setResizeArrowFn: (fn: ResizeArrowFnType) => void,
|
setResizeArrowFn: (fn: ResizeArrowFnType) => void,
|
||||||
sidesWithSameLength: boolean,
|
sidesWithSameLength: boolean,
|
||||||
xPointer: number,
|
pointerX: number,
|
||||||
yPointer: number,
|
pointerY: number,
|
||||||
lastX: number,
|
lastX: number,
|
||||||
lastY: number,
|
lastY: number,
|
||||||
) => {
|
) => {
|
||||||
@ -172,8 +156,8 @@ const resizeSingleTwoPointElement = (
|
|||||||
setResizeArrowFn,
|
setResizeArrowFn,
|
||||||
isResizeEnd,
|
isResizeEnd,
|
||||||
sidesWithSameLength,
|
sidesWithSameLength,
|
||||||
xPointer,
|
pointerX,
|
||||||
yPointer,
|
pointerY,
|
||||||
lastX,
|
lastX,
|
||||||
lastY,
|
lastY,
|
||||||
);
|
);
|
||||||
@ -274,43 +258,35 @@ const resizeSingleElement = (
|
|||||||
resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
||||||
sidesWithSameLength: boolean,
|
sidesWithSameLength: boolean,
|
||||||
isResizeFromCenter: boolean,
|
isResizeFromCenter: boolean,
|
||||||
xPointer: number,
|
pointerX: number,
|
||||||
yPointer: number,
|
pointerY: number,
|
||||||
offsetPointer: number,
|
offsetX: number,
|
||||||
|
offsetY: number,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
// rotation pointer with reverse angle
|
// rotation pointer with reverse angle
|
||||||
const [rotatedX, rotatedY] = rotate(
|
const [rotatedX, rotatedY] = rotate(
|
||||||
xPointer,
|
pointerX - offsetX,
|
||||||
yPointer,
|
pointerY - offsetY,
|
||||||
cx,
|
cx,
|
||||||
cy,
|
cy,
|
||||||
-element.angle,
|
-element.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 scaleX = 1;
|
||||||
let scaleY = 1;
|
let scaleY = 1;
|
||||||
if (resizeHandle === "e" || resizeHandle === "ne" || resizeHandle === "se") {
|
if (resizeHandle === "e" || resizeHandle === "ne" || resizeHandle === "se") {
|
||||||
scaleX = adjustWithOffsetPointer(rotatedX - x1) / (x2 - x1);
|
scaleX = (rotatedX - x1) / (x2 - x1);
|
||||||
}
|
}
|
||||||
if (resizeHandle === "s" || resizeHandle === "sw" || resizeHandle === "se") {
|
if (resizeHandle === "s" || resizeHandle === "sw" || resizeHandle === "se") {
|
||||||
scaleY = adjustWithOffsetPointer(rotatedY - y1) / (y2 - y1);
|
scaleY = (rotatedY - y1) / (y2 - y1);
|
||||||
}
|
}
|
||||||
if (resizeHandle === "w" || resizeHandle === "nw" || resizeHandle === "sw") {
|
if (resizeHandle === "w" || resizeHandle === "nw" || resizeHandle === "sw") {
|
||||||
scaleX = adjustWithOffsetPointer(x2 - rotatedX) / (x2 - x1);
|
scaleX = (x2 - rotatedX) / (x2 - x1);
|
||||||
}
|
}
|
||||||
if (resizeHandle === "n" || resizeHandle === "nw" || resizeHandle === "ne") {
|
if (resizeHandle === "n" || resizeHandle === "nw" || resizeHandle === "ne") {
|
||||||
scaleY = adjustWithOffsetPointer(y2 - rotatedY) / (y2 - y1);
|
scaleY = (y2 - rotatedY) / (y2 - y1);
|
||||||
}
|
}
|
||||||
let nextWidth = element.width * scaleX;
|
let nextWidth = element.width * scaleX;
|
||||||
let nextHeight = element.height * scaleY;
|
let nextHeight = element.height * scaleY;
|
||||||
@ -388,16 +364,17 @@ const resizeSingleElement = (
|
|||||||
const resizeMultipleElements = (
|
const resizeMultipleElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
resizeHandle: "nw" | "ne" | "sw" | "se",
|
resizeHandle: "nw" | "ne" | "sw" | "se",
|
||||||
xPointer: number,
|
pointerX: number,
|
||||||
yPointer: number,
|
pointerY: number,
|
||||||
offsetPointer: number,
|
offsetX: number,
|
||||||
|
offsetY: number,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||||
switch (resizeHandle) {
|
switch (resizeHandle) {
|
||||||
case "se": {
|
case "se": {
|
||||||
const scale = Math.max(
|
const scale = Math.max(
|
||||||
(xPointer - offsetPointer - x1) / (x2 - x1),
|
(pointerX - offsetX - x1) / (x2 - x1),
|
||||||
(yPointer - offsetPointer - y1) / (y2 - y1),
|
(pointerY - offsetY - y1) / (y2 - y1),
|
||||||
);
|
);
|
||||||
if (scale > 0) {
|
if (scale > 0) {
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
@ -412,8 +389,8 @@ const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
case "nw": {
|
case "nw": {
|
||||||
const scale = Math.max(
|
const scale = Math.max(
|
||||||
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
(x2 - (pointerX - offsetX)) / (x2 - x1),
|
||||||
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
(y2 - (pointerY - offsetY)) / (y2 - y1),
|
||||||
);
|
);
|
||||||
if (scale > 0) {
|
if (scale > 0) {
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
@ -428,8 +405,8 @@ const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
case "ne": {
|
case "ne": {
|
||||||
const scale = Math.max(
|
const scale = Math.max(
|
||||||
(xPointer - offsetPointer - x1) / (x2 - x1),
|
(pointerX - offsetX - x1) / (x2 - x1),
|
||||||
(y2 - offsetPointer - yPointer) / (y2 - y1),
|
(y2 - (pointerY - offsetY)) / (y2 - y1),
|
||||||
);
|
);
|
||||||
if (scale > 0) {
|
if (scale > 0) {
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
@ -444,8 +421,8 @@ const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
case "sw": {
|
case "sw": {
|
||||||
const scale = Math.max(
|
const scale = Math.max(
|
||||||
(x2 - offsetPointer - xPointer) / (x2 - x1),
|
(x2 - (pointerX - offsetX)) / (x2 - x1),
|
||||||
(yPointer - offsetPointer - y1) / (y2 - y1),
|
(pointerY - offsetY - y1) / (y2 - y1),
|
||||||
);
|
);
|
||||||
if (scale > 0) {
|
if (scale > 0) {
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
@ -468,3 +445,39 @@ export const canResizeMutlipleElements = (
|
|||||||
["rectangle", "diamond", "ellipse"].includes(element.type),
|
["rectangle", "diamond", "ellipse"].includes(element.type),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user