You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
success/packages/excalidraw/element/dragElements.ts

197 lines
5.3 KiB
TypeScript

import { updateBoundElements } from "./binding";
import type { Bounds } from "./bounds";
import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers";
import type { NonDeletedExcalidrawElement } from "./types";
import type { AppState, PointerDownState } from "../types";
import { getBoundTextElement } from "./textElement";
import { getGridPoint } from "../math";
import type Scene from "../scene/Scene";
import { isArrowElement, isFrameLikeElement } from "./typeChecks";
export const dragSelectedElements = (
pointerDownState: PointerDownState,
selectedElements: NonDeletedExcalidrawElement[],
offset: { x: number; y: number },
appState: AppState,
scene: Scene,
snapOffset: {
x: number;
y: number;
},
gridSize: AppState["gridSize"],
) => {
// we do not want a frame and its elements to be selected at the same time
// but when it happens (due to some bug), we want to avoid updating element
// in the frame twice, hence the use of set
const elementsToUpdate = new Set<NonDeletedExcalidrawElement>(
selectedElements,
);
const frames = selectedElements
.filter((e) => isFrameLikeElement(e))
.map((f) => f.id);
if (frames.length > 0) {
for (const element of scene.getNonDeletedElements()) {
if (element.frameId !== null && frames.includes(element.frameId)) {
elementsToUpdate.add(element);
}
}
}
const commonBounds = getCommonBounds(
Array.from(elementsToUpdate).map(
(el) => pointerDownState.originalElements.get(el.id) ?? el,
),
);
const adjustedOffset = calculateOffset(
commonBounds,
offset,
snapOffset,
gridSize,
);
elementsToUpdate.forEach((element) => {
updateElementCoords(pointerDownState, element, adjustedOffset);
if (
// skip arrow labels since we calculate its position during render
!isArrowElement(element)
) {
const textElement = getBoundTextElement(
element,
scene.getNonDeletedElementsMap(),
);
if (textElement) {
updateElementCoords(pointerDownState, textElement, adjustedOffset);
}
}
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
simultaneouslyUpdated: Array.from(elementsToUpdate),
});
});
};
const calculateOffset = (
commonBounds: Bounds,
dragOffset: { x: number; y: number },
snapOffset: { x: number; y: number },
gridSize: AppState["gridSize"],
): { x: number; y: number } => {
const [x, y] = commonBounds;
let nextX = x + dragOffset.x + snapOffset.x;
let nextY = y + dragOffset.y + snapOffset.y;
if (snapOffset.x === 0 || snapOffset.y === 0) {
const [nextGridX, nextGridY] = getGridPoint(
x + dragOffset.x,
y + dragOffset.y,
gridSize,
);
if (snapOffset.x === 0) {
nextX = nextGridX;
}
if (snapOffset.y === 0) {
nextY = nextGridY;
}
}
return {
x: nextX - x,
y: nextY - y,
};
};
const updateElementCoords = (
pointerDownState: PointerDownState,
element: NonDeletedExcalidrawElement,
dragOffset: { x: number; y: number },
) => {
const originalElement =
pointerDownState.originalElements.get(element.id) ?? element;
const nextX = originalElement.x + dragOffset.x;
const nextY = originalElement.y + dragOffset.y;
mutateElement(element, {
x: nextX,
y: nextY,
});
};
export const getDragOffsetXY = (
selectedElements: NonDeletedExcalidrawElement[],
x: number,
y: number,
): [number, number] => {
const [x1, y1] = getCommonBounds(selectedElements);
return [x - x1, y - y1];
};
export const dragNewElement = (
draggingElement: NonDeletedExcalidrawElement,
elementType: AppState["activeTool"]["type"],
originX: number,
originY: number,
x: number,
y: number,
width: number,
height: number,
shouldMaintainAspectRatio: boolean,
shouldResizeFromCenter: boolean,
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
true */
widthAspectRatio?: number | null,
originOffset: {
x: number;
y: number;
} | null = null,
) => {
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
if (widthAspectRatio) {
height = width / widthAspectRatio;
} else {
// Depending on where the cursor is at (x, y) relative to where the starting point is
// (originX, originY), we use ONLY width or height to control size increase.
// This allows the cursor to always "stick" to one of the sides of the bounding box.
if (Math.abs(y - originY) > Math.abs(x - originX)) {
({ width, height } = getPerfectElementSize(
elementType,
height,
x < originX ? -width : width,
));
} else {
({ width, height } = getPerfectElementSize(
elementType,
width,
y < originY ? -height : height,
));
}
if (height < 0) {
height = -height;
}
}
}
let newX = x < originX ? originX - width : originX;
let newY = y < originY ? originY - height : originY;
if (shouldResizeFromCenter) {
width += width;
height += height;
newX = originX - width / 2;
newY = originY - height / 2;
}
if (width !== 0 && height !== 0) {
mutateElement(draggingElement, {
x: newX + (originOffset?.x ?? 0),
y: newY + (originOffset?.y ?? 0),
width,
height,
});
}
};