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.
197 lines
5.3 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
};
|