Feature: Align elements (#2267)
Co-authored-by: Maximilian Massing <maximilian.massing@googlemail.com> Co-authored-by: Sven Kube <github@sven-kube.de> Co-authored-by: Maximilian Massing <massing@sipgate.de>
This commit is contained in:
parent
411bc2aa0a
commit
856ab50090
221
src/actions/actionAlign.tsx
Normal file
221
src/actions/actionAlign.tsx
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { register } from "./register";
|
||||||
|
import {
|
||||||
|
AlignBottomIcon,
|
||||||
|
AlignLeftIcon,
|
||||||
|
AlignRightIcon,
|
||||||
|
AlignTopIcon,
|
||||||
|
CenterHorizontallyIcon,
|
||||||
|
CenterVerticallyIcon,
|
||||||
|
} from "../components/icons";
|
||||||
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
|
import { getElementMap, getNonDeletedElements } from "../element";
|
||||||
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { alignElements, Alignment } from "../align";
|
||||||
|
import { getShortcutKey } from "../utils";
|
||||||
|
|
||||||
|
const enableActionGroup = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1;
|
||||||
|
|
||||||
|
function alignSelectedElements(
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: Readonly<AppState>,
|
||||||
|
alignment: Alignment,
|
||||||
|
) {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedElements = alignElements(selectedElements, alignment);
|
||||||
|
|
||||||
|
const updatedElementsMap = getElementMap(updatedElements);
|
||||||
|
|
||||||
|
return elements.map((element) => updatedElementsMap[element.id] || element);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionAlignTop = register({
|
||||||
|
name: "alignTop",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements: alignSelectedElements(elements, appState, {
|
||||||
|
position: "start",
|
||||||
|
axis: "y",
|
||||||
|
}),
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) => {
|
||||||
|
return (
|
||||||
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP
|
||||||
|
);
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<ToolButton
|
||||||
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
|
type="button"
|
||||||
|
icon={<AlignTopIcon appearance={appState.appearance} />}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={`${t("labels.alignTop")} — ${getShortcutKey(
|
||||||
|
"CtrlOrCmd+Shift+Up",
|
||||||
|
)}`}
|
||||||
|
aria-label={t("labels.alignTop")}
|
||||||
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionAlignBottom = register({
|
||||||
|
name: "alignBottom",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements: alignSelectedElements(elements, appState, {
|
||||||
|
position: "end",
|
||||||
|
axis: "y",
|
||||||
|
}),
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) => {
|
||||||
|
return (
|
||||||
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN
|
||||||
|
);
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<ToolButton
|
||||||
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
|
type="button"
|
||||||
|
icon={<AlignBottomIcon appearance={appState.appearance} />}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={`${t("labels.alignBottom")} — ${getShortcutKey(
|
||||||
|
"CtrlOrCmd+Shift+Down",
|
||||||
|
)}`}
|
||||||
|
aria-label={t("labels.alignBottom")}
|
||||||
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionAlignLeft = register({
|
||||||
|
name: "alignLeft",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements: alignSelectedElements(elements, appState, {
|
||||||
|
position: "start",
|
||||||
|
axis: "x",
|
||||||
|
}),
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) => {
|
||||||
|
return (
|
||||||
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT
|
||||||
|
);
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<ToolButton
|
||||||
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
|
type="button"
|
||||||
|
icon={<AlignLeftIcon appearance={appState.appearance} />}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={`${t("labels.alignLeft")} — ${getShortcutKey(
|
||||||
|
"CtrlOrCmd+Shift+Left",
|
||||||
|
)}`}
|
||||||
|
aria-label={t("labels.alignLeft")}
|
||||||
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionAlignRight = register({
|
||||||
|
name: "alignRight",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements: alignSelectedElements(elements, appState, {
|
||||||
|
position: "end",
|
||||||
|
axis: "x",
|
||||||
|
}),
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
keyTest: (event) => {
|
||||||
|
return (
|
||||||
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.shiftKey &&
|
||||||
|
event.key === KEYS.ARROW_RIGHT
|
||||||
|
);
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<ToolButton
|
||||||
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
|
type="button"
|
||||||
|
icon={<AlignRightIcon appearance={appState.appearance} />}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={`${t("labels.alignRight")} — ${getShortcutKey(
|
||||||
|
"CtrlOrCmd+Shift+Right",
|
||||||
|
)}`}
|
||||||
|
aria-label={t("labels.alignRight")}
|
||||||
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionAlignVerticallyCentered = register({
|
||||||
|
name: "alignVerticallyCentered",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements: alignSelectedElements(elements, appState, {
|
||||||
|
position: "center",
|
||||||
|
axis: "y",
|
||||||
|
}),
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<ToolButton
|
||||||
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
|
type="button"
|
||||||
|
icon={<CenterVerticallyIcon appearance={appState.appearance} />}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={t("labels.centerVertically")}
|
||||||
|
aria-label={t("labels.centerVertically")}
|
||||||
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionAlignHorizontallyCentered = register({
|
||||||
|
name: "alignHorizontallyCentered",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements: alignSelectedElements(elements, appState, {
|
||||||
|
position: "center",
|
||||||
|
axis: "x",
|
||||||
|
}),
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<ToolButton
|
||||||
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
|
type="button"
|
||||||
|
icon={<CenterHorizontallyIcon appearance={appState.appearance} />}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={t("labels.centerHorizontally")}
|
||||||
|
aria-label={t("labels.centerHorizontally")}
|
||||||
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
@ -51,3 +51,12 @@ export { actionGroup, actionUngroup } from "./actionGroup";
|
|||||||
export { actionGoToCollaborator } from "./actionNavigate";
|
export { actionGoToCollaborator } from "./actionNavigate";
|
||||||
|
|
||||||
export { actionAddToLibrary } from "./actionAddToLibrary";
|
export { actionAddToLibrary } from "./actionAddToLibrary";
|
||||||
|
|
||||||
|
export {
|
||||||
|
actionAlignTop,
|
||||||
|
actionAlignBottom,
|
||||||
|
actionAlignLeft,
|
||||||
|
actionAlignRight,
|
||||||
|
actionAlignVerticallyCentered,
|
||||||
|
actionAlignHorizontallyCentered,
|
||||||
|
} from "./actionAlign";
|
||||||
|
@ -65,7 +65,13 @@ export type ActionName =
|
|||||||
| "ungroup"
|
| "ungroup"
|
||||||
| "goToCollaborator"
|
| "goToCollaborator"
|
||||||
| "addToLibrary"
|
| "addToLibrary"
|
||||||
| "changeSharpness";
|
| "changeSharpness"
|
||||||
|
| "alignTop"
|
||||||
|
| "alignBottom"
|
||||||
|
| "alignLeft"
|
||||||
|
| "alignRight"
|
||||||
|
| "alignVerticallyCentered"
|
||||||
|
| "alignHorizontallyCentered";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
|
95
src/align.ts
Normal file
95
src/align.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { ExcalidrawElement } from "./element/types";
|
||||||
|
import { newElementWith } from "./element/mutateElement";
|
||||||
|
import { getCommonBounds } from "./element";
|
||||||
|
|
||||||
|
interface Box {
|
||||||
|
minX: number;
|
||||||
|
minY: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Alignment {
|
||||||
|
position: "start" | "center" | "end";
|
||||||
|
axis: "x" | "y";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const alignElements = (
|
||||||
|
selectedElements: ExcalidrawElement[],
|
||||||
|
alignment: Alignment,
|
||||||
|
): ExcalidrawElement[] => {
|
||||||
|
const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements);
|
||||||
|
|
||||||
|
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
|
||||||
|
|
||||||
|
return groups.flatMap((group) => {
|
||||||
|
const translation = calculateTranslation(
|
||||||
|
group,
|
||||||
|
selectionBoundingBox,
|
||||||
|
alignment,
|
||||||
|
);
|
||||||
|
return group.map((element) =>
|
||||||
|
newElementWith(element, {
|
||||||
|
x: element.x + translation.x,
|
||||||
|
y: element.y + translation.y,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaximumGroups = (
|
||||||
|
elements: ExcalidrawElement[],
|
||||||
|
): ExcalidrawElement[][] => {
|
||||||
|
const groups: Map<String, ExcalidrawElement[]> = new Map<
|
||||||
|
String,
|
||||||
|
ExcalidrawElement[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
elements.forEach((element: ExcalidrawElement) => {
|
||||||
|
const groupId =
|
||||||
|
element.groupIds.length === 0
|
||||||
|
? element.id
|
||||||
|
: element.groupIds[element.groupIds.length - 1];
|
||||||
|
|
||||||
|
const currentGroupMembers = groups.get(groupId) || [];
|
||||||
|
|
||||||
|
groups.set(groupId, [...currentGroupMembers, element]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groups.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTranslation = (
|
||||||
|
group: ExcalidrawElement[],
|
||||||
|
selectionBoundingBox: Box,
|
||||||
|
{ 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCommonBoundingBox(elements: ExcalidrawElement[]): Box {
|
||||||
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
return { minX, minY, maxX, maxY };
|
||||||
|
}
|
@ -83,6 +83,21 @@ export const SelectedShapeActions = ({
|
|||||||
{renderAction("bringForward")}
|
{renderAction("bringForward")}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
{targetElements.length > 1 && (
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t("labels.align")}</legend>
|
||||||
|
<div className="buttonList">
|
||||||
|
{renderAction("alignLeft")}
|
||||||
|
{renderAction("alignHorizontallyCentered")}
|
||||||
|
{renderAction("alignRight")}
|
||||||
|
{renderAction("alignTop")}
|
||||||
|
{renderAction("alignVerticallyCentered")}
|
||||||
|
{renderAction("alignBottom")}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isMobile && !isEditing && targetElements.length > 0 && (
|
{!isMobile && !isEditing && targetElements.length > 0 && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.actions")}</legend>
|
<legend>{t("labels.actions")}</legend>
|
||||||
|
@ -280,6 +280,22 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.bringForward")}
|
label={t("labels.bringForward")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.alignTop")}
|
||||||
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.alignBottom")}
|
||||||
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.alignLeft")}
|
||||||
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.alignRight")}
|
||||||
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.duplicateSelection")}
|
label={t("labels.duplicateSelection")}
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
|
@ -202,6 +202,143 @@ export const SendToBackIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Align action icons created from scratch to match those of z-index actions
|
||||||
|
//
|
||||||
|
export const AlignTopIcon = React.memo(
|
||||||
|
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M 2,5 H 22"
|
||||||
|
fill={iconFillColor(appearance)}
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 6,7 C 5.446,7 5,7.446 5,8 v 9.999992 c 0,0.554 0.446,1 1,1 h 3.0000001 c 0.554,0 0.9999999,-0.446 0.9999999,-1 V 8 C 10,7.446 9.5540001,7 9.0000001,7 Z m 9,0 c -0.554,0 -1,0.446 -1,1 v 5.999992 c 0,0.554 0.446,1 1,1 h 3 c 0.554,0 1,-0.446 1,-1 V 8 C 19,7.446 18.554,7 18,7 Z"
|
||||||
|
fill={activeElementColor(appearance)}
|
||||||
|
stroke={activeElementColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 24 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AlignBottomIcon = React.memo(
|
||||||
|
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M 2,19 H 22"
|
||||||
|
fill={iconFillColor(appearance)}
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 6,16.999992 c -0.554,0 -1,-0.446 -1,-1 V 6 C 5,5.446 5.446,5 6,5 H 9.0000001 C 9.5540001,5 10,5.446 10,6 v 9.999992 c 0,0.554 -0.4459999,1 -0.9999999,1 z m 9,0 c -0.554,0 -1,-0.446 -1,-1 V 10 c 0,-0.554 0.446,-1 1,-1 h 3 c 0.554,0 1,0.446 1,1 v 5.999992 c 0,0.554 -0.446,1 -1,1 z"
|
||||||
|
fill={activeElementColor(appearance)}
|
||||||
|
stroke={activeElementColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 24 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AlignLeftIcon = React.memo(
|
||||||
|
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M 5,2 V 22"
|
||||||
|
fill={iconFillColor(appearance)}
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 7.000004,5.999996 c 0,-0.554 0.446,-1 1,-1 h 9.999992 c 0.554,0 1,0.446 1,1 v 3.0000001 c 0,0.554 -0.446,0.9999999 -1,0.9999999 H 8.000004 c -0.554,0 -1,-0.4459999 -1,-0.9999999 z m 0,9 c 0,-0.554 0.446,-1 1,-1 h 5.999992 c 0.554,0 1,0.446 1,1 v 3 c 0,0.554 -0.446,1 -1,1 H 8.000004 c -0.554,0 -1,-0.446 -1,-1 z"
|
||||||
|
fill={activeElementColor(appearance)}
|
||||||
|
stroke={activeElementColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 24 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AlignRightIcon = React.memo(
|
||||||
|
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M 19,2 V 22"
|
||||||
|
fill={iconFillColor(appearance)}
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 16.999996,5.999996 c 0,-0.554 -0.446,-1 -1,-1 H 6.000004 c -0.554,0 -1,0.446 -1,1 v 3.0000001 c 0,0.554 0.446,0.9999999 1,0.9999999 h 9.999992 c 0.554,0 1,-0.4459999 1,-0.9999999 z m 0,9 c 0,-0.554 -0.446,-1 -1,-1 h -5.999992 c -0.554,0 -1,0.446 -1,1 v 3 c 0,0.554 0.446,1 1,1 h 5.999992 c 0.554,0 1,-0.446 1,-1 z"
|
||||||
|
fill={activeElementColor(appearance)}
|
||||||
|
stroke={activeElementColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 24 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CenterVerticallyIcon = React.memo(
|
||||||
|
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="m 5.000004,16.999996 c 0,0.554 0.446,1 1,1 h 3 c 0.554,0 1,-0.446 1,-1 v -10 c 0,-0.554 -0.446,-1 -1,-1 h -3 c -0.554,0 -1,0.446 -1,1 z m 9,-2 c 0,0.554 0.446,1 1,1 h 3 c 0.554,0 1,-0.446 1,-1 v -6 c 0,-0.554 -0.446,-1 -1,-1 h -3 c -0.554,0 -1,0.446 -1,1 z"
|
||||||
|
fill={activeElementColor(appearance)}
|
||||||
|
stroke={activeElementColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 2,12 H 22"
|
||||||
|
fill={iconFillColor(appearance)}
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeDasharray="1, 2.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 24 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CenterHorizontallyIcon = React.memo(
|
||||||
|
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M 7 5 C 6.446 5 6 5.446 6 6 L 6 9 C 6 9.554 6.446 10 7 10 L 17 10 C 17.554 10 18 9.554 18 9 L 18 6 C 18 5.446 17.554 5 17 5 L 7 5 z M 9 14 C 8.446 14 8 14.446 8 15 L 8 18 C 8 18.554 8.446 19 9 19 L 15 19 C 15.554 19 16 18.554 16 18 L 16 15 C 16 14.446 15.554 14 15 14 L 9 14 z "
|
||||||
|
fill={activeElementColor(appearance)}
|
||||||
|
stroke={activeElementColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 12,2 V 22"
|
||||||
|
fill={iconFillColor(appearance)}
|
||||||
|
stroke={iconFillColor(appearance)}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeDasharray="1, 2.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 24 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const users = createIcon(
|
export const users = createIcon(
|
||||||
"M192 256c61.9 0 112-50.1 112-112S253.9 32 192 32 80 82.1 80 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C51.6 288 0 339.6 0 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zM480 256c53 0 96-43 96-96s-43-96-96-96-96 43-96 96 43 96 96 96zm48 32h-3.8c-13.9 4.8-28.6 8-44.2 8s-30.3-3.2-44.2-8H432c-20.4 0-39.2 5.9-55.7 15.4 24.4 26.3 39.7 61.2 39.7 99.8v38.4c0 2.2-.5 4.3-.6 6.4H592c26.5 0 48-21.5 48-48 0-61.9-50.1-112-112-112z",
|
"M192 256c61.9 0 112-50.1 112-112S253.9 32 192 32 80 82.1 80 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C51.6 288 0 339.6 0 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zM480 256c53 0 96-43 96-96s-43-96-96-96-96 43-96 96 43 96 96 96zm48 32h-3.8c-13.9 4.8-28.6 8-44.2 8s-30.3-3.2-44.2-8H432c-20.4 0-39.2 5.9-55.7 15.4 24.4 26.3 39.7 61.2 39.7 99.8v38.4c0 2.2-.5 4.3-.6 6.4H592c26.5 0 48-21.5 48-48 0-61.9-50.1-112-112-112z",
|
||||||
{ width: 640, height: 512, mirror: true },
|
{ width: 640, height: 512, mirror: true },
|
||||||
|
@ -74,7 +74,14 @@
|
|||||||
"addToLibrary": "Add to library",
|
"addToLibrary": "Add to library",
|
||||||
"removeFromLibrary": "Remove from library",
|
"removeFromLibrary": "Remove from library",
|
||||||
"libraryLoadingMessage": "Loading library...",
|
"libraryLoadingMessage": "Loading library...",
|
||||||
"loadingScene": "Loading scene..."
|
"loadingScene": "Loading scene...",
|
||||||
|
"align": "Align",
|
||||||
|
"alignTop": "Align top",
|
||||||
|
"alignBottom": "Align bottom",
|
||||||
|
"alignLeft": "Align left",
|
||||||
|
"alignRight": "Align right",
|
||||||
|
"centerVertically": "Center vertically",
|
||||||
|
"centerHorizontally": "Center horizontally"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
|
579
src/tests/align.test.tsx
Normal file
579
src/tests/align.test.tsx
Normal file
@ -0,0 +1,579 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { render } from "./test-utils";
|
||||||
|
import App from "../components/App";
|
||||||
|
import { setLanguage } from "../i18n";
|
||||||
|
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
import {
|
||||||
|
actionAlignVerticallyCentered,
|
||||||
|
actionAlignHorizontallyCentered,
|
||||||
|
actionGroup,
|
||||||
|
actionAlignTop,
|
||||||
|
actionAlignBottom,
|
||||||
|
actionAlignLeft,
|
||||||
|
actionAlignRight,
|
||||||
|
} from "../actions";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Unmount ReactDOM from root
|
||||||
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
await setLanguage("en.json");
|
||||||
|
render(<App />);
|
||||||
|
});
|
||||||
|
|
||||||
|
function 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
});
|
||||||
|
|
||||||
|
function 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
|
||||||
|
|
||||||
|
expect(API.getSelectedElements()[0].x).toEqual(50);
|
||||||
|
expect(API.getSelectedElements()[1].x).toEqual(150);
|
||||||
|
expect(API.getSelectedElements()[2].x).toEqual(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
function 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
h.app.actionManager.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();
|
||||||
|
});
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
});
|
||||||
|
|
||||||
|
function 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
|
||||||
|
h.app.actionManager.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
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
|
||||||
|
h.app.actionManager.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);
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user