aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/element/sizeHelpers.ts
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/element/sizeHelpers.ts
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/element/sizeHelpers.ts')
-rw-r--r--packages/excalidraw/element/sizeHelpers.ts231
1 files changed, 231 insertions, 0 deletions
diff --git a/packages/excalidraw/element/sizeHelpers.ts b/packages/excalidraw/element/sizeHelpers.ts
new file mode 100644
index 0000000..f633789
--- /dev/null
+++ b/packages/excalidraw/element/sizeHelpers.ts
@@ -0,0 +1,231 @@
+import type { ElementsMap, ExcalidrawElement } from "./types";
+import { mutateElement } from "./mutateElement";
+import { isFreeDrawElement, isLinearElement } from "./typeChecks";
+import { SHIFT_LOCKING_ANGLE } from "../constants";
+import type { AppState, Offsets, Zoom } from "../types";
+import { getCommonBounds, getElementBounds } from "./bounds";
+import { viewportCoordsToSceneCoords } from "../utils";
+
+// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
+// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
+// - could also be part of `_clearElements`
+export const isInvisiblySmallElement = (
+ element: ExcalidrawElement,
+): boolean => {
+ if (isLinearElement(element) || isFreeDrawElement(element)) {
+ return element.points.length < 2;
+ }
+ return element.width === 0 && element.height === 0;
+};
+
+export const isElementInViewport = (
+ element: ExcalidrawElement,
+ width: number,
+ height: number,
+ viewTransformations: {
+ zoom: Zoom;
+ offsetLeft: number;
+ offsetTop: number;
+ scrollX: number;
+ scrollY: number;
+ },
+ elementsMap: ElementsMap,
+) => {
+ const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
+ const topLeftSceneCoords = viewportCoordsToSceneCoords(
+ {
+ clientX: viewTransformations.offsetLeft,
+ clientY: viewTransformations.offsetTop,
+ },
+ viewTransformations,
+ );
+ const bottomRightSceneCoords = viewportCoordsToSceneCoords(
+ {
+ clientX: viewTransformations.offsetLeft + width,
+ clientY: viewTransformations.offsetTop + height,
+ },
+ viewTransformations,
+ );
+
+ return (
+ topLeftSceneCoords.x <= x2 &&
+ topLeftSceneCoords.y <= y2 &&
+ bottomRightSceneCoords.x >= x1 &&
+ bottomRightSceneCoords.y >= y1
+ );
+};
+
+export const isElementCompletelyInViewport = (
+ elements: ExcalidrawElement[],
+ width: number,
+ height: number,
+ viewTransformations: {
+ zoom: Zoom;
+ offsetLeft: number;
+ offsetTop: number;
+ scrollX: number;
+ scrollY: number;
+ },
+ elementsMap: ElementsMap,
+ padding?: Offsets,
+) => {
+ const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
+ const topLeftSceneCoords = viewportCoordsToSceneCoords(
+ {
+ clientX: viewTransformations.offsetLeft + (padding?.left || 0),
+ clientY: viewTransformations.offsetTop + (padding?.top || 0),
+ },
+ viewTransformations,
+ );
+ const bottomRightSceneCoords = viewportCoordsToSceneCoords(
+ {
+ clientX: viewTransformations.offsetLeft + width - (padding?.right || 0),
+ clientY: viewTransformations.offsetTop + height - (padding?.bottom || 0),
+ },
+ viewTransformations,
+ );
+
+ return (
+ x1 >= topLeftSceneCoords.x &&
+ y1 >= topLeftSceneCoords.y &&
+ x2 <= bottomRightSceneCoords.x &&
+ y2 <= bottomRightSceneCoords.y
+ );
+};
+
+/**
+ * Makes a perfect shape or diagonal/horizontal/vertical line
+ */
+export const getPerfectElementSize = (
+ elementType: AppState["activeTool"]["type"],
+ width: number,
+ height: number,
+): { width: number; height: number } => {
+ const absWidth = Math.abs(width);
+ const absHeight = Math.abs(height);
+
+ if (
+ elementType === "line" ||
+ elementType === "arrow" ||
+ elementType === "freedraw"
+ ) {
+ const lockedAngle =
+ Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
+ SHIFT_LOCKING_ANGLE;
+ if (lockedAngle === 0) {
+ height = 0;
+ } else if (lockedAngle === Math.PI / 2) {
+ width = 0;
+ } else {
+ height = absWidth * Math.tan(lockedAngle) * Math.sign(height) || height;
+ }
+ } else if (elementType !== "selection") {
+ height = absWidth * Math.sign(height);
+ }
+ return { width, height };
+};
+
+export const getLockedLinearCursorAlignSize = (
+ originX: number,
+ originY: number,
+ x: number,
+ y: number,
+) => {
+ let width = x - originX;
+ let height = y - originY;
+
+ const lockedAngle =
+ Math.round(Math.atan(height / width) / SHIFT_LOCKING_ANGLE) *
+ SHIFT_LOCKING_ANGLE;
+
+ if (lockedAngle === 0) {
+ height = 0;
+ } else if (lockedAngle === Math.PI / 2) {
+ width = 0;
+ } else {
+ // locked angle line, y = mx + b => mx - y + b = 0
+ const a1 = Math.tan(lockedAngle);
+ const b1 = -1;
+ const c1 = originY - a1 * originX;
+
+ // line through cursor, perpendicular to locked angle line
+ const a2 = -1 / a1;
+ const b2 = -1;
+ const c2 = y - a2 * x;
+
+ // intersection of the two lines above
+ const intersectX = (b1 * c2 - b2 * c1) / (a1 * b2 - a2 * b1);
+ const intersectY = (c1 * a2 - c2 * a1) / (a1 * b2 - a2 * b1);
+
+ // delta
+ width = intersectX - originX;
+ height = intersectY - originY;
+ }
+
+ return { width, height };
+};
+
+export const resizePerfectLineForNWHandler = (
+ element: ExcalidrawElement,
+ x: number,
+ y: number,
+) => {
+ const anchorX = element.x + element.width;
+ const anchorY = element.y + element.height;
+ const distanceToAnchorX = x - anchorX;
+ const distanceToAnchorY = y - anchorY;
+ if (Math.abs(distanceToAnchorX) < Math.abs(distanceToAnchorY) / 2) {
+ mutateElement(element, {
+ x: anchorX,
+ width: 0,
+ y,
+ height: -distanceToAnchorY,
+ });
+ } else if (Math.abs(distanceToAnchorY) < Math.abs(element.width) / 2) {
+ mutateElement(element, {
+ y: anchorY,
+ height: 0,
+ });
+ } else {
+ const nextHeight =
+ Math.sign(distanceToAnchorY) *
+ Math.sign(distanceToAnchorX) *
+ element.width;
+ mutateElement(element, {
+ x,
+ y: anchorY - nextHeight,
+ width: -distanceToAnchorX,
+ height: nextHeight,
+ });
+ }
+};
+
+export const getNormalizedDimensions = (
+ element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
+): {
+ width: ExcalidrawElement["width"];
+ height: ExcalidrawElement["height"];
+ x: ExcalidrawElement["x"];
+ y: ExcalidrawElement["y"];
+} => {
+ const ret = {
+ width: element.width,
+ height: element.height,
+ x: element.x,
+ y: element.y,
+ };
+
+ if (element.width < 0) {
+ const nextWidth = Math.abs(element.width);
+ ret.width = nextWidth;
+ ret.x = element.x - nextWidth;
+ }
+
+ if (element.height < 0) {
+ const nextHeight = Math.abs(element.height);
+ ret.height = nextHeight;
+ ret.y = element.y - nextHeight;
+ }
+
+ return ret;
+};