Move align, distribute, zindex to element package

This commit is contained in:
Marcel Mraz 2025-03-18 20:17:40 +01:00
parent a3fef45365
commit e7a0a7e0b7
No known key found for this signature in database
GPG key ID: 4EBD6E62DC830CD2
8 changed files with 46 additions and 36 deletions

View file

@ -0,0 +1,78 @@
import { updateBoundElements } from "@excalidraw/element/binding";
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { getMaximumGroups } from "@excalidraw/element/groups";
import type { BoundingBox } from "@excalidraw/element/bounds";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
export interface Alignment {
position: "start" | "center" | "end";
axis: "x" | "y";
}
export const alignElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
alignment: Alignment,
scene: Scene,
): ExcalidrawElement[] => {
const groups: ExcalidrawElement[][] = getMaximumGroups(
selectedElements,
elementsMap,
);
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
return groups.flatMap((group) => {
const translation = calculateTranslation(
group,
selectionBoundingBox,
alignment,
);
return group.map((element) => {
// update element
const updatedEle = mutateElement(element, {
x: element.x + translation.x,
y: element.y + translation.y,
});
// update bound elements
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: group,
});
return updatedEle;
});
});
};
const calculateTranslation = (
group: ExcalidrawElement[],
selectionBoundingBox: BoundingBox,
{ axis, position }: Alignment,
): { x: number; y: number } => {
const groupBoundingBox = getCommonBoundingBox(group);
const [min, max]: ["minX" | "minY", "maxX" | "maxY"] =
axis === "x" ? ["minX", "maxX"] : ["minY", "maxY"];
const noTranslation = { x: 0, y: 0 };
if (position === "start") {
return {
...noTranslation,
[axis]: selectionBoundingBox[min] - groupBoundingBox[min],
};
} else if (position === "end") {
return {
...noTranslation,
[axis]: selectionBoundingBox[max] - groupBoundingBox[max],
};
} // else if (position === "center") {
return {
...noTranslation,
[axis]:
(selectionBoundingBox[min] + selectionBoundingBox[max]) / 2 -
(groupBoundingBox[min] + groupBoundingBox[max]) / 2,
};
};

View file

@ -0,0 +1,93 @@
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
import { newElementWith } from "@excalidraw/element/mutateElement";
import { getMaximumGroups } from "@excalidraw/element/groups";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
export interface Distribution {
space: "between";
axis: "x" | "y";
}
export const distributeElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
distribution: Distribution,
): ExcalidrawElement[] => {
const [start, mid, end, extent] =
distribution.axis === "x"
? (["minX", "midX", "maxX", "width"] as const)
: (["minY", "midY", "maxY", "height"] as const);
const bounds = getCommonBoundingBox(selectedElements);
const groups = getMaximumGroups(selectedElements, elementsMap)
.map((group) => [group, getCommonBoundingBox(group)] as const)
.sort((a, b) => a[1][mid] - b[1][mid]);
let span = 0;
for (const group of groups) {
span += group[1][extent];
}
const step = (bounds[extent] - span) / (groups.length - 1);
if (step < 0) {
// If we have a negative step, we'll need to distribute from centers
// rather than from gaps. Buckle up, this is a weird one.
// Get indices of boxes that define start and end of our bounding box
const index0 = groups.findIndex((g) => g[1][start] === bounds[start]);
const index1 = groups.findIndex((g) => g[1][end] === bounds[end]);
// Get our step, based on the distance between the center points of our
// start and end boxes
const step =
(groups[index1][1][mid] - groups[index0][1][mid]) / (groups.length - 1);
let pos = groups[index0][1][mid];
return groups.flatMap(([group, box], index) => {
const translation = {
x: 0,
y: 0,
};
// Don't move our start and end boxes
if (index !== index0 && index !== index1) {
pos += step;
translation[distribution.axis] = pos - box[mid];
}
return group.map((element) =>
newElementWith(element, {
x: element.x + translation.x,
y: element.y + translation.y,
}),
);
});
}
// Distribute from gaps
let pos = bounds[start];
return groups.flatMap(([group, box]) => {
const translation = {
x: 0,
y: 0,
};
translation[distribution.axis] = pos - box[start];
pos += step;
pos += box[extent];
return group.map((element) =>
newElementWith(element, {
x: element.x + translation.x,
y: element.y + translation.y,
}),
);
});
};

View file

