Shift loses pointer fixing #1296 (#1330)

* change resize math to absolute instead of delta

* typings

* small change for width on rotation

* apply absolute resize to other sides

* revert&change math.ts

* polish, polish, polish

* refactor with offset

* eliminate nextX

* rename to offsetPointer

* fix curved lines

* prefer arrow function

* remove unused variables/comments for now

Co-authored-by: daishi <daishi@axlight.com>
This commit is contained in:
José Quinto 2020-04-09 16:14:32 +01:00 committed by GitHub
parent bd32a26653
commit 8efe0b7d05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 205 additions and 254 deletions

View File

@ -2,7 +2,7 @@ import { AppState } from "../types";
import { SHIFT_LOCKING_ANGLE } from "../constants"; import { SHIFT_LOCKING_ANGLE } from "../constants";
import { getSelectedElements, globalSceneState } from "../scene"; import { getSelectedElements, globalSceneState } from "../scene";
import { rescalePoints } from "../points"; import { rescalePoints } from "../points";
import { rotate, adjustXYWithRotation } from "../math"; import { rotate, resizeXYWidthHightWithRotation } from "../math";
import { import {
ExcalidrawLinearElement, ExcalidrawLinearElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
@ -27,7 +27,7 @@ export type ResizeArrowFnType = (
deltaY: number, deltaY: number,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
perfect: boolean, sidesWithSameLength: boolean,
) => void; ) => void;
const arrowResizeOrigin: ResizeArrowFnType = ( const arrowResizeOrigin: ResizeArrowFnType = (
@ -37,7 +37,7 @@ const arrowResizeOrigin: ResizeArrowFnType = (
deltaY, deltaY,
pointerX, pointerX,
pointerY, pointerY,
perfect, sidesWithSameLength,
) => { ) => {
const [px, py] = element.points[pointIndex]; const [px, py] = element.points[pointIndex];
let x = element.x + deltaX; let x = element.x + deltaX;
@ -45,7 +45,7 @@ const arrowResizeOrigin: ResizeArrowFnType = (
let pointX = px - deltaX; let pointX = px - deltaX;
let pointY = py - deltaY; let pointY = py - deltaY;
if (perfect) { if (sidesWithSameLength) {
const { width, height } = getPerfectElementSize( const { width, height } = getPerfectElementSize(
element.type, element.type,
px + element.x - pointerX, px + element.x - pointerX,
@ -73,10 +73,10 @@ const arrowResizeEnd: ResizeArrowFnType = (
deltaY, deltaY,
pointerX, pointerX,
pointerY, pointerY,
perfect, sidesWithSameLength,
) => { ) => {
const [px, py] = element.points[pointIndex]; const [px, py] = element.points[pointIndex];
if (perfect) { if (sidesWithSameLength) {
const { width, height } = getPerfectElementSize( const { width, height } = getPerfectElementSize(
element.type, element.type,
pointerX - element.x, pointerX - element.x,
@ -96,7 +96,31 @@ const arrowResizeEnd: ResizeArrowFnType = (
} }
}; };
export function resizeElements( 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 = (
resizeHandle: ResizeTestType, resizeHandle: ResizeTestType,
setResizeHandle: (nextResizeHandle: ResizeTestType) => void, setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
appState: AppState, appState: AppState,
@ -104,11 +128,11 @@ export function resizeElements(
resizeArrowFn: ResizeArrowFnType | null, resizeArrowFn: ResizeArrowFnType | null,
setResizeArrowFn: (fn: ResizeArrowFnType) => void, setResizeArrowFn: (fn: ResizeArrowFnType) => void,
event: PointerEvent, event: PointerEvent,
x: number, xPointer: number,
y: number, yPointer: number,
lastX: number, lastX: number,
lastY: number, lastY: number,
) { ) => {
setAppState({ setAppState({
isResizing: resizeHandle !== "rotation", isResizing: resizeHandle !== "rotation",
isRotating: resizeHandle === "rotation", isRotating: resizeHandle === "rotation",
@ -117,224 +141,79 @@ export function resizeElements(
globalSceneState.getElements(), globalSceneState.getElements(),
appState, appState,
); );
const handleOffset = 4 / appState.zoom; // XXX import constant
const dashedLinePadding = 4 / appState.zoom; // XXX import constant
const offsetPointer = handleOffset + dashedLinePadding;
const minSize = handleOffset * 4;
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const element = selectedElements[0]; const [element] = selectedElements;
const angle = element.angle; if (resizeHandle === "rotation") {
// reverse rotate delta const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const [deltaX, deltaY] = rotate(x - lastX, y - lastY, 0, 0, -angle); const cx = (x1 + x2) / 2;
switch (resizeHandle) { const cy = (y1 + y2) / 2;
case "nw": let angle = (5 * Math.PI) / 2 + Math.atan2(yPointer - cy, xPointer - cx);
if (isLinearElement(element) && element.points.length === 2) { if (event.shiftKey) {
const [, p1] = element.points; angle += SHIFT_LOCKING_ANGLE / 2;
angle -= angle % SHIFT_LOCKING_ANGLE;
if (!resizeArrowFn) {
if (p1[0] < 0 || p1[1] < 0) {
resizeArrowFn = arrowResizeEnd;
} else {
resizeArrowFn = arrowResizeOrigin;
}
}
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
setResizeArrowFn(resizeArrowFn);
} else {
const width = element.width - deltaX;
const height = event.shiftKey ? width : element.height - deltaY;
const dY = element.height - height;
mutateElement(element, {
width,
height,
...adjustXYWithRotation("nw", element, deltaX, dY, angle),
...(isLinearElement(element) && width !== 0 && height !== 0
? {
points: rescalePoints(
0,
width,
rescalePoints(1, height, element.points),
),
}
: {}),
});
}
break;
case "ne":
if (isLinearElement(element) && element.points.length === 2) {
const [, p1] = element.points;
if (!resizeArrowFn) {
if (p1[0] >= 0) {
resizeArrowFn = arrowResizeEnd;
} else {
resizeArrowFn = arrowResizeOrigin;
}
}
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
setResizeArrowFn(resizeArrowFn);
} else {
const width = element.width + deltaX;
const height = event.shiftKey ? width : element.height - deltaY;
const dY = element.height - height;
mutateElement(element, {
width,
height,
...adjustXYWithRotation("ne", element, deltaX, dY, angle),
...(isLinearElement(element) && width !== 0 && height !== 0
? {
points: rescalePoints(
0,
width,
rescalePoints(1, height, element.points),
),
}
: {}),
});
}
break;
case "sw":
if (isLinearElement(element) && element.points.length === 2) {
const [, p1] = element.points;
if (!resizeArrowFn) {
if (p1[0] <= 0) {
resizeArrowFn = arrowResizeEnd;
} else {
resizeArrowFn = arrowResizeOrigin;
}
}
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
setResizeArrowFn(resizeArrowFn);
} else {
const width = element.width - deltaX;
const height = event.shiftKey ? width : element.height + deltaY;
const dY = height - element.height;
mutateElement(element, {
width,
height,
...adjustXYWithRotation("sw", element, deltaX, dY, angle),
...(isLinearElement(element) && width !== 0 && height !== 0
? {
points: rescalePoints(
0,
width,
rescalePoints(1, height, element.points),
),
}
: {}),
});
}
break;
case "se":
if (isLinearElement(element) && element.points.length === 2) {
const [, p1] = element.points;
if (!resizeArrowFn) {
if (p1[0] > 0 || p1[1] > 0) {
resizeArrowFn = arrowResizeEnd;
} else {
resizeArrowFn = arrowResizeOrigin;
}
}
resizeArrowFn(element, 1, deltaX, deltaY, x, y, event.shiftKey);
setResizeArrowFn(resizeArrowFn);
} else {
const width = element.width + deltaX;
const height = event.shiftKey ? width : element.height + deltaY;
const dY = height - element.height;
mutateElement(element, {
width,
height,
...adjustXYWithRotation("se", element, deltaX, dY, angle),
...(isLinearElement(element) && width !== 0 && height !== 0
? {
points: rescalePoints(
0,
width,
rescalePoints(1, height, element.points),
),
}
: {}),
});
}
break;
case "n": {
const height = element.height - deltaY;
if (isLinearElement(element) && height !== 0) {
mutateElement(element, {
height,
...adjustXYWithRotation("n", element, 0, deltaY, angle),
points: rescalePoints(1, height, element.points),
});
} else {
mutateElement(element, {
height,
...adjustXYWithRotation("n", element, 0, deltaY, angle),
});
}
break;
} }
case "w": { if (angle >= 2 * Math.PI) {
const width = element.width - deltaX; angle -= 2 * Math.PI;
if (isLinearElement(element) && width !== 0) {
mutateElement(element, {
width,
...adjustXYWithRotation("w", element, deltaX, 0, angle),
points: rescalePoints(0, width, element.points),
});
} else {
mutateElement(element, {
width,
...adjustXYWithRotation("w", element, deltaX, 0, angle),
});
}
break;
} }
case "s": { mutateElement(element, { angle });
const height = element.height + deltaY; } else if (
isLinearElement(element) &&
if (isLinearElement(element) && height !== 0) { element.points.length === 2 &&
mutateElement(element, { (resizeHandle === "nw" ||
height, resizeHandle === "ne" ||
...adjustXYWithRotation("s", element, 0, deltaY, angle), resizeHandle === "sw" ||
points: rescalePoints(1, height, element.points), resizeHandle === "se")
}); ) {
} else { const [, [px, py]] = element.points;
mutateElement(element, { const isResizeEnd =
height, (resizeHandle === "nw" && (px < 0 || py < 0)) ||
...adjustXYWithRotation("s", element, 0, deltaY, angle), (resizeHandle === "ne" && px >= 0) ||
}); (resizeHandle === "sw" && px <= 0) ||
} (resizeHandle === "se" && (px > 0 || py > 0));
break; applyResizeArrowFn(
} element,
case "e": { resizeArrowFn,
const width = element.width + deltaX; setResizeArrowFn,
isResizeEnd,
if (isLinearElement(element) && width !== 0) { event.shiftKey,
mutateElement(element, { xPointer,
width, yPointer,
...adjustXYWithRotation("e", element, deltaX, 0, angle), lastX,
points: rescalePoints(0, width, element.points), lastY,
}); );
} else { } else if (resizeHandle) {
mutateElement(element, { const [x1, y1] = getElementAbsoluteCoords(element);
width, const resized = resizeXYWidthHightWithRotation(
...adjustXYWithRotation("e", element, deltaX, 0, angle), resizeHandle,
}); x1,
} y1,
break; element.width,
} element.height,
case "rotation": { x1 - element.x,
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); y1 - element.y,
const cx = (x1 + x2) / 2; element.angle,
const cy = (y1 + y2) / 2; xPointer,
let angle = (5 * Math.PI) / 2 + Math.atan2(y - cy, x - cx); yPointer,
if (event.shiftKey) { offsetPointer,
angle += SHIFT_LOCKING_ANGLE / 2; event.shiftKey,
angle -= angle % SHIFT_LOCKING_ANGLE; );
} if (resized.width !== 0 && resized.height !== 0) {
if (angle >= 2 * Math.PI) { mutateElement(element, {
angle -= 2 * Math.PI; ...resized,
} ...(isLinearElement(element)
mutateElement(element, { angle }); ? {
break; points: rescalePoints(
0,
resized.width,
rescalePoints(1, resized.height, element.points),
),
}
: {}),
});
} }
} }
@ -359,15 +238,12 @@ export function resizeElements(
return true; return true;
} else if (selectedElements.length > 1) { } else if (selectedElements.length > 1) {
const [x1, y1, x2, y2] = getCommonBounds(selectedElements); const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
const handleOffset = 4 / appState.zoom; // XXX import constant
const dashedLinePadding = 4 / appState.zoom; // XXX import constant
const minSize = handleOffset * 4;
const minScale = Math.max(minSize / (x2 - x1), minSize / (y2 - y1)); const minScale = Math.max(minSize / (x2 - x1), minSize / (y2 - y1));
switch (resizeHandle) { switch (resizeHandle) {
case "se": { case "se": {
const scale = Math.max( const scale = Math.max(
(x - handleOffset - dashedLinePadding - x1) / (x2 - x1), (xPointer - offsetPointer - x1) / (x2 - x1),
(y - handleOffset - dashedLinePadding - y1) / (y2 - y1), (yPointer - offsetPointer - y1) / (y2 - y1),
); );
if (scale > minScale) { if (scale > minScale) {
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
@ -382,8 +258,8 @@ export function resizeElements(
} }
case "nw": { case "nw": {
const scale = Math.max( const scale = Math.max(
(x2 - handleOffset - dashedLinePadding - x) / (x2 - x1), (x2 - offsetPointer - xPointer) / (x2 - x1),
(y2 - handleOffset - dashedLinePadding - y) / (y2 - y1), (y2 - offsetPointer - yPointer) / (y2 - y1),
); );
if (scale > minScale) { if (scale > minScale) {
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
@ -398,8 +274,8 @@ export function resizeElements(
} }
case "ne": { case "ne": {
const scale = Math.max( const scale = Math.max(
(x - handleOffset - dashedLinePadding - x1) / (x2 - x1), (xPointer - offsetPointer - x1) / (x2 - x1),
(y2 - handleOffset - dashedLinePadding - y) / (y2 - y1), (y2 - offsetPointer - yPointer) / (y2 - y1),
); );
if (scale > minScale) { if (scale > minScale) {
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
@ -414,8 +290,8 @@ export function resizeElements(
} }
case "sw": { case "sw": {
const scale = Math.max( const scale = Math.max(
(x2 - handleOffset - dashedLinePadding - x) / (x2 - x1), (x2 - offsetPointer - xPointer) / (x2 - x1),
(y - handleOffset - dashedLinePadding - y1) / (y2 - y1), (yPointer - offsetPointer - y1) / (y2 - y1),
); );
if (scale > minScale) { if (scale > minScale) {
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
@ -431,12 +307,12 @@ export function resizeElements(
} }
} }
return false; return false;
} };
export function canResizeMutlipleElements( export const canResizeMutlipleElements = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
) { ) => {
return elements.every((element) => return elements.every((element) =>
["rectangle", "diamond", "ellipse"].includes(element.type), ["rectangle", "diamond", "ellipse"].includes(element.type),
); );
} };

15
src/math.test.ts Normal file
View File

@ -0,0 +1,15 @@
import { rotate } from "./math";
describe("rotate", () => {
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
const x1 = 10;
const y1 = 20;
const x2 = 20;
const y2 = 30;
const angle = Math.PI / 2;
const [rotatedX, rotatedY] = rotate(x1, y1, x2, y2, angle);
expect([rotatedX, rotatedY]).toEqual([30, 20]);
const res2 = rotate(rotatedX, rotatedY, x2, y2, -angle);
expect(res2).toEqual([x1, x2]);
});
});

View File

@ -56,32 +56,92 @@ export function rotate(
]; ];
} }
export function adjustXYWithRotation( const adjustXYWithRotation = (
side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se", side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
position: { x: number; y: number }, x: number,
y: number,
angle: number,
deltaX: number, deltaX: number,
deltaY: number, deltaY: number,
angle: number, ) => {
) { const cos = Math.cos(angle);
let { x, y } = position; const sin = Math.sin(angle);
deltaX /= 2;
deltaY /= 2;
if (side === "e" || side === "ne" || side === "se") { if (side === "e" || side === "ne" || side === "se") {
x -= (deltaX / 2) * (1 - Math.cos(angle)); x += deltaX * (1 - cos);
y -= (deltaX / 2) * -Math.sin(angle); y += deltaX * -sin;
} }
if (side === "s" || side === "sw" || side === "se") { if (side === "s" || side === "sw" || side === "se") {
x -= (deltaY / 2) * Math.sin(angle); x += deltaY * sin;
y -= (deltaY / 2) * (1 - Math.cos(angle)); y += deltaY * (1 - cos);
} }
if (side === "w" || side === "nw" || side === "sw") { if (side === "w" || side === "nw" || side === "sw") {
x += (deltaX / 2) * (1 + Math.cos(angle)); x += deltaX * (1 + cos);
y += (deltaX / 2) * Math.sin(angle); y += deltaX * sin;
} }
if (side === "n" || side === "nw" || side === "ne") { if (side === "n" || side === "nw" || side === "ne") {
x += (deltaY / 2) * -Math.sin(angle); x += deltaY * -sin;
y += (deltaY / 2) * (1 + Math.cos(angle)); y += deltaY * (1 + cos);
} }
return { x, y }; return { x, y };
} };
export const resizeXYWidthHightWithRotation = (
side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
x: number,
y: number,
width: number,
height: number,
offsetX: number,
offsetY: number,
angle: number,
xPointer: number,
yPointer: number,
offsetPointer: number,
sidesWithSameLength: boolean,
) => {
// center point for rotation
const cx = x + width / 2;
const cy = y + height / 2;
// rotation with current angle
const [rotatedX, rotatedY] = rotate(xPointer, yPointer, cx, cy, -angle);
let scaleX = 1;
let scaleY = 1;
if (side === "e" || side === "ne" || side === "se") {
scaleX = (rotatedX - offsetPointer - x) / width;
}
if (side === "s" || side === "sw" || side === "se") {
scaleY = (rotatedY - offsetPointer - y) / height;
}
if (side === "w" || side === "nw" || side === "sw") {
scaleX = (x + width - offsetPointer - rotatedX) / width;
}
if (side === "n" || side === "nw" || side === "ne") {
scaleY = (y + height - offsetPointer - rotatedY) / height;
}
let nextWidth = width * scaleX;
let nextHeight = height * scaleY;
if (sidesWithSameLength) {
nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
}
return {
width: nextWidth,
height: nextHeight,
...adjustXYWithRotation(
side,
x - offsetX,
y - offsetY,
angle,
width - nextWidth,
height - nextHeight,
),
};
};
export const getPointOnAPath = (point: Point, path: Point[]) => { export const getPointOnAPath = (point: Point, path: Point[]) => {
const [px, py] = point; const [px, py] = point;