123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- import { ARROW_TYPE } from "@excalidraw/common";
- import { pointFrom } from "@excalidraw/math";
- import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
- import Scene from "@excalidraw/excalidraw/scene/Scene";
- import { actionSelectAll } from "@excalidraw/excalidraw/actions";
- import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
- import { API } from "@excalidraw/excalidraw/tests/helpers/api";
- import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
- import {
- act,
- fireEvent,
- GlobalTestState,
- queryByTestId,
- render,
- } from "@excalidraw/excalidraw/tests/test-utils";
- import "@excalidraw/utils/test-utils";
- import type { LocalPoint } from "@excalidraw/math";
- import { bindLinearElement } from "../src/binding";
- import type {
- ExcalidrawArrowElement,
- ExcalidrawBindableElement,
- ExcalidrawElbowArrowElement,
- } from "../src/types";
- const { h } = window;
- const mouse = new Pointer("mouse");
- describe("elbow arrow segment move", () => {
- beforeEach(async () => {
- localStorage.clear();
- await render(<Excalidraw handleKeyboardGlobally={true} />);
- });
- it("can move the second segment of a fully connected elbow arrow", () => {
- UI.createElement("rectangle", {
- x: -100,
- y: -50,
- width: 100,
- height: 100,
- });
- UI.createElement("rectangle", {
- x: 200,
- y: 150,
- width: 100,
- height: 100,
- });
- UI.clickTool("arrow");
- UI.clickOnTestId("elbow-arrow");
- mouse.reset();
- mouse.moveTo(0, 0);
- mouse.click();
- mouse.moveTo(200, 200);
- mouse.click();
- mouse.reset();
- mouse.moveTo(100, 100);
- mouse.down();
- mouse.moveTo(115, 100);
- mouse.up();
- const arrow = h.scene.getSelectedElements(
- h.state,
- )[0] as ExcalidrawElbowArrowElement;
- expect(h.state.selectedElementIds).toEqual({ [arrow.id]: true });
- expect(arrow.fixedSegments?.length).toBe(1);
- expect(arrow.points).toCloselyEqualPoints([
- [0, 0],
- [110, 0],
- [110, 200],
- [190, 200],
- ]);
- mouse.reset();
- mouse.moveTo(105, 74.275);
- mouse.doubleClick();
- expect(arrow.points).toCloselyEqualPoints([
- [0, 0],
- [110, 0],
- [110, 200],
- [190, 200],
- ]);
- });
- it("can move the second segment of an unconnected elbow arrow", () => {
- UI.clickTool("arrow");
- UI.clickOnTestId("elbow-arrow");
- mouse.reset();
- mouse.moveTo(0, 0);
- mouse.click();
- mouse.moveTo(250, 200);
- mouse.click();
- mouse.reset();
- mouse.moveTo(125, 100);
- mouse.down();
- mouse.moveTo(130, 100);
- mouse.up();
- const arrow = h.scene.getSelectedElements(
- h.state,
- )[0] as ExcalidrawArrowElement;
- expect(arrow.points).toCloselyEqualPoints([
- [0, 0],
- [130, 0],
- [130, 200],
- [250, 200],
- ]);
- mouse.reset();
- mouse.moveTo(130, 100);
- mouse.doubleClick();
- expect(arrow.points).toCloselyEqualPoints([
- [0, 0],
- [125, 0],
- [125, 200],
- [250, 200],
- ]);
- });
- });
- describe("elbow arrow routing", () => {
- it("can properly generate orthogonal arrow points", () => {
- const scene = new Scene();
- const arrow = API.createElement({
- type: "arrow",
- elbowed: true,
- }) as ExcalidrawElbowArrowElement;
- scene.insertElement(arrow);
- mutateElement(arrow, {
- points: [
- pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
- pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
- ],
- });
- expect(arrow.points).toEqual([
- [0, 0],
- [0, 100],
- [90, 100],
- [90, 200],
- ]);
- expect(arrow.x).toEqual(-45);
- expect(arrow.y).toEqual(-100.1);
- expect(arrow.width).toEqual(90);
- expect(arrow.height).toEqual(200);
- });
- it("can generate proper points for bound elbow arrow", () => {
- const scene = new Scene();
- const rectangle1 = API.createElement({
- type: "rectangle",
- x: -150,
- y: -150,
- width: 100,
- height: 100,
- }) as ExcalidrawBindableElement;
- const rectangle2 = API.createElement({
- type: "rectangle",
- x: 50,
- y: 50,
- width: 100,
- height: 100,
- }) as ExcalidrawBindableElement;
- const arrow = API.createElement({
- type: "arrow",
- elbowed: true,
- x: -45,
- y: -100.1,
- width: 90,
- height: 200,
- points: [pointFrom(0, 0), pointFrom(90, 200)],
- }) as ExcalidrawElbowArrowElement;
- scene.insertElement(rectangle1);
- scene.insertElement(rectangle2);
- scene.insertElement(arrow);
- const elementsMap = scene.getNonDeletedElementsMap();
- bindLinearElement(arrow, rectangle1, "start", elementsMap);
- bindLinearElement(arrow, rectangle2, "end", elementsMap);
- expect(arrow.startBinding).not.toBe(null);
- expect(arrow.endBinding).not.toBe(null);
- mutateElement(arrow, {
- points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
- });
- expect(arrow.points).toEqual([
- [0, 0],
- [45, 0],
- [45, 200],
- [90, 200],
- ]);
- });
- });
- describe("elbow arrow ui", () => {
- beforeEach(async () => {
- localStorage.clear();
- await render(<Excalidraw handleKeyboardGlobally={true} />);
- fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
- button: 2,
- clientX: 1,
- clientY: 1,
- });
- const contextMenu = UI.queryContextMenu();
- fireEvent.click(queryByTestId(contextMenu!, "stats")!);
- });
- it("can follow bound shapes", async () => {
- UI.createElement("rectangle", {
- x: -150,
- y: -150,
- width: 100,
- height: 100,
- });
- UI.createElement("rectangle", {
- x: 50,
- y: 50,
- width: 100,
- height: 100,
- });
- UI.clickTool("arrow");
- UI.clickOnTestId("elbow-arrow");
- expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
- mouse.reset();
- mouse.moveTo(-43, -99);
- mouse.click();
- mouse.moveTo(43, 99);
- mouse.click();
- const arrow = h.scene.getSelectedElements(
- h.state,
- )[0] as ExcalidrawArrowElement;
- expect(arrow.type).toBe("arrow");
- expect(arrow.elbowed).toBe(true);
- expect(arrow.points).toEqual([
- [0, 0],
- [45, 0],
- [45, 200],
- [90, 200],
- ]);
- });
- it("can follow bound rotated shapes", async () => {
- UI.createElement("rectangle", {
- x: -150,
- y: -150,
- width: 100,
- height: 100,
- });
- UI.createElement("rectangle", {
- x: 50,
- y: 50,
- width: 100,
- height: 100,
- });
- UI.clickTool("arrow");
- UI.clickOnTestId("elbow-arrow");
- mouse.reset();
- mouse.moveTo(-43, -99);
- mouse.click();
- mouse.moveTo(43, 99);
- mouse.click();
- const arrow = h.scene.getSelectedElements(
- h.state,
- )[0] as ExcalidrawArrowElement;
- mouse.click(51, 51);
- const inputAngle = UI.queryStatsProperty("A")?.querySelector(
- ".drag-input",
- ) as HTMLInputElement;
- UI.updateInput(inputAngle, String("40"));
- expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
- [0, 0],
- [35, 0],
- [35, 165],
- [103, 165],
- ]);
- });
- it("keeps arrow shape when the whole set of arrow and bindables are duplicated", async () => {
- UI.createElement("rectangle", {
- x: -150,
- y: -150,
- width: 100,
- height: 100,
- });
- UI.createElement("rectangle", {
- x: 50,
- y: 50,
- width: 100,
- height: 100,
- });
- UI.clickTool("arrow");
- UI.clickOnTestId("elbow-arrow");
- mouse.reset();
- mouse.moveTo(-43, -99);
- mouse.click();
- mouse.moveTo(43, 99);
- mouse.click();
- const arrow = h.scene.getSelectedElements(
- h.state,
- )[0] as ExcalidrawArrowElement;
- const originalArrowId = arrow.id;
- expect(arrow.startBinding).not.toBe(null);
- expect(arrow.endBinding).not.toBe(null);
- act(() => {
- h.app.actionManager.executeAction(actionSelectAll);
- });
- act(() => {
- h.app.actionManager.executeAction(actionDuplicateSelection);
- });
- expect(h.elements.length).toEqual(6);
- const duplicatedArrow = h.scene.getSelectedElements(
- h.state,
- )[2] as ExcalidrawArrowElement;
- expect(duplicatedArrow.id).not.toBe(originalArrowId);
- expect(duplicatedArrow.type).toBe("arrow");
- expect(duplicatedArrow.elbowed).toBe(true);
- expect(duplicatedArrow.points).toEqual([
- [0, 0],
- [45, 0],
- [45, 200],
- [90, 200],
- ]);
- expect(arrow.startBinding).not.toBe(null);
- expect(arrow.endBinding).not.toBe(null);
- });
- it("changes arrow shape to unbind variant if only the connected elbow arrow is duplicated", async () => {
- UI.createElement("rectangle", {
- x: -150,
- y: -150,
- width: 100,
- height: 100,
- });
- UI.createElement("rectangle", {
- x: 50,
- y: 50,
- width: 100,
- height: 100,
- });
- UI.clickTool("arrow");
- UI.clickOnTestId("elbow-arrow");
- mouse.reset();
- mouse.moveTo(-43, -99);
- mouse.click();
- mouse.moveTo(43, 99);
- mouse.click();
- const arrow = h.scene.getSelectedElements(
- h.state,
- )[0] as ExcalidrawArrowElement;
- const originalArrowId = arrow.id;
- expect(arrow.startBinding).not.toBe(null);
- expect(arrow.endBinding).not.toBe(null);
- act(() => {
- h.app.actionManager.executeAction(actionDuplicateSelection);
- });
- expect(h.elements.length).toEqual(4);
- const duplicatedArrow = h.scene.getSelectedElements(
- h.state,
- )[0] as ExcalidrawArrowElement;
- expect(duplicatedArrow.id).not.toBe(originalArrowId);
- expect(duplicatedArrow.type).toBe("arrow");
- expect(duplicatedArrow.elbowed).toBe(true);
- expect(duplicatedArrow.points).toEqual([
- [0, 0],
- [0, 100],
- [90, 100],
- [90, 200],
- ]);
- });
- });
|