@ -0,0 +1,544 @@
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
import { getElementsInGroup } from "@excalidraw/element/groups";
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
import { getSelectedElements } from "@excalidraw/excalidraw/scene";
import type {
ExcalidrawElement,
ExcalidrawFrameLikeElement,
} from "@excalidraw/element/types";
import type { AppState } from "@excalidraw/excalidraw/types";
import Scene from "../../excalidraw/scene/Scene";
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
return element.frameId === frameId || element.id === frameId;
};
/**
* Returns indices of elements to move based on selected elements.
* Includes contiguous deleted elements that are between two selected elements,
* e.g.: [0 (selected), 1 (deleted), 2 (deleted), 3 (selected)]
*
* Specified elements (elementsToBeMoved) take precedence over
* appState.selectedElementsIds
*/
const getIndicesToMove = (
elements: readonly ExcalidrawElement[],
appState: AppState,
elementsToBeMoved?: readonly ExcalidrawElement[],
) => {
let selectedIndices: number[] = [];
let deletedIndices: number[] = [];
let includeDeletedIndex = null;
let index = -1;
const selectedElementIds = arrayToMap(
elementsToBeMoved
? elementsToBeMoved
: getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
}),
);
while (++index < elements.length) {
const element = elements[index];
if (selectedElementIds.get(element.id)) {
if (deletedIndices.length) {
selectedIndices = selectedIndices.concat(deletedIndices);
deletedIndices = [];
}
selectedIndices.push(index);
includeDeletedIndex = index + 1;
} else if (element.isDeleted && includeDeletedIndex === index) {
includeDeletedIndex = index + 1;
deletedIndices.push(index);
} else {
deletedIndices = [];
}
}
return selectedIndices;
};
const toContiguousGroups = (array: number[]) => {
let cursor = 0;
return array.reduce((acc, value, index) => {
if (index > 0 && array[index - 1] !== value - 1) {
cursor = ++cursor;
}
(acc[cursor] || (acc[cursor] = [])).push(value);
return acc;
}, [] as number[][]);
};
/**
* @returns index of target element, consindering tightly-bound elements
* (currently non-linear elements bound to a container) as a one unit.
* If no binding present, returns `undefined`.
*/
const getTargetIndexAccountingForBinding = (
nextElement: ExcalidrawElement,
elements: readonly ExcalidrawElement[],
direction: "left" | "right",
) => {
if ("containerId" in nextElement && nextElement.containerId) {
const containerElement = Scene.getScene(nextElement)!.getElement(
nextElement.containerId,
);
if (containerElement) {
return direction === "left"
? Math.min(
elements.indexOf(containerElement),
elements.indexOf(nextElement),
)
: Math.max(
elements.indexOf(containerElement),
elements.indexOf(nextElement),
);
}
} else {
const boundElementId = nextElement.boundElements?.find(
(binding) => binding.type !== "arrow",
)?.id;
if (boundElementId) {
const boundTextElement =
Scene.getScene(nextElement)!.getElement(boundElementId);
if (boundTextElement) {
return direction === "left"
? Math.min(
elements.indexOf(boundTextElement),
elements.indexOf(nextElement),
)
: Math.max(
elements.indexOf(boundTextElement),
elements.indexOf(nextElement),
);
}
}
}
};
const getContiguousFrameRangeElements = (
allElements: readonly ExcalidrawElement[],
frameId: ExcalidrawFrameLikeElement["id"],
) => {
let rangeStart = -1;
let rangeEnd = -1;
allElements.forEach((element, index) => {
if (isOfTargetFrame(element, frameId)) {
if (rangeStart === -1) {
rangeStart = index;
}
rangeEnd = index;
}
});
if (rangeStart === -1) {
return [];
}
return allElements.slice(rangeStart, rangeEnd + 1);
};
/**
* Returns next candidate index that's available to be moved to. Currently that
* is a non-deleted element, and not inside a group (unless we're editing it).
*/
const getTargetIndex = (
appState: AppState,
elements: readonly ExcalidrawElement[],
boundaryIndex: number,
direction: "left" | "right",
/**
* Frame id if moving frame children.
* If whole frame (including all children) is being moved, supply `null`.
*/
containingFrame: ExcalidrawFrameLikeElement["id"] | null,
) => {
const sourceElement = elements[boundaryIndex];
const indexFilter = (element: ExcalidrawElement) => {
if (element.isDeleted) {
return false;
}
if (containingFrame) {
return element.frameId === containingFrame;
}
// if we're editing group, find closest sibling irrespective of whether
// there's a different-group element between them (for legacy reasons)
if (appState.editingGroupId) {
return element.groupIds.includes(appState.editingGroupId);
}
return true;
};
const candidateIndex =
direction === "left"
? findLastIndex(
elements,
(el) => indexFilter(el),
Math.max(0, boundaryIndex - 1),
)
: findIndex(elements, (el) => indexFilter(el), boundaryIndex + 1);
const nextElement = elements[candidateIndex];
if (!nextElement) {
return -1;
}
if (appState.editingGroupId) {
if (
// candidate element is a sibling in current editing group → return
sourceElement?.groupIds.join("") === nextElement?.groupIds.join("")
) {
return (
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
candidateIndex
);
} else if (!nextElement?.groupIds.includes(appState.editingGroupId)) {
// candidate element is outside current editing group → prevent
return -1;
}
}
if (
!containingFrame &&
(nextElement.frameId || isFrameLikeElement(nextElement))
) {
const frameElements = getContiguousFrameRangeElements(
elements,
nextElement.frameId || nextElement.id,
);
return direction === "left"
? elements.indexOf(frameElements[0])
: elements.indexOf(frameElements[frameElements.length - 1]);
}
if (!nextElement.groupIds.length) {
return (
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
candidateIndex
);
}
const siblingGroupId = appState.editingGroupId
? nextElement.groupIds[
nextElement.groupIds.indexOf(appState.editingGroupId) - 1
]
: nextElement.groupIds[nextElement.groupIds.length - 1];
const elementsInSiblingGroup = getElementsInGroup(elements, siblingGroupId);
if (elementsInSiblingGroup.length) {
// assumes getElementsInGroup() returned elements are sorted
// by zIndex (ascending)
return direction === "left"
? elements.indexOf(elementsInSiblingGroup[0])
: elements.indexOf(
elementsInSiblingGroup[elementsInSiblingGroup.length - 1],
);
}
return candidateIndex;
};
const getTargetElementsMap = <T extends ExcalidrawElement>(
elements: readonly T[],
indices: number[],
) => {
return indices.reduce((acc, index) => {
const element = elements[index];
acc.set(element.id, element);
return acc;
}, new Map<string, ExcalidrawElement>());
};
const shiftElementsByOne = (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
) => {
const indicesToMove = getIndicesToMove(elements, appState);
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
let groupedIndices = toContiguousGroups(indicesToMove);
if (direction === "right") {
groupedIndices = groupedIndices.reverse();
}
const selectedFrames = new Set<ExcalidrawFrameLikeElement["id"]>(
indicesToMove
.filter((idx) => isFrameLikeElement(elements[idx]))
.map((idx) => elements[idx].id),
);
groupedIndices.forEach((indices, i) => {
const leadingIndex = indices[0];
const trailingIndex = indices[indices.length - 1];
const boundaryIndex = direction === "left" ? leadingIndex : trailingIndex;
const containingFrame = indices.some((idx) => {
const el = elements[idx];
return el.frameId && selectedFrames.has(el.frameId);
})
? null
: elements[boundaryIndex]?.frameId;
const targetIndex = getTargetIndex(
appState,
elements,
boundaryIndex,
direction,
containingFrame,
);
if (targetIndex === -1 || boundaryIndex === targetIndex) {
return;
}
const leadingElements =
direction === "left"
? elements.slice(0, targetIndex)
: elements.slice(0, leadingIndex);
const targetElements = elements.slice(leadingIndex, trailingIndex + 1);
const displacedElements =
direction === "left"
? elements.slice(targetIndex, leadingIndex)
: elements.slice(trailingIndex + 1, targetIndex + 1);
const trailingElements =
direction === "left"
? elements.slice(trailingIndex + 1)
: elements.slice(targetIndex + 1);
elements =
direction === "left"
? [
...leadingElements,
...targetElements,
...displacedElements,
...trailingElements,
]
: [
...leadingElements,
...displacedElements,
...targetElements,
...trailingElements,
];
});
syncMovedIndices(elements, targetElementsMap);
return elements;
};
const shiftElementsToEnd = (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
containingFrame: ExcalidrawFrameLikeElement["id"] | null,
elementsToBeMoved?: readonly ExcalidrawElement[],
) => {
const indicesToMove = getIndicesToMove(elements, appState, elementsToBeMoved);
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
const displacedElements: ExcalidrawElement[] = [];
let leadingIndex: number;
let trailingIndex: number;
if (direction === "left") {
if (containingFrame) {
leadingIndex = findIndex(elements, (el) =>
isOfTargetFrame(el, containingFrame),
);
} else if (appState.editingGroupId) {
const groupElements = getElementsInGroup(
elements,
appState.editingGroupId,
);
if (!groupElements.length) {
return elements;
}
leadingIndex = elements.indexOf(groupElements[0]);
} else {
leadingIndex = 0;
}
trailingIndex = indicesToMove[indicesToMove.length - 1];
} else {
if (containingFrame) {
trailingIndex = findLastIndex(elements, (el) =>
isOfTargetFrame(el, containingFrame),
);
} else if (appState.editingGroupId) {
const groupElements = getElementsInGroup(
elements,
appState.editingGroupId,
);
if (!groupElements.length) {
return elements;
}
trailingIndex = elements.indexOf(groupElements[groupElements.length - 1]);
} else {
trailingIndex = elements.length - 1;
}
leadingIndex = indicesToMove[0];
}
if (leadingIndex === -1) {
leadingIndex = 0;
}
for (let index = leadingIndex; index < trailingIndex + 1; index++) {
if (!indicesToMove.includes(index)) {
displacedElements.push(elements[index]);
}
}
const targetElements = Array.from(targetElementsMap.values());
const leadingElements = elements.slice(0, leadingIndex);
const trailingElements = elements.slice(trailingIndex + 1);
const nextElements =
direction === "left"
? [
...leadingElements,
...targetElements,
...displacedElements,
...trailingElements,
]
: [
...leadingElements,
...displacedElements,
...targetElements,
...trailingElements,
];
syncMovedIndices(nextElements, targetElementsMap);
return nextElements;
};
function shiftElementsAccountingForFrames(
allElements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
shiftFunction: (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
containingFrame: ExcalidrawFrameLikeElement["id"] | null,
elementsToBeMoved?: readonly ExcalidrawElement[],
) => ExcalidrawElement[] | readonly ExcalidrawElement[],
) {
const elementsToMove = arrayToMap(
getSelectedElements(allElements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
}),
);
const frameAwareContiguousElementsToMove: {
regularElements: ExcalidrawElement[];
frameChildren: Map<ExcalidrawFrameLikeElement["id"], ExcalidrawElement[]>;
} = { regularElements: [], frameChildren: new Map() };
const fullySelectedFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
for (const element of allElements) {
if (elementsToMove.has(element.id) && isFrameLikeElement(element)) {
fullySelectedFrames.add(element.id);
}
}
for (const element of allElements) {
if (elementsToMove.has(element.id)) {
if (
isFrameLikeElement(element) ||
(element.frameId && fullySelectedFrames.has(element.frameId))
) {
frameAwareContiguousElementsToMove.regularElements.push(element);
} else if (!element.frameId) {
frameAwareContiguousElementsToMove.regularElements.push(element);
} else {
const frameChildren =
frameAwareContiguousElementsToMove.frameChildren.get(
element.frameId,
) || [];
frameChildren.push(element);
frameAwareContiguousElementsToMove.frameChildren.set(
element.frameId,
frameChildren,
);
}
}
}
let nextElements = allElements;
const frameChildrenSets = Array.from(
frameAwareContiguousElementsToMove.frameChildren.entries(),
);
for (const [frameId, children] of frameChildrenSets) {
nextElements = shiftFunction(
allElements,
appState,
direction,
frameId,
children,
);
}
return shiftFunction(
nextElements,
appState,
direction,
null,
frameAwareContiguousElementsToMove.regularElements,
);
}
// public API
// -----------------------------------------------------------------------------
export const moveOneLeft = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElementsByOne(allElements, appState, "left");
};
export const moveOneRight = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElementsByOne(allElements, appState, "right");
};
export const moveAllLeft = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElementsAccountingForFrames(
allElements,
appState,
"left",
shiftElementsToEnd,
);
};
export const moveAllRight = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElementsAccountingForFrames(
allElements,
appState,
"right",
shiftElementsToEnd,
);
};

