2020-08-08 21:04:15 -07:00
|
|
|
import { updateBoundElements } from "./binding";
|
2020-06-24 00:24:52 +09:00
|
|
|
import { getCommonBounds } from "./bounds";
|
|
|
|
import { mutateElement } from "./mutateElement";
|
|
|
|
import { getPerfectElementSize } from "./sizeHelpers";
|
2020-08-08 21:04:15 -07:00
|
|
|
import { NonDeletedExcalidrawElement } from "./types";
|
2021-12-22 19:16:49 +05:30
|
|
|
import { AppState, PointerDownState } from "../types";
|
2022-03-02 17:04:09 +01:00
|
|
|
import { getBoundTextElement } from "./textElement";
|
2021-12-22 19:16:49 +05:30
|
|
|
import { isSelectedViaGroup } from "../groups";
|
2023-06-15 00:42:01 +08:00
|
|
|
import Scene from "../scene/Scene";
|
|
|
|
import { isFrameElement } from "./typeChecks";
|
2020-06-24 00:24:52 +09:00
|
|
|
|
|
|
|
export const dragSelectedElements = (
|
2020-09-11 17:22:40 +02:00
|
|
|
pointerDownState: PointerDownState,
|
2020-06-24 00:24:52 +09:00
|
|
|
selectedElements: NonDeletedExcalidrawElement[],
|
|
|
|
pointerX: number,
|
|
|
|
pointerY: number,
|
2020-09-11 17:22:40 +02:00
|
|
|
lockDirection: boolean = false,
|
|
|
|
distanceX: number = 0,
|
|
|
|
distanceY: number = 0,
|
2021-12-22 19:16:49 +05:30
|
|
|
appState: AppState,
|
2023-06-15 00:42:01 +08:00
|
|
|
scene: Scene,
|
2020-06-24 00:24:52 +09:00
|
|
|
) => {
|
|
|
|
const [x1, y1] = getCommonBounds(selectedElements);
|
2020-08-08 21:04:15 -07:00
|
|
|
const offset = { x: pointerX - x1, y: pointerY - y1 };
|
2023-06-15 00:42:01 +08:00
|
|
|
|
|
|
|
// 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) => isFrameElement(e))
|
|
|
|
.map((f) => f.id);
|
|
|
|
|
|
|
|
if (frames.length > 0) {
|
|
|
|
const elementsInFrames = scene
|
|
|
|
.getNonDeletedElements()
|
|
|
|
.filter((e) => e.frameId !== null)
|
|
|
|
.filter((e) => frames.includes(e.frameId!));
|
|
|
|
|
|
|
|
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
|
|
|
}
|
|
|
|
|
|
|
|
elementsToUpdate.forEach((element) => {
|
2021-12-16 21:14:03 +05:30
|
|
|
updateElementCoords(
|
|
|
|
lockDirection,
|
|
|
|
distanceX,
|
|
|
|
distanceY,
|
|
|
|
pointerDownState,
|
|
|
|
element,
|
|
|
|
offset,
|
|
|
|
);
|
2021-12-22 19:16:49 +05:30
|
|
|
// update coords of bound text only if we're dragging the container directly
|
|
|
|
// (we don't drag the group that it's part of)
|
|
|
|
if (
|
|
|
|
// container isn't part of any group
|
|
|
|
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
|
|
|
!element.groupIds.length ||
|
|
|
|
// container is part of a group, but we're dragging the container directly
|
|
|
|
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
|
|
|
) {
|
2022-03-02 17:04:09 +01:00
|
|
|
const textElement = getBoundTextElement(element);
|
2023-06-15 00:42:01 +08:00
|
|
|
if (
|
|
|
|
textElement &&
|
|
|
|
// when container is added to a frame, so will its bound text
|
|
|
|
// so the text is already in `elementsToUpdate` and we should avoid
|
|
|
|
// updating its coords again
|
|
|
|
(!textElement.frameId || !frames.includes(textElement.frameId))
|
|
|
|
) {
|
2021-12-16 21:14:03 +05:30
|
|
|
updateElementCoords(
|
|
|
|
lockDirection,
|
|
|
|
distanceX,
|
|
|
|
distanceY,
|
|
|
|
pointerDownState,
|
2022-03-02 17:04:09 +01:00
|
|
|
textElement,
|
2021-12-16 21:14:03 +05:30
|
|
|
offset,
|
|
|
|
);
|
|
|
|
}
|
2020-09-11 17:22:40 +02:00
|
|
|
}
|
|
|
|
updateBoundElements(element, {
|
2023-06-15 00:42:01 +08:00
|
|
|
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
2020-06-24 00:24:52 +09:00
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
const updateElementCoords = (
|
|
|
|
lockDirection: boolean,
|
|
|
|
distanceX: number,
|
|
|
|
distanceY: number,
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
element: NonDeletedExcalidrawElement,
|
|
|
|
offset: { x: number; y: number },
|
|
|
|
) => {
|
|
|
|
let x: number;
|
|
|
|
let y: number;
|
|
|
|
if (lockDirection) {
|
|
|
|
const lockX = lockDirection && distanceX < distanceY;
|
|
|
|
const lockY = lockDirection && distanceX > distanceY;
|
|
|
|
const original = pointerDownState.originalElements.get(element.id);
|
|
|
|
x = lockX && original ? original.x : element.x + offset.x;
|
|
|
|
y = lockY && original ? original.y : element.y + offset.y;
|
|
|
|
} else {
|
|
|
|
x = element.x + offset.x;
|
|
|
|
y = element.y + offset.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
mutateElement(element, {
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
});
|
|
|
|
};
|
2020-06-24 00:24:52 +09:00
|
|
|
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,
|
2022-03-25 20:46:01 +05:30
|
|
|
elementType: AppState["activeTool"]["type"],
|
2020-06-24 00:24:52 +09:00
|
|
|
originX: number,
|
|
|
|
originY: number,
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
width: number,
|
|
|
|
height: number,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldMaintainAspectRatio: boolean,
|
|
|
|
shouldResizeFromCenter: boolean,
|
|
|
|
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
|
|
|
true */
|
|
|
|
widthAspectRatio?: number | null,
|
2020-06-24 00:24:52 +09:00
|
|
|
) => {
|
2022-08-02 15:40:17 +02:00
|
|
|
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
2021-10-21 22:05:48 +02:00
|
|
|
if (widthAspectRatio) {
|
|
|
|
height = width / widthAspectRatio;
|
|
|
|
} else {
|
2022-08-01 19:24:46 +08:00
|
|
|
// 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,
|
|
|
|
));
|
|
|
|
}
|
2020-06-24 00:24:52 +09:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (height < 0) {
|
|
|
|
height = -height;
|
|
|
|
}
|
2020-06-24 00:24:52 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let newX = x < originX ? originX - width : originX;
|
|
|
|
let newY = y < originY ? originY - height : originY;
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldResizeFromCenter) {
|
2020-06-24 00:24:52 +09:00
|
|
|
width += width;
|
|
|
|
height += height;
|
|
|
|
newX = originX - width / 2;
|
|
|
|
newY = originY - height / 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (width !== 0 && height !== 0) {
|
|
|
|
mutateElement(draggingElement, {
|
|
|
|
x: newX,
|
|
|
|
y: newY,
|
2020-11-29 18:32:51 +02:00
|
|
|
width,
|
|
|
|
height,
|
2020-06-24 00:24:52 +09:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|