* resize single elements from the side * fix lint * do not resize texts from the sides (for we want to wrap/unwrap) * omit side handles for frames too * upgrade types * enable resizing from the sides for multiple elements as well * fix lint * maintain aspect ratio when elements are not of the same angle * lint * always resize proportionally for multiple elements * increase side resizing padding * code cleanup * adaptive handles * do not resize for linear elements with only two points * prioritize point dragging over edge resizing * lint * allow free resizing for multiple elements at degree 0 * always resize from the sides * reduce hit threshold * make small multiple elements movable * lint * show side handles on touch screen and mobile devices * differentiate touchscreens * keep proportional with text in multi-element resizing * update snapshot * update multi elements resizing logic * lint * reduce side resizing padding * bound texts do not scale in normal cases * lint * test sides for texts * wrap text * do not update text size when changing its alignment * keep text wrapped/unwrapped when editing * change wrapped size to auto size from context menu * fix test * lint * increase min width for wrapped texts * wrap wrapped text in container * unwrap when binding text to container * rename `wrapped` to `autoResize` * fix lint * revert: use `center` align when wrapping text in container * update snaps * fix lint * simplify logic on autoResize * lint and test * snapshots * remove unnecessary code * snapshots * fix: defaults not set correctly * tests for wrapping texts when resized * tests for text wrapping when edited * fix autoResize refactor * include autoResize flag check * refactor * feat: rename action label & change contextmenu position * fix: update version on `autoResize` action * fix infinite loop when editing text in a container * simplify * always maintain `width` if `!autoResize` * maintain `x` if `!autoResize` * maintain `y` pos after fontSize change if `!autoResize` * refactor * when editing, do not wrap text in textWysiwyg * simplify text editor * make test more readable * comment * rename action to match file name * revert function signature change * only update in app --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
269 lines
6.7 KiB
TypeScript
269 lines
6.7 KiB
TypeScript
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 {
|
|
angleToDegrees,
|
|
pointOnLine,
|
|
pointRotate,
|
|
} from "../../utils/geometry/geometry";
|
|
import type { Line, Point } from "../../utils/geometry/shape";
|
|
import { isLinearElement } from "./typeChecks";
|
|
|
|
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 = (
|
|
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 = SIDE_RESIZING_THRESHOLD / zoom.value;
|
|
const sides = getSelectionBorders(
|
|
[x1 - SPACING, y1 - SPACING],
|
|
[x2 + SPACING, y2 + SPACING],
|
|
[cx, cy],
|
|
angleToDegrees(element.angle),
|
|
);
|
|
|
|
for (const [dir, side] of Object.entries(sides)) {
|
|
// test to see if x, y are on the line segment
|
|
if (pointOnLine([x, y], side as Line, SPACING)) {
|
|
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 = (
|
|
[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,
|
|
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(
|
|
[x1 - SPACING, y1 - SPACING],
|
|
[x2 + SPACING, y2 + SPACING],
|
|
[cx, cy],
|
|
angleToDegrees(0),
|
|
);
|
|
|
|
for (const [dir, side] of Object.entries(sides)) {
|
|
// test to see if x, y are on the line segment
|
|
if (pointOnLine([scenePointerX, scenePointerY], side as Line, 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 = (
|
|
[x1, y1]: Point,
|
|
[x2, y2]: Point,
|
|
center: Point,
|
|
angleInDegrees: number,
|
|
) => {
|
|
const topLeft = pointRotate([x1, y1], angleInDegrees, center);
|
|
const topRight = pointRotate([x2, y1], angleInDegrees, center);
|
|
const bottomLeft = pointRotate([x1, y2], angleInDegrees, center);
|
|
const bottomRight = pointRotate([x2, y2], angleInDegrees, center);
|
|
|
|
return {
|
|
n: [topLeft, topRight],
|
|
e: [topRight, bottomRight],
|
|
s: [bottomRight, bottomLeft],
|
|
w: [bottomLeft, topLeft],
|
|
};
|
|
};
|