fix resizing: dynamic pointer offset for better UX (#1560)

pull/1562/head
Daishi Kato 5 years ago committed by GitHub
parent 8c8458ceb0
commit 3b1d6910aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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…
Cancel
Save