aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/element/resizeTest.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/element/resizeTest.ts')
-rw-r--r--packages/excalidraw/element/resizeTest.ts287
1 files changed, 287 insertions, 0 deletions
diff --git a/packages/excalidraw/element/resizeTest.ts b/packages/excalidraw/element/resizeTest.ts
new file mode 100644
index 0000000..375ff98
--- /dev/null
+++ b/packages/excalidraw/element/resizeTest.ts
@@ -0,0 +1,287 @@
+import type {
+ ExcalidrawElement,
+ PointerType,
+ NonDeletedExcalidrawElement,
+ ElementsMap,
+} from "./types";
+
+import type {
+ TransformHandleType,
+ TransformHandle,
+ MaybeTransformHandleType,
+} from "./transformHandles";
+import {
+ getTransformHandlesFromCoords,
+ getTransformHandles,
+ getOmitSidesForDevice,
+ canResizeFromSides,
+} from "./transformHandles";
+import type { AppState, Device, Zoom } from "../types";
+import type { Bounds } from "./bounds";
+import { getElementAbsoluteCoords } from "./bounds";
+import { SIDE_RESIZING_THRESHOLD } from "../constants";
+import { isImageElement, isLinearElement } from "./typeChecks";
+import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
+import {
+ pointFrom,
+ pointOnLineSegment,
+ pointRotateRads,
+ type Radians,
+} from "@excalidraw/math";
+
+const isInsideTransformHandle = (
+ transformHandle: TransformHandle,
+ x: number,
+ y: number,
+) =>
+ x >= transformHandle[0] &&
+ x <= transformHandle[0] + transformHandle[2] &&
+ y >= transformHandle[1] &&
+ y <= transformHandle[1] + transformHandle[3];
+
+export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
+ element: NonDeletedExcalidrawElement,
+ elementsMap: ElementsMap,
+ appState: AppState,
+ x: number,
+ y: number,
+ zoom: Zoom,
+ pointerType: PointerType,
+ device: Device,
+): MaybeTransformHandleType => {
+ if (!appState.selectedElementIds[element.id]) {
+ return false;
+ }
+
+ const { rotation: rotationTransformHandle, ...transformHandles } =
+ getTransformHandles(
+ element,
+ zoom,
+ elementsMap,
+ pointerType,
+ getOmitSidesForDevice(device),
+ );
+
+ if (
+ rotationTransformHandle &&
+ isInsideTransformHandle(rotationTransformHandle, x, y)
+ ) {
+ return "rotation" as TransformHandleType;
+ }
+
+ const filter = Object.keys(transformHandles).filter((key) => {
+ const transformHandle =
+ transformHandles[key as Exclude<TransformHandleType, "rotation">]!;
+ if (!transformHandle) {
+ return false;
+ }
+ return isInsideTransformHandle(transformHandle, x, y);
+ });
+
+ if (filter.length > 0) {
+ return filter[0] as TransformHandleType;
+ }
+
+ if (canResizeFromSides(device)) {
+ const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
+ element,
+ elementsMap,
+ );
+
+ // do not resize from the sides for linear elements with only two points
+ if (!(isLinearElement(element) && element.points.length <= 2)) {
+ const SPACING = isImageElement(element)
+ ? 0
+ : SIDE_RESIZING_THRESHOLD / zoom.value;
+ const ZOOMED_SIDE_RESIZING_THRESHOLD =
+ SIDE_RESIZING_THRESHOLD / zoom.value;
+ const sides = getSelectionBorders(
+ pointFrom(x1 - SPACING, y1 - SPACING),
+ pointFrom(x2 + SPACING, y2 + SPACING),
+ pointFrom(cx, cy),
+ element.angle,
+ );
+
+ for (const [dir, side] of Object.entries(sides)) {
+ // test to see if x, y are on the line segment
+ if (
+ pointOnLineSegment(
+ pointFrom(x, y),
+ side as LineSegment<Point>,
+ ZOOMED_SIDE_RESIZING_THRESHOLD,
+ )
+ ) {
+ return dir as TransformHandleType;
+ }
+ }
+ }
+ }
+
+ return false;
+};
+
+export const getElementWithTransformHandleType = (
+ elements: readonly NonDeletedExcalidrawElement[],
+ appState: AppState,
+ scenePointerX: number,
+ scenePointerY: number,
+ zoom: Zoom,
+ pointerType: PointerType,
+ elementsMap: ElementsMap,
+ device: Device,
+) => {
+ return elements.reduce((result, element) => {
+ if (result) {
+ return result;
+ }
+ const transformHandleType = resizeTest(
+ element,
+ elementsMap,
+ appState,
+ scenePointerX,
+ scenePointerY,
+ zoom,
+ pointerType,
+ device,
+ );
+ return transformHandleType ? { element, transformHandleType } : null;
+ }, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null);
+};
+
+export const getTransformHandleTypeFromCoords = <
+ Point extends GlobalPoint | LocalPoint,
+>(
+ [x1, y1, x2, y2]: Bounds,
+ scenePointerX: number,
+ scenePointerY: number,
+ zoom: Zoom,
+ pointerType: PointerType,
+ device: Device,
+): MaybeTransformHandleType => {
+ const transformHandles = getTransformHandlesFromCoords(
+ [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
+ 0 as Radians,
+ zoom,
+ pointerType,
+ getOmitSidesForDevice(device),
+ );
+
+ const found = Object.keys(transformHandles).find((key) => {
+ const transformHandle =
+ transformHandles[key as Exclude<TransformHandleType, "rotation">]!;
+ return (
+ transformHandle &&
+ isInsideTransformHandle(transformHandle, scenePointerX, scenePointerY)
+ );
+ });
+
+ if (found) {
+ return found as MaybeTransformHandleType;
+ }
+
+ if (canResizeFromSides(device)) {
+ const cx = (x1 + x2) / 2;
+ const cy = (y1 + y2) / 2;
+
+ const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
+
+ const sides = getSelectionBorders(
+ pointFrom(x1 - SPACING, y1 - SPACING),
+ pointFrom(x2 + SPACING, y2 + SPACING),
+ pointFrom(cx, cy),
+ 0 as Radians,
+ );
+
+ for (const [dir, side] of Object.entries(sides)) {
+ // test to see if x, y are on the line segment
+ if (
+ pointOnLineSegment(
+ pointFrom(scenePointerX, scenePointerY),
+ side as LineSegment<Point>,
+ SPACING,
+ )
+ ) {
+ return dir as TransformHandleType;
+ }
+ }
+ }
+
+ return false;
+};
+
+const RESIZE_CURSORS = ["ns", "nesw", "ew", "nwse"];
+const rotateResizeCursor = (cursor: string, angle: number) => {
+ const index = RESIZE_CURSORS.indexOf(cursor);
+ if (index >= 0) {
+ const a = Math.round(angle / (Math.PI / 4));
+ cursor = RESIZE_CURSORS[(index + a) % RESIZE_CURSORS.length];
+ }
+ return cursor;
+};
+
+/*
+ * Returns bi-directional cursor for the element being resized
+ */
+export const getCursorForResizingElement = (resizingElement: {
+ element?: ExcalidrawElement;
+ transformHandleType: MaybeTransformHandleType;
+}): string => {
+ const { element, transformHandleType } = resizingElement;
+ const shouldSwapCursors =
+ element && Math.sign(element.height) * Math.sign(element.width) === -1;
+ let cursor = null;
+
+ switch (transformHandleType) {
+ case "n":
+ case "s":
+ cursor = "ns";
+ break;
+ case "w":
+ case "e":
+ cursor = "ew";
+ break;
+ case "nw":
+ case "se":
+ if (shouldSwapCursors) {
+ cursor = "nesw";
+ } else {
+ cursor = "nwse";
+ }
+ break;
+ case "ne":
+ case "sw":
+ if (shouldSwapCursors) {
+ cursor = "nwse";
+ } else {
+ cursor = "nesw";
+ }
+ break;
+ case "rotation":
+ return "grab";
+ }
+
+ if (cursor && element) {
+ cursor = rotateResizeCursor(cursor, element.angle);
+ }
+
+ return cursor ? `${cursor}-resize` : "";
+};
+
+const getSelectionBorders = <Point extends LocalPoint | GlobalPoint>(
+ [x1, y1]: Point,
+ [x2, y2]: Point,
+ center: Point,
+ angle: Radians,
+) => {
+ const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle);
+ const topRight = pointRotateRads(pointFrom(x2, y1), center, angle);
+ const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle);
+ const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle);
+
+ return {
+ n: [topLeft, topRight],
+ e: [topRight, bottomRight],
+ s: [bottomRight, bottomLeft],
+ w: [bottomLeft, topLeft],
+ };
+};