View file

@ -0,0 +1,584 @@
import { KEYS } from "@excalidraw/common";
import {
actionAlignVerticallyCentered,
actionAlignHorizontallyCentered,
actionGroup,
actionAlignTop,
actionAlignBottom,
actionAlignLeft,
actionAlignRight,
} from "@excalidraw/excalidraw/actions";
import { defaultLang, setLanguage } from "@excalidraw/excalidraw/i18n";
import { Excalidraw } from "@excalidraw/excalidraw";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
import {
act,
unmountComponent,
render,
} from "@excalidraw/excalidraw/tests/test-utils";
const mouse = new Pointer("mouse");
const createAndSelectTwoRectangles = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
const createAndSelectTwoRectanglesWithDifferentSizes = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(110, 110);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
describe("aligning", () => {
beforeEach(async () => {
unmountComponent();
mouse.reset();
await act(() => {
return setLanguage(defaultLang);
});
await render(<Excalidraw handleKeyboardGlobally={true} />);
});
it("aligns two objects correctly to the top", () => {
createAndSelectTwoRectangles();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.ARROW_UP);
});
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(0);
});
it("aligns two objects correctly to the bottom", () => {
createAndSelectTwoRectangles();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.ARROW_DOWN);
});
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(110);
expect(API.getSelectedElements()[1].y).toEqual(110);
});
it("aligns two objects correctly to the left", () => {
createAndSelectTwoRectangles();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.ARROW_LEFT);
});
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(0);
// Check if y position did not change
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
});
it("aligns two objects correctly to the right", () => {
createAndSelectTwoRectangles();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
expect(API.getSelectedElements()[0].x).toEqual(110);
expect(API.getSelectedElements()[1].x).toEqual(110);
// Check if y position did not change
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
});
it("centers two objects with different sizes correctly vertically", () => {
createAndSelectTwoRectanglesWithDifferentSizes();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
API.executeAction(actionAlignVerticallyCentered);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(60);
expect(API.getSelectedElements()[1].y).toEqual(55);
});
it("centers two objects with different sizes correctly horizontally", () => {
createAndSelectTwoRectanglesWithDifferentSizes();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(60);
expect(API.getSelectedElements()[1].x).toEqual(55);
// Check if y position did not change
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
});
const createAndSelectGroupAndRectangle = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
// Add the created group to the current selection
mouse.restorePosition(0, 0);
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
it("aligns a group with another element correctly to the top", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(0);
});
it("aligns a group with another element correctly to the bottom", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(200);
});
it("aligns a group with another element correctly to the left", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(0);
});
it("aligns a group with another element correctly to the right", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(200);
});
it("centers a group with another element correctly vertically", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(100);
});
it("centers a group with another element correctly horizontally", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(100);
});
const createAndSelectTwoGroups = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already selected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
mouse.restorePosition(200, 200);
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
API.executeAction(actionGroup);
// Select the first group.
// The second group is already selected because it was the last group created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
it("aligns two groups correctly to the top", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(0);
expect(API.getSelectedElements()[3].y).toEqual(100);
});
it("aligns two groups correctly to the bottom", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(200);
expect(API.getSelectedElements()[1].y).toEqual(300);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
});
it("aligns two groups correctly to the left", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(0);
expect(API.getSelectedElements()[3].x).toEqual(100);
});
it("aligns two groups correctly to the right", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(200);
expect(API.getSelectedElements()[1].x).toEqual(300);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
});
it("centers two groups correctly vertically", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(100);
expect(API.getSelectedElements()[3].y).toEqual(200);
});
it("centers two groups correctly horizontally", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(100);
expect(API.getSelectedElements()[3].x).toEqual(200);
});
const createAndSelectNestedGroupAndRectangle = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
// Create first group of rectangles
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
// Add group to current selection
mouse.restorePosition(0, 0);
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
// Create the nested group
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(300, 300);
mouse.up(100, 100);
// Select the nested group, the rectangle is already selected
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
it("aligns nested group and other element correctly to the top", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(0);
});
it("aligns nested group and other element correctly to the bottom", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(300);
expect(API.getSelectedElements()[3].y).toEqual(300);
});
it("aligns nested group and other element correctly to the left", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(0);
});
it("aligns nested group and other element correctly to the right", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(300);
expect(API.getSelectedElements()[3].x).toEqual(300);
});
it("centers nested group and other element correctly vertically", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(250);
expect(API.getSelectedElements()[3].y).toEqual(150);
});
it("centers nested group and other element correctly horizontally", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(250);
expect(API.getSelectedElements()[3].x).toEqual(150);
});
});

File diff suppressed because it is too large Load diff