Enhance aspect ratio tools | Rectangle, Diamond, Ellipses (#2439)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
4c90ea5667
commit
aa221837fc
@ -164,6 +164,7 @@ import {
|
|||||||
shouldEnableBindingForPointerEvent,
|
shouldEnableBindingForPointerEvent,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||||
|
import { deepCopyElement } from "../element/newElement";
|
||||||
import { renderSpreadsheet } from "../charts";
|
import { renderSpreadsheet } from "../charts";
|
||||||
import { isValidLibrary } from "../data/json";
|
import { isValidLibrary } from "../data/json";
|
||||||
import { getNewZoom } from "../scene/zoom";
|
import { getNewZoom } from "../scene/zoom";
|
||||||
@ -206,8 +207,7 @@ export type PointerDownState = Readonly<{
|
|||||||
// The previous pointer position
|
// The previous pointer position
|
||||||
lastCoords: { x: number; y: number };
|
lastCoords: { x: number; y: number };
|
||||||
// map of original elements data
|
// map of original elements data
|
||||||
// (for now only a subset of props for perf reasons)
|
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
||||||
originalElements: Map<string, Pick<ExcalidrawElement, "x" | "y" | "angle">>;
|
|
||||||
resize: {
|
resize: {
|
||||||
// Handle when resizing, might change during the pointer interaction
|
// Handle when resizing, might change during the pointer interaction
|
||||||
handleType: MaybeTransformHandleType;
|
handleType: MaybeTransformHandleType;
|
||||||
@ -246,6 +246,10 @@ export type PointerDownState = Readonly<{
|
|||||||
onMove: null | ((event: PointerEvent) => void);
|
onMove: null | ((event: PointerEvent) => void);
|
||||||
// It's defined on the initial pointer down event
|
// It's defined on the initial pointer down event
|
||||||
onUp: null | ((event: PointerEvent) => void);
|
onUp: null | ((event: PointerEvent) => void);
|
||||||
|
// It's defined on the initial pointer down event
|
||||||
|
onKeyDown: null | ((event: KeyboardEvent) => void);
|
||||||
|
// It's defined on the initial pointer down event
|
||||||
|
onKeyUp: null | ((event: KeyboardEvent) => void);
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@ -2002,12 +2006,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
pointerDownState,
|
pointerDownState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onKeyDown = this.onKeyDownFromPointerDownHandler(pointerDownState);
|
||||||
|
const onKeyUp = this.onKeyUpFromPointerDownHandler(pointerDownState);
|
||||||
|
|
||||||
lastPointerUp = onPointerUp;
|
lastPointerUp = onPointerUp;
|
||||||
|
|
||||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||||
|
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
||||||
|
window.addEventListener(EVENT.KEYUP, onKeyUp);
|
||||||
pointerDownState.eventListeners.onMove = onPointerMove;
|
pointerDownState.eventListeners.onMove = onPointerMove;
|
||||||
pointerDownState.eventListeners.onUp = onPointerUp;
|
pointerDownState.eventListeners.onUp = onPointerUp;
|
||||||
|
pointerDownState.eventListeners.onKeyUp = onKeyUp;
|
||||||
|
pointerDownState.eventListeners.onKeyDown = onKeyDown;
|
||||||
};
|
};
|
||||||
|
|
||||||
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
|
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
|
||||||
@ -2182,11 +2193,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
// we need to duplicate because we'll be updating this state
|
// we need to duplicate because we'll be updating this state
|
||||||
lastCoords: { ...origin },
|
lastCoords: { ...origin },
|
||||||
originalElements: this.scene.getElements().reduce((acc, element) => {
|
originalElements: this.scene.getElements().reduce((acc, element) => {
|
||||||
acc.set(element.id, {
|
acc.set(element.id, deepCopyElement(element));
|
||||||
x: element.x,
|
|
||||||
y: element.y,
|
|
||||||
angle: element.angle,
|
|
||||||
});
|
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map() as PointerDownState["originalElements"]),
|
}, new Map() as PointerDownState["originalElements"]),
|
||||||
resize: {
|
resize: {
|
||||||
@ -2213,6 +2220,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
eventListeners: {
|
eventListeners: {
|
||||||
onMove: null,
|
onMove: null,
|
||||||
onUp: null,
|
onUp: null,
|
||||||
|
onKeyUp: null,
|
||||||
|
onKeyDown: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -2614,6 +2623,30 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onKeyDownFromPointerDownHandler(
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
): (event: KeyboardEvent) => void {
|
||||||
|
return withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
|
if (this.maybeHandleResize(pointerDownState, event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyUpFromPointerDownHandler(
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
): (event: KeyboardEvent) => void {
|
||||||
|
return withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
|
// Prevents focus from escaping excalidraw tab
|
||||||
|
event.key === KEYS.ALT && event.preventDefault();
|
||||||
|
if (this.maybeHandleResize(pointerDownState, event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private onPointerMoveFromPointerDownHandler(
|
private onPointerMoveFromPointerDownHandler(
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): (event: PointerEvent) => void {
|
): (event: PointerEvent) => void {
|
||||||
@ -2670,43 +2703,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pointerDownState.resize.isResizing) {
|
if (pointerDownState.resize.isResizing) {
|
||||||
const selectedElements = getSelectedElements(
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
this.scene.getElements(),
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
this.state,
|
if (this.maybeHandleResize(pointerDownState, event)) {
|
||||||
);
|
return true;
|
||||||
const transformHandleType = pointerDownState.resize.handleType;
|
|
||||||
this.setState({
|
|
||||||
// TODO: rename this state field to "isScaling" to distinguish
|
|
||||||
// it from the generic "isResizing" which includes scaling and
|
|
||||||
// rotating
|
|
||||||
isResizing: transformHandleType && transformHandleType !== "rotation",
|
|
||||||
isRotating: transformHandleType === "rotation",
|
|
||||||
});
|
|
||||||
const [resizeX, resizeY] = getGridPoint(
|
|
||||||
pointerCoords.x - pointerDownState.resize.offset.x,
|
|
||||||
pointerCoords.y - pointerDownState.resize.offset.y,
|
|
||||||
this.state.gridSize,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
transformElements(
|
|
||||||
pointerDownState,
|
|
||||||
transformHandleType,
|
|
||||||
(newTransformHandle) => {
|
|
||||||
pointerDownState.resize.handleType = newTransformHandle;
|
|
||||||
},
|
|
||||||
selectedElements,
|
|
||||||
pointerDownState.resize.arrowDirection,
|
|
||||||
getRotateWithDiscreteAngleKey(event),
|
|
||||||
getResizeWithSidesSameLengthKey(event),
|
|
||||||
getResizeCenterPointKey(event),
|
|
||||||
resizeX,
|
|
||||||
resizeY,
|
|
||||||
pointerDownState.resize.center.x,
|
|
||||||
pointerDownState.resize.center.y,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.maybeSuggestBindingForAll(selectedElements);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2881,33 +2881,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.state.startBoundElement,
|
this.state.startBoundElement,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (draggingElement.type === "selection") {
|
|
||||||
dragNewElement(
|
|
||||||
draggingElement,
|
|
||||||
this.state.elementType,
|
|
||||||
pointerDownState.origin.x,
|
|
||||||
pointerDownState.origin.y,
|
|
||||||
pointerCoords.x,
|
|
||||||
pointerCoords.y,
|
|
||||||
distance(pointerDownState.origin.x, pointerCoords.x),
|
|
||||||
distance(pointerDownState.origin.y, pointerCoords.y),
|
|
||||||
getResizeWithSidesSameLengthKey(event),
|
|
||||||
getResizeCenterPointKey(event),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
dragNewElement(
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
draggingElement,
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
this.state.elementType,
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||||
pointerDownState.originInGrid.x,
|
|
||||||
pointerDownState.originInGrid.y,
|
|
||||||
gridX,
|
|
||||||
gridY,
|
|
||||||
distance(pointerDownState.originInGrid.x, gridX),
|
|
||||||
distance(pointerDownState.originInGrid.y, gridY),
|
|
||||||
getResizeWithSidesSameLengthKey(event),
|
|
||||||
getResizeCenterPointKey(event),
|
|
||||||
);
|
|
||||||
this.maybeSuggestBindingForAll([draggingElement]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
@ -3029,6 +3006,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
EVENT.POINTER_UP,
|
EVENT.POINTER_UP,
|
||||||
pointerDownState.eventListeners.onUp!,
|
pointerDownState.eventListeners.onUp!,
|
||||||
);
|
);
|
||||||
|
window.removeEventListener(
|
||||||
|
EVENT.KEYDOWN,
|
||||||
|
pointerDownState.eventListeners.onKeyDown!,
|
||||||
|
);
|
||||||
|
window.removeEventListener(
|
||||||
|
EVENT.KEYUP,
|
||||||
|
pointerDownState.eventListeners.onKeyUp!,
|
||||||
|
);
|
||||||
|
|
||||||
if (draggingElement?.type === "draw") {
|
if (draggingElement?.type === "draw") {
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
@ -3451,6 +3436,96 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
this.openContextMenu(event);
|
this.openContextMenu(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private maybeDragNewGenericElement = (
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
event: MouseEvent | KeyboardEvent,
|
||||||
|
): void => {
|
||||||
|
const draggingElement = this.state.draggingElement;
|
||||||
|
const pointerCoords = pointerDownState.lastCoords;
|
||||||
|
if (!draggingElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (draggingElement.type === "selection") {
|
||||||
|
dragNewElement(
|
||||||
|
draggingElement,
|
||||||
|
this.state.elementType,
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
pointerCoords.x,
|
||||||
|
pointerCoords.y,
|
||||||
|
distance(pointerDownState.origin.x, pointerCoords.x),
|
||||||
|
distance(pointerDownState.origin.y, pointerCoords.y),
|
||||||
|
getResizeWithSidesSameLengthKey(event),
|
||||||
|
getResizeCenterPointKey(event),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
pointerCoords.x,
|
||||||
|
pointerCoords.y,
|
||||||
|
this.state.gridSize,
|
||||||
|
);
|
||||||
|
dragNewElement(
|
||||||
|
draggingElement,
|
||||||
|
this.state.elementType,
|
||||||
|
pointerDownState.originInGrid.x,
|
||||||
|
pointerDownState.originInGrid.y,
|
||||||
|
gridX,
|
||||||
|
gridY,
|
||||||
|
distance(pointerDownState.originInGrid.x, gridX),
|
||||||
|
distance(pointerDownState.originInGrid.y, gridY),
|
||||||
|
getResizeWithSidesSameLengthKey(event),
|
||||||
|
getResizeCenterPointKey(event),
|
||||||
|
);
|
||||||
|
this.maybeSuggestBindingForAll([draggingElement]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private maybeHandleResize = (
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
event: MouseEvent | KeyboardEvent,
|
||||||
|
): boolean => {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
this.scene.getElements(),
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
const transformHandleType = pointerDownState.resize.handleType;
|
||||||
|
this.setState({
|
||||||
|
// TODO: rename this state field to "isScaling" to distinguish
|
||||||
|
// it from the generic "isResizing" which includes scaling and
|
||||||
|
// rotating
|
||||||
|
isResizing: transformHandleType && transformHandleType !== "rotation",
|
||||||
|
isRotating: transformHandleType === "rotation",
|
||||||
|
});
|
||||||
|
const pointerCoords = pointerDownState.lastCoords;
|
||||||
|
const [resizeX, resizeY] = getGridPoint(
|
||||||
|
pointerCoords.x - pointerDownState.resize.offset.x,
|
||||||
|
pointerCoords.y - pointerDownState.resize.offset.y,
|
||||||
|
this.state.gridSize,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
transformElements(
|
||||||
|
pointerDownState,
|
||||||
|
transformHandleType,
|
||||||
|
(newTransformHandle) => {
|
||||||
|
pointerDownState.resize.handleType = newTransformHandle;
|
||||||
|
},
|
||||||
|
selectedElements,
|
||||||
|
pointerDownState.resize.arrowDirection,
|
||||||
|
getRotateWithDiscreteAngleKey(event),
|
||||||
|
getResizeCenterPointKey(event),
|
||||||
|
getResizeWithSidesSameLengthKey(event),
|
||||||
|
resizeX,
|
||||||
|
resizeY,
|
||||||
|
pointerDownState.resize.center.x,
|
||||||
|
pointerDownState.resize.center.y,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.maybeSuggestBindingForAll(selectedElements);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
private openContextMenu = ({
|
private openContextMenu = ({
|
||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
|
import {
|
||||||
|
rotate,
|
||||||
|
adjustXYWithRotation,
|
||||||
|
getFlipAdjustment,
|
||||||
|
centerPoint,
|
||||||
|
rotatePoint,
|
||||||
|
} from "../math";
|
||||||
import {
|
import {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
ExcalidrawGenericElement,
|
||||||
|
ExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isGenericElement, isLinearElement, isTextElement } from "./typeChecks";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import {
|
import {
|
||||||
@ -25,8 +33,10 @@ import { updateBoundElements } from "./binding";
|
|||||||
import {
|
import {
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
|
TransformHandleDirection,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { PointerDownState } from "../components/App";
|
import { PointerDownState } from "../components/App";
|
||||||
|
import { Point } from "../types";
|
||||||
|
|
||||||
const normalizeAngle = (angle: number): number => {
|
const normalizeAngle = (angle: number): number => {
|
||||||
if (angle >= 2 * Math.PI) {
|
if (angle >= 2 * Math.PI) {
|
||||||
@ -43,8 +53,8 @@ export const transformElements = (
|
|||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
resizeArrowDirection: "origin" | "end",
|
resizeArrowDirection: "origin" | "end",
|
||||||
isRotateWithDiscreteAngle: boolean,
|
isRotateWithDiscreteAngle: boolean,
|
||||||
isResizeWithSidesSameLength: boolean,
|
|
||||||
isResizeCenterPoint: boolean,
|
isResizeCenterPoint: boolean,
|
||||||
|
shouldKeepSidesRatio: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
centerX: number,
|
centerX: number,
|
||||||
@ -76,7 +86,7 @@ export const transformElements = (
|
|||||||
pointerY,
|
pointerY,
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
element.type === "text" &&
|
isTextElement(element) &&
|
||||||
(transformHandleType === "nw" ||
|
(transformHandleType === "nw" ||
|
||||||
transformHandleType === "ne" ||
|
transformHandleType === "ne" ||
|
||||||
transformHandleType === "sw" ||
|
transformHandleType === "sw" ||
|
||||||
@ -91,22 +101,35 @@ export const transformElements = (
|
|||||||
);
|
);
|
||||||
updateBoundElements(element);
|
updateBoundElements(element);
|
||||||
} else if (transformHandleType) {
|
} else if (transformHandleType) {
|
||||||
resizeSingleElement(
|
if (isGenericElement(element)) {
|
||||||
element,
|
resizeSingleGenericElement(
|
||||||
transformHandleType,
|
pointerDownState.originalElements.get(element.id) as typeof element,
|
||||||
isResizeWithSidesSameLength,
|
shouldKeepSidesRatio,
|
||||||
isResizeCenterPoint,
|
element,
|
||||||
pointerX,
|
transformHandleType,
|
||||||
pointerY,
|
isResizeCenterPoint,
|
||||||
);
|
pointerX,
|
||||||
setTransformHandle(
|
pointerY,
|
||||||
normalizeTransformHandleType(element, transformHandleType),
|
);
|
||||||
);
|
} else {
|
||||||
if (element.width < 0) {
|
const keepSquareAspectRatio = shouldKeepSidesRatio;
|
||||||
mutateElement(element, { width: -element.width });
|
resizeSingleNonGenericElement(
|
||||||
}
|
element,
|
||||||
if (element.height < 0) {
|
transformHandleType,
|
||||||
mutateElement(element, { height: -element.height });
|
isResizeCenterPoint,
|
||||||
|
keepSquareAspectRatio,
|
||||||
|
pointerX,
|
||||||
|
pointerY,
|
||||||
|
);
|
||||||
|
setTransformHandle(
|
||||||
|
normalizeTransformHandleType(element, transformHandleType),
|
||||||
|
);
|
||||||
|
if (element.width < 0) {
|
||||||
|
mutateElement(element, { width: -element.width });
|
||||||
|
}
|
||||||
|
if (element.height < 0) {
|
||||||
|
mutateElement(element, { height: -element.height });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,17 +414,153 @@ const resizeSingleTextElement = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resizeSingleElement = (
|
const resizeSingleGenericElement = (
|
||||||
|
stateAtResizeStart: NonDeleted<ExcalidrawGenericElement>,
|
||||||
|
shouldKeepSidesRatio: boolean,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
transformHandleType: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
transformHandleDirection: TransformHandleDirection,
|
||||||
sidesWithSameLength: boolean,
|
|
||||||
isResizeFromCenter: boolean,
|
isResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
|
) => {
|
||||||
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(stateAtResizeStart);
|
||||||
|
const startTopLeft: Point = [x1, y1];
|
||||||
|
const startBottomRight: Point = [x2, y2];
|
||||||
|
const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
|
||||||
|
|
||||||
|
// Calculate new dimensions based on cursor position
|
||||||
|
let newWidth = stateAtResizeStart.width;
|
||||||
|
let newHeight = stateAtResizeStart.height;
|
||||||
|
const rotatedPointer = rotatePoint(
|
||||||
|
[pointerX, pointerY],
|
||||||
|
startCenter,
|
||||||
|
-stateAtResizeStart.angle,
|
||||||
|
);
|
||||||
|
if (transformHandleDirection.includes("e")) {
|
||||||
|
newWidth = rotatedPointer[0] - startTopLeft[0];
|
||||||
|
}
|
||||||
|
if (transformHandleDirection.includes("s")) {
|
||||||
|
newHeight = rotatedPointer[1] - startTopLeft[1];
|
||||||
|
}
|
||||||
|
if (transformHandleDirection.includes("w")) {
|
||||||
|
newWidth = startBottomRight[0] - rotatedPointer[0];
|
||||||
|
}
|
||||||
|
if (transformHandleDirection.includes("n")) {
|
||||||
|
newHeight = startBottomRight[1] - rotatedPointer[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust dimensions for resizing from center
|
||||||
|
if (isResizeFromCenter) {
|
||||||
|
newWidth = 2 * newWidth - stateAtResizeStart.width;
|
||||||
|
newHeight = 2 * newHeight - stateAtResizeStart.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust dimensions to keep sides ratio
|
||||||
|
if (shouldKeepSidesRatio) {
|
||||||
|
const widthRatio = Math.abs(newWidth) / stateAtResizeStart.width;
|
||||||
|
const heightRatio = Math.abs(newHeight) / stateAtResizeStart.height;
|
||||||
|
if (transformHandleDirection.length === 1) {
|
||||||
|
newHeight *= widthRatio;
|
||||||
|
newWidth *= heightRatio;
|
||||||
|
}
|
||||||
|
if (transformHandleDirection.length === 2) {
|
||||||
|
const ratio = Math.max(widthRatio, heightRatio);
|
||||||
|
newWidth = stateAtResizeStart.width * ratio * Math.sign(newWidth);
|
||||||
|
newHeight = stateAtResizeStart.height * ratio * Math.sign(newHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new topLeft based on fixed corner during resize
|
||||||
|
let newTopLeft = startTopLeft as [number, number];
|
||||||
|
if (["n", "w", "nw"].includes(transformHandleDirection)) {
|
||||||
|
newTopLeft = [
|
||||||
|
startBottomRight[0] - Math.abs(newWidth),
|
||||||
|
startBottomRight[1] - Math.abs(newHeight),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (transformHandleDirection === "ne") {
|
||||||
|
const bottomLeft = [
|
||||||
|
stateAtResizeStart.x,
|
||||||
|
stateAtResizeStart.y + stateAtResizeStart.height,
|
||||||
|
];
|
||||||
|
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newHeight)];
|
||||||
|
}
|
||||||
|
if (transformHandleDirection === "sw") {
|
||||||
|
const topRight = [
|
||||||
|
stateAtResizeStart.x + stateAtResizeStart.width,
|
||||||
|
stateAtResizeStart.y,
|
||||||
|
];
|
||||||
|
newTopLeft = [topRight[0] - Math.abs(newWidth), topRight[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keeps opposite handle fixed during resize
|
||||||
|
if (shouldKeepSidesRatio) {
|
||||||
|
if (["s", "n"].includes(transformHandleDirection)) {
|
||||||
|
newTopLeft[0] = startCenter[0] - newWidth / 2;
|
||||||
|
}
|
||||||
|
if (["e", "w"].includes(transformHandleDirection)) {
|
||||||
|
newTopLeft[1] = startCenter[1] - newHeight / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip horizontally
|
||||||
|
if (newWidth < 0) {
|
||||||
|
if (transformHandleDirection.includes("e")) {
|
||||||
|
newTopLeft[0] -= Math.abs(newWidth);
|
||||||
|
}
|
||||||
|
if (transformHandleDirection.includes("w")) {
|
||||||
|
newTopLeft[0] += Math.abs(newWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Flip vertically
|
||||||
|
if (newHeight < 0) {
|
||||||
|
if (transformHandleDirection.includes("s")) {
|
||||||
|
newTopLeft[1] -= Math.abs(newHeight);
|
||||||
|
}
|
||||||
|
if (transformHandleDirection.includes("n")) {
|
||||||
|
newTopLeft[1] += Math.abs(newHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResizeFromCenter) {
|
||||||
|
newTopLeft[0] = startCenter[0] - Math.abs(newWidth) / 2;
|
||||||
|
newTopLeft[1] = startCenter[1] - Math.abs(newHeight) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust topLeft to new rotation point
|
||||||
|
const angle = stateAtResizeStart.angle;
|
||||||
|
const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
|
||||||
|
const newCenter: Point = [
|
||||||
|
newTopLeft[0] + Math.abs(newWidth) / 2,
|
||||||
|
newTopLeft[1] + Math.abs(newHeight) / 2,
|
||||||
|
];
|
||||||
|
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
||||||
|
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
||||||
|
|
||||||
|
const resizedElement = {
|
||||||
|
width: Math.abs(newWidth),
|
||||||
|
height: Math.abs(newHeight),
|
||||||
|
x: newTopLeft[0],
|
||||||
|
y: newTopLeft[1],
|
||||||
|
};
|
||||||
|
updateBoundElements(element, {
|
||||||
|
newSize: { width: resizedElement.width, height: resizedElement.height },
|
||||||
|
});
|
||||||
|
mutateElement(element, resizedElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeSingleNonGenericElement = (
|
||||||
|
element: NonDeleted<Exclude<ExcalidrawElement, ExcalidrawGenericElement>>,
|
||||||
|
transformHandleType: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
||||||
|
isResizeFromCenter: boolean,
|
||||||
|
keepSquareAspectRatio: boolean,
|
||||||
|
pointerX: number,
|
||||||
|
pointerY: 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(
|
||||||
pointerX,
|
pointerX,
|
||||||
@ -410,6 +569,7 @@ const resizeSingleElement = (
|
|||||||
cy,
|
cy,
|
||||||
-element.angle,
|
-element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
let scaleX = 1;
|
let scaleX = 1;
|
||||||
let scaleY = 1;
|
let scaleY = 1;
|
||||||
if (
|
if (
|
||||||
@ -442,9 +602,10 @@ const resizeSingleElement = (
|
|||||||
}
|
}
|
||||||
let nextWidth = element.width * scaleX;
|
let nextWidth = element.width * scaleX;
|
||||||
let nextHeight = element.height * scaleY;
|
let nextHeight = element.height * scaleY;
|
||||||
if (sidesWithSameLength) {
|
if (keepSquareAspectRatio) {
|
||||||
nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
|
nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
nextWidth,
|
nextWidth,
|
||||||
@ -454,7 +615,9 @@ const resizeSingleElement = (
|
|||||||
const deltaY1 = (y1 - nextY1) / 2;
|
const deltaY1 = (y1 - nextY1) / 2;
|
||||||
const deltaX2 = (x2 - nextX2) / 2;
|
const deltaX2 = (x2 - nextX2) / 2;
|
||||||
const deltaY2 = (y2 - nextY2) / 2;
|
const deltaY2 = (y2 - nextY2) / 2;
|
||||||
|
|
||||||
const rescaledPoints = rescalePointsInElement(element, nextWidth, nextHeight);
|
const rescaledPoints = rescalePointsInElement(element, nextWidth, nextHeight);
|
||||||
|
|
||||||
updateBoundElements(element, {
|
updateBoundElements(element, {
|
||||||
newSize: { width: nextWidth, height: nextHeight },
|
newSize: { width: nextWidth, height: nextHeight },
|
||||||
});
|
});
|
||||||
@ -491,6 +654,7 @@ const resizeSingleElement = (
|
|||||||
deltaX2,
|
deltaX2,
|
||||||
deltaY2,
|
deltaY2,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nextWidth !== 0 &&
|
nextWidth !== 0 &&
|
||||||
nextHeight !== 0 &&
|
nextHeight !== 0 &&
|
||||||
|
@ -4,7 +4,7 @@ import { getElementAbsoluteCoords, Bounds } from "./bounds";
|
|||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { Zoom } from "../types";
|
import { Zoom } from "../types";
|
||||||
|
|
||||||
export type TransformHandleType =
|
export type TransformHandleDirection =
|
||||||
| "n"
|
| "n"
|
||||||
| "s"
|
| "s"
|
||||||
| "w"
|
| "w"
|
||||||
@ -12,8 +12,9 @@ export type TransformHandleType =
|
|||||||
| "nw"
|
| "nw"
|
||||||
| "ne"
|
| "ne"
|
||||||
| "sw"
|
| "sw"
|
||||||
| "se"
|
| "se";
|
||||||
| "rotation";
|
|
||||||
|
export type TransformHandleType = TransformHandleDirection | "rotation";
|
||||||
|
|
||||||
export type TransformHandle = [number, number, number, number];
|
export type TransformHandle = [number, number, number, number];
|
||||||
export type TransformHandles = Partial<
|
export type TransformHandles = Partial<
|
||||||
|
@ -3,8 +3,21 @@ import {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawGenericElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
export const isGenericElement = (
|
||||||
|
element: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawGenericElement => {
|
||||||
|
return (
|
||||||
|
element != null &&
|
||||||
|
(element.type === "selection" ||
|
||||||
|
element.type === "rectangle" ||
|
||||||
|
element.type === "diamond" ||
|
||||||
|
element.type === "ellipse")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isTextElement = (
|
export const isTextElement = (
|
||||||
element: ExcalidrawElement | null,
|
element: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawTextElement => {
|
): element is ExcalidrawTextElement => {
|
||||||
|
11
src/keys.ts
11
src/keys.ts
@ -26,6 +26,7 @@ export const KEYS = {
|
|||||||
ARROW_RIGHT: "ArrowRight",
|
ARROW_RIGHT: "ArrowRight",
|
||||||
ARROW_UP: "ArrowUp",
|
ARROW_UP: "ArrowUp",
|
||||||
BACKSPACE: "Backspace",
|
BACKSPACE: "Backspace",
|
||||||
|
ALT: "Alt",
|
||||||
CTRL_OR_CMD: isDarwin ? "metaKey" : "ctrlKey",
|
CTRL_OR_CMD: isDarwin ? "metaKey" : "ctrlKey",
|
||||||
DELETE: "Delete",
|
DELETE: "Delete",
|
||||||
ENTER: "Enter",
|
ENTER: "Enter",
|
||||||
@ -59,8 +60,10 @@ export const isArrowKey = (key: string) =>
|
|||||||
export const getResizeCenterPointKey = (event: MouseEvent | KeyboardEvent) =>
|
export const getResizeCenterPointKey = (event: MouseEvent | KeyboardEvent) =>
|
||||||
event.altKey;
|
event.altKey;
|
||||||
|
|
||||||
export const getResizeWithSidesSameLengthKey = (event: MouseEvent) =>
|
export const getResizeWithSidesSameLengthKey = (
|
||||||
event.shiftKey;
|
event: MouseEvent | KeyboardEvent,
|
||||||
|
) => event.shiftKey;
|
||||||
|
|
||||||
export const getRotateWithDiscreteAngleKey = (event: MouseEvent) =>
|
export const getRotateWithDiscreteAngleKey = (
|
||||||
event.shiftKey;
|
event: MouseEvent | KeyboardEvent,
|
||||||
|
) => event.shiftKey;
|
||||||
|
@ -17,6 +17,12 @@ export const rotate = (
|
|||||||
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
|
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const rotatePoint = (
|
||||||
|
point: Point,
|
||||||
|
center: Point,
|
||||||
|
angle: number,
|
||||||
|
): [number, number] => rotate(point[0], point[1], center[0], center[1], angle);
|
||||||
|
|
||||||
export const adjustXYWithRotation = (
|
export const adjustXYWithRotation = (
|
||||||
sides: {
|
sides: {
|
||||||
n?: boolean;
|
n?: boolean;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,27 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`resize element rectangle 1`] = `
|
|
||||||
Object {
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElementIds": null,
|
|
||||||
"fillStyle": "hachure",
|
|
||||||
"groupIds": Array [],
|
|
||||||
"height": 50,
|
|
||||||
"id": "id0",
|
|
||||||
"isDeleted": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"seed": 337897,
|
|
||||||
"strokeColor": "#000000",
|
|
||||||
"strokeSharpness": "sharp",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 1,
|
|
||||||
"type": "rectangle",
|
|
||||||
"version": 3,
|
|
||||||
"versionNonce": 401146281,
|
|
||||||
"width": 30,
|
|
||||||
"x": 29,
|
|
||||||
"y": 47,
|
|
||||||
}
|
|
||||||
`;
|
|
@ -14,11 +14,13 @@ let altKey = false;
|
|||||||
let shiftKey = false;
|
let shiftKey = false;
|
||||||
let ctrlKey = false;
|
let ctrlKey = false;
|
||||||
|
|
||||||
|
export type KeyboardModifiers = {
|
||||||
|
alt?: boolean;
|
||||||
|
shift?: boolean;
|
||||||
|
ctrl?: boolean;
|
||||||
|
};
|
||||||
export class Keyboard {
|
export class Keyboard {
|
||||||
static withModifierKeys = (
|
static withModifierKeys = (modifiers: KeyboardModifiers, cb: () => void) => {
|
||||||
modifiers: { alt?: boolean; shift?: boolean; ctrl?: boolean },
|
|
||||||
cb: () => void,
|
|
||||||
) => {
|
|
||||||
const prevAltKey = altKey;
|
const prevAltKey = altKey;
|
||||||
const prevShiftKey = shiftKey;
|
const prevShiftKey = shiftKey;
|
||||||
const prevCtrlKey = ctrlKey;
|
const prevCtrlKey = ctrlKey;
|
||||||
|
@ -13,7 +13,6 @@ import Excalidraw from "../packages/excalidraw/index";
|
|||||||
import { setLanguage } from "../i18n";
|
import { setLanguage } from "../i18n";
|
||||||
import { setDateTimeForTests } from "../utils";
|
import { setDateTimeForTests } from "../utils";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getTransformHandles as _getTransformHandles } from "../element";
|
|
||||||
import { queryByText } from "@testing-library/react";
|
import { queryByText } from "@testing-library/react";
|
||||||
import { copiedStyles } from "../actions/actionStyles";
|
import { copiedStyles } from "../actions/actionStyles";
|
||||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||||
@ -44,27 +43,6 @@ const clickLabeledElement = (label: string) => {
|
|||||||
fireEvent.click(element);
|
fireEvent.click(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
type HandlerRectanglesRet = keyof ReturnType<typeof _getTransformHandles>;
|
|
||||||
const getTransformHandles = (pointerType: "mouse" | "touch" | "pen") => {
|
|
||||||
const rects = _getTransformHandles(
|
|
||||||
API.getSelectedElement(),
|
|
||||||
h.state.zoom,
|
|
||||||
pointerType,
|
|
||||||
) as {
|
|
||||||
[T in HandlerRectanglesRet]: [number, number, number, number];
|
|
||||||
};
|
|
||||||
|
|
||||||
const rv: { [K in keyof typeof rects]: [number, number] } = {} as any;
|
|
||||||
|
|
||||||
for (const handlePos in rects) {
|
|
||||||
const [x, y, width, height] = rects[handlePos as keyof typeof rects];
|
|
||||||
|
|
||||||
rv[handlePos as keyof typeof rects] = [x + width / 2, y + height / 2];
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is always called at the end of your test, so usually you don't need to call it.
|
* This is always called at the end of your test, so usually you don't need to call it.
|
||||||
* However, if you have a long test, you might want to call it during the test so it's easier
|
* However, if you have a long test, you might want to call it during the test so it's easier
|
||||||
@ -204,67 +182,6 @@ describe("regression tests", () => {
|
|||||||
expect(API.getSelectedElement().strokeColor).toBe("#5f3dc4");
|
expect(API.getSelectedElement().strokeColor).toBe("#5f3dc4");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resize an element, trying every resize handle", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(10, 10);
|
|
||||||
|
|
||||||
const transformHandles = getTransformHandles("mouse");
|
|
||||||
// @ts-ignore
|
|
||||||
delete transformHandles.rotation; // exclude rotation handle
|
|
||||||
for (const handlePos in transformHandles) {
|
|
||||||
const [x, y] = transformHandles[
|
|
||||||
handlePos as keyof typeof transformHandles
|
|
||||||
];
|
|
||||||
const { width: prevWidth, height: prevHeight } = API.getSelectedElement();
|
|
||||||
mouse.restorePosition(x, y);
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(-5, -5);
|
|
||||||
|
|
||||||
const {
|
|
||||||
width: nextWidthNegative,
|
|
||||||
height: nextHeightNegative,
|
|
||||||
} = API.getSelectedElement();
|
|
||||||
expect(
|
|
||||||
prevWidth !== nextWidthNegative || prevHeight !== nextHeightNegative,
|
|
||||||
).toBeTruthy();
|
|
||||||
checkpoint(`resize handle ${handlePos} (-5, -5)`);
|
|
||||||
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(5, 5);
|
|
||||||
|
|
||||||
const { width, height } = API.getSelectedElement();
|
|
||||||
expect(width).toBe(prevWidth);
|
|
||||||
expect(height).toBe(prevHeight);
|
|
||||||
checkpoint(`unresize handle ${handlePos} (-5, -5)`);
|
|
||||||
|
|
||||||
mouse.restorePosition(x, y);
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(5, 5);
|
|
||||||
|
|
||||||
const {
|
|
||||||
width: nextWidthPositive,
|
|
||||||
height: nextHeightPositive,
|
|
||||||
} = API.getSelectedElement();
|
|
||||||
expect(
|
|
||||||
prevWidth !== nextWidthPositive || prevHeight !== nextHeightPositive,
|
|
||||||
).toBeTruthy();
|
|
||||||
checkpoint(`resize handle ${handlePos} (+5, +5)`);
|
|
||||||
|
|
||||||
mouse.down();
|
|
||||||
mouse.up(-5, -5);
|
|
||||||
|
|
||||||
const {
|
|
||||||
width: finalWidth,
|
|
||||||
height: finalHeight,
|
|
||||||
} = API.getSelectedElement();
|
|
||||||
expect(finalWidth).toBe(prevWidth);
|
|
||||||
expect(finalHeight).toBe(prevHeight);
|
|
||||||
|
|
||||||
checkpoint(`unresize handle ${handlePos} (+5, +5)`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("click on an element and drag it", () => {
|
it("click on an element and drag it", () => {
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { render, fireEvent } from "./test-utils";
|
import { render } from "./test-utils";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import App from "../components/App";
|
||||||
import * as Renderer from "../renderer/renderScene";
|
import * as Renderer from "../renderer/renderScene";
|
||||||
import { reseed } from "../random";
|
import { reseed } from "../random";
|
||||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
import { UI, Pointer, Keyboard, KeyboardModifiers } from "./helpers/ui";
|
||||||
import { getTransformHandles } from "../element/transformHandles";
|
import {
|
||||||
|
getTransformHandles,
|
||||||
|
TransformHandleDirection,
|
||||||
|
} from "../element/transformHandles";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
@ -21,70 +25,119 @@ beforeEach(() => {
|
|||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
describe("resize element", () => {
|
describe("resize rectangle ellipses and diamond elements", () => {
|
||||||
it("rectangle", async () => {
|
const elemData = {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
x: 0,
|
||||||
const canvas = container.querySelector("canvas")!;
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
};
|
||||||
|
// Value for irrelevant cursor movements
|
||||||
|
const _ = 234;
|
||||||
|
|
||||||
{
|
it.each`
|
||||||
// create element
|
handle | move | dimensions | topLeft
|
||||||
const tool = getByToolName("rectangle");
|
${"n"} | ${[_, -100]} | ${[100, 200]} | ${[elemData.x, -100]}
|
||||||
fireEvent.click(tool);
|
${"s"} | ${[_, 39]} | ${[100, 139]} | ${[elemData.x, elemData.x]}
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
${"e"} | ${[-20, _]} | ${[80, 100]} | ${[elemData.x, elemData.y]}
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
${"w"} | ${[-20, _]} | ${[120, 100]} | ${[-20, elemData.y]}
|
||||||
fireEvent.pointerUp(canvas);
|
${"ne"} | ${[10, 55]} | ${[110, 45]} | ${[elemData.x, 55]}
|
||||||
|
${"se"} | ${[-30, -10]} | ${[70, 90]} | ${[elemData.x, elemData.y]}
|
||||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
${"nw"} | ${[-300, -200]} | ${[400, 300]} | ${[-300, -200]}
|
||||||
expect(h.state.selectionElement).toBeNull();
|
${"sw"} | ${[40, -20]} | ${[60, 80]} | ${[40, 0]}
|
||||||
expect(h.elements.length).toEqual(1);
|
`("resizes with handle $handle", ({ handle, move, dimensions, topLeft }) => {
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
render(<App />);
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
const rectangle = UI.createElement("rectangle", elemData);
|
||||||
|
resize(rectangle, handle, move);
|
||||||
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
|
const element = h.elements[0];
|
||||||
|
expect([element.width, element.height]).toEqual(dimensions);
|
||||||
renderScene.mockClear();
|
expect([element.x, element.y]).toEqual(topLeft);
|
||||||
}
|
|
||||||
|
|
||||||
// select the element first
|
|
||||||
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
|
||||||
fireEvent.pointerUp(canvas);
|
|
||||||
|
|
||||||
// select a handler rectangle (top-left)
|
|
||||||
fireEvent.pointerDown(canvas, { clientX: 21, clientY: 13 });
|
|
||||||
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 });
|
|
||||||
fireEvent.pointerUp(canvas);
|
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
|
||||||
expect(h.state.selectionElement).toBeNull();
|
|
||||||
expect(h.elements.length).toEqual(1);
|
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
|
|
||||||
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
|
|
||||||
|
|
||||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
handle | move | dimensions | topLeft
|
||||||
|
${"n"} | ${[_, -100]} | ${[200, 200]} | ${[-50, -100]}
|
||||||
|
${"nw"} | ${[-300, -200]} | ${[400, 400]} | ${[-300, -300]}
|
||||||
|
${"sw"} | ${[40, -20]} | ${[80, 80]} | ${[20, 0]}
|
||||||
|
`(
|
||||||
|
"resizes with fixed side ratios on handle $handle",
|
||||||
|
({ handle, move, dimensions, topLeft }) => {
|
||||||
|
render(<App />);
|
||||||
|
const rectangle = UI.createElement("rectangle", elemData);
|
||||||
|
resize(rectangle, handle, move, { shift: true });
|
||||||
|
const element = h.elements[0];
|
||||||
|
expect([element.width, element.height]).toEqual(dimensions);
|
||||||
|
expect([element.x, element.y]).toEqual(topLeft);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
handle | move | dimensions | topLeft
|
||||||
|
${"nw"} | ${[0, 120]} | ${[100, 100]} | ${[0, 100]}
|
||||||
|
${"ne"} | ${[-120, 0]} | ${[100, 100]} | ${[-100, 0]}
|
||||||
|
${"sw"} | ${[200, -200]} | ${[100, 100]} | ${[100, -100]}
|
||||||
|
${"n"} | ${[_, 150]} | ${[50, 50]} | ${[25, 100]}
|
||||||
|
`(
|
||||||
|
"Flips while resizing and keeping side ratios on handle $handle",
|
||||||
|
({ handle, move, dimensions, topLeft }) => {
|
||||||
|
render(<App />);
|
||||||
|
const rectangle = UI.createElement("rectangle", elemData);
|
||||||
|
resize(rectangle, handle, move, { shift: true });
|
||||||
|
const element = h.elements[0];
|
||||||
|
expect([element.width, element.height]).toEqual(dimensions);
|
||||||
|
expect([element.x, element.y]).toEqual(topLeft);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
handle | move | dimensions | topLeft
|
||||||
|
${"ne"} | ${[50, -100]} | ${[200, 300]} | ${[-50, -100]}
|
||||||
|
${"s"} | ${[_, -20]} | ${[100, 60]} | ${[0, 20]}
|
||||||
|
`(
|
||||||
|
"Resizes from center on handle $handle",
|
||||||
|
({ handle, move, dimensions, topLeft }) => {
|
||||||
|
render(<App />);
|
||||||
|
const rectangle = UI.createElement("rectangle", elemData);
|
||||||
|
resize(rectangle, handle, move, { alt: true });
|
||||||
|
const element = h.elements[0];
|
||||||
|
expect([element.width, element.height]).toEqual(dimensions);
|
||||||
|
expect([element.x, element.y]).toEqual(topLeft);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
handle | move | dimensions | topLeft
|
||||||
|
${"nw"} | ${[100, 120]} | ${[140, 140]} | ${[-20, -20]}
|
||||||
|
${"e"} | ${[-130, _]} | ${[160, 160]} | ${[-30, -30]}
|
||||||
|
`(
|
||||||
|
"Resizes from center, flips and keeps side rations on handle $handle",
|
||||||
|
({ handle, move, dimensions, topLeft }) => {
|
||||||
|
render(<App />);
|
||||||
|
const rectangle = UI.createElement("rectangle", elemData);
|
||||||
|
resize(rectangle, handle, move, { alt: true, shift: true });
|
||||||
|
const element = h.elements[0];
|
||||||
|
expect([element.width, element.height]).toEqual(dimensions);
|
||||||
|
expect([element.x, element.y]).toEqual(topLeft);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resize element with aspect ratio when SHIFT is clicked", () => {
|
function resize(
|
||||||
it("rectangle", async () => {
|
element: ExcalidrawElement,
|
||||||
await render(<ExcalidrawApp />);
|
handleDir: TransformHandleDirection,
|
||||||
|
mouseMove: [number, number],
|
||||||
const rectangle = UI.createElement("rectangle", {
|
keyboardModifiers: KeyboardModifiers = {},
|
||||||
x: 0,
|
) {
|
||||||
width: 30,
|
mouse.select(element);
|
||||||
height: 50,
|
const handle = getTransformHandles(element, h.state.zoom, "mouse")[
|
||||||
});
|
handleDir
|
||||||
|
]!;
|
||||||
mouse.select(rectangle);
|
const clientX = handle[0] + handle[2] / 2;
|
||||||
|
const clientY = handle[1] + handle[3] / 2;
|
||||||
const se = getTransformHandles(rectangle, h.state.zoom, "mouse").se!;
|
Keyboard.withModifierKeys(keyboardModifiers, () => {
|
||||||
const clientX = se[0] + se[2] / 2;
|
mouse.reset();
|
||||||
const clientY = se[1] + se[3] / 2;
|
mouse.down(clientX, clientY);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
mouse.move(mouseMove[0], mouseMove[1]);
|
||||||
mouse.reset();
|
mouse.up();
|
||||||
mouse.down(clientX, clientY);
|
|
||||||
mouse.move(1, 1);
|
|
||||||
mouse.up();
|
|
||||||
});
|
|
||||||
expect([h.elements[0].width, h.elements[0].height]).toEqual([51, 51]);
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user