mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Enhance aspect ratio tools | Rectangle, Diamond, Ellipses (#2439)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
4c90ea5667
commit
aa221837fc
11 changed files with 488 additions and 9492 deletions
File diff suppressed because it is too large
Load diff
|
@ -1,27 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`resize element rectangle 1`] = `
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElementIds": null,
|
||||
"fillStyle": "hachure",
|
||||
"groupIds": Array [],
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
"strokeColor": "#000000",
|
||||
"strokeSharpness": "sharp",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"version": 3,
|
||||
"versionNonce": 401146281,
|
||||
"width": 30,
|
||||
"x": 29,
|
||||
"y": 47,
|
||||
}
|
||||
`;
|
|
@ -14,11 +14,13 @@ let altKey = false;
|
|||
let shiftKey = false;
|
||||
let ctrlKey = false;
|
||||
|
||||
export type KeyboardModifiers = {
|
||||
alt?: boolean;
|
||||
shift?: boolean;
|
||||
ctrl?: boolean;
|
||||
};
|
||||
export class Keyboard {
|
||||
static withModifierKeys = (
|
||||
modifiers: { alt?: boolean; shift?: boolean; ctrl?: boolean },
|
||||
cb: () => void,
|
||||
) => {
|
||||
static withModifierKeys = (modifiers: KeyboardModifiers, cb: () => void) => {
|
||||
const prevAltKey = altKey;
|
||||
const prevShiftKey = shiftKey;
|
||||
const prevCtrlKey = ctrlKey;
|
||||
|
|
|
@ -13,7 +13,6 @@ import Excalidraw from "../packages/excalidraw/index";
|
|||
import { setLanguage } from "../i18n";
|
||||
import { setDateTimeForTests } from "../utils";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { getTransformHandles as _getTransformHandles } from "../element";
|
||||
import { queryByText } from "@testing-library/react";
|
||||
import { copiedStyles } from "../actions/actionStyles";
|
||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||
|
@ -44,27 +43,6 @@ const clickLabeledElement = (label: string) => {
|
|||
fireEvent.click(element);
|
||||
};
|
||||
|
||||
type HandlerRectanglesRet = keyof ReturnType<typeof _getTransformHandles>;
|
||||
const getTransformHandles = (pointerType: "mouse" | "touch" | "pen") => {
|
||||
const rects = _getTransformHandles(
|
||||
API.getSelectedElement(),
|
||||
h.state.zoom,
|
||||
pointerType,
|
||||
) as {
|
||||
[T in HandlerRectanglesRet]: [number, number, number, number];
|
||||
};
|
||||
|
||||
const rv: { [K in keyof typeof rects]: [number, number] } = {} as any;
|
||||
|
||||
for (const handlePos in rects) {
|
||||
const [x, y, width, height] = rects[handlePos as keyof typeof rects];
|
||||
|
||||
rv[handlePos as keyof typeof rects] = [x + width / 2, y + height / 2];
|
||||
}
|
||||
|
||||
return rv;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is always called at the end of your test, so usually you don't need to call it.
|
||||
* However, if you have a long test, you might want to call it during the test so it's easier
|
||||
|
@ -204,67 +182,6 @@ describe("regression tests", () => {
|
|||
expect(API.getSelectedElement().strokeColor).toBe("#5f3dc4");
|
||||
});
|
||||
|
||||
it("resize an element, trying every resize handle", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.up(10, 10);
|
||||
|
||||
const transformHandles = getTransformHandles("mouse");
|
||||
// @ts-ignore
|
||||
delete transformHandles.rotation; // exclude rotation handle
|
||||
for (const handlePos in transformHandles) {
|
||||
const [x, y] = transformHandles[
|
||||
handlePos as keyof typeof transformHandles
|
||||
];
|
||||
const { width: prevWidth, height: prevHeight } = API.getSelectedElement();
|
||||
mouse.restorePosition(x, y);
|
||||
mouse.down();
|
||||
mouse.up(-5, -5);
|
||||
|
||||
const {
|
||||
width: nextWidthNegative,
|
||||
height: nextHeightNegative,
|
||||
} = API.getSelectedElement();
|
||||
expect(
|
||||
prevWidth !== nextWidthNegative || prevHeight !== nextHeightNegative,
|
||||
).toBeTruthy();
|
||||
checkpoint(`resize handle ${handlePos} (-5, -5)`);
|
||||
|
||||
mouse.down();
|
||||
mouse.up(5, 5);
|
||||
|
||||
const { width, height } = API.getSelectedElement();
|
||||
expect(width).toBe(prevWidth);
|
||||
expect(height).toBe(prevHeight);
|
||||
checkpoint(`unresize handle ${handlePos} (-5, -5)`);
|
||||
|
||||
mouse.restorePosition(x, y);
|
||||
mouse.down();
|
||||
mouse.up(5, 5);
|
||||
|
||||
const {
|
||||
width: nextWidthPositive,
|
||||
height: nextHeightPositive,
|
||||
} = API.getSelectedElement();
|
||||
expect(
|
||||
prevWidth !== nextWidthPositive || prevHeight !== nextHeightPositive,
|
||||
).toBeTruthy();
|
||||
checkpoint(`resize handle ${handlePos} (+5, +5)`);
|
||||
|
||||
mouse.down();
|
||||
mouse.up(-5, -5);
|
||||
|
||||
const {
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
} = API.getSelectedElement();
|
||||
expect(finalWidth).toBe(prevWidth);
|
||||
expect(finalHeight).toBe(prevHeight);
|
||||
|
||||
checkpoint(`unresize handle ${handlePos} (+5, +5)`);
|
||||
}
|
||||
});
|
||||
|
||||
it("click on an element and drag it", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { render, fireEvent } from "./test-utils";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { render } from "./test-utils";
|
||||
import App from "../components/App";
|
||||
import * as Renderer from "../renderer/renderScene";
|
||||
import { reseed } from "../random";
|
||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||
import { getTransformHandles } from "../element/transformHandles";
|
||||
import { UI, Pointer, Keyboard, KeyboardModifiers } from "./helpers/ui";
|
||||
import {
|
||||
getTransformHandles,
|
||||
TransformHandleDirection,
|
||||
} from "../element/transformHandles";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
|
@ -21,70 +25,119 @@ beforeEach(() => {
|
|||
|
||||
const { h } = window;
|
||||
|
||||
describe("resize element", () => {
|
||||
it("rectangle", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
describe("resize rectangle ellipses and diamond elements", () => {
|
||||
const elemData = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
};
|
||||
// Value for irrelevant cursor movements
|
||||
const _ = 234;
|
||||
|
||||
{
|
||||
// create element
|
||||
const tool = getByToolName("rectangle");
|
||||
fireEvent.click(tool);
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
||||
|
||||
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
|
||||
|
||||
renderScene.mockClear();
|
||||
}
|
||||
|
||||
// select the element first
|
||||
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
// select a handler rectangle (top-left)
|
||||
fireEvent.pointerDown(canvas, { clientX: 21, clientY: 13 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([29, 47]);
|
||||
expect([h.elements[0].width, h.elements[0].height]).toEqual([30, 50]);
|
||||
|
||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||
it.each`
|
||||
handle | move | dimensions | topLeft
|
||||
${"n"} | ${[_, -100]} | ${[100, 200]} | ${[elemData.x, -100]}
|
||||
${"s"} | ${[_, 39]} | ${[100, 139]} | ${[elemData.x, elemData.x]}
|
||||
${"e"} | ${[-20, _]} | ${[80, 100]} | ${[elemData.x, elemData.y]}
|
||||
${"w"} | ${[-20, _]} | ${[120, 100]} | ${[-20, elemData.y]}
|
||||
${"ne"} | ${[10, 55]} | ${[110, 45]} | ${[elemData.x, 55]}
|
||||
${"se"} | ${[-30, -10]} | ${[70, 90]} | ${[elemData.x, elemData.y]}
|
||||
${"nw"} | ${[-300, -200]} | ${[400, 300]} | ${[-300, -200]}
|
||||
${"sw"} | ${[40, -20]} | ${[60, 80]} | ${[40, 0]}
|
||||
`("resizes with handle $handle", ({ handle, move, dimensions, topLeft }) => {
|
||||
render(<App />);
|
||||
const rectangle = UI.createElement("rectangle", elemData);
|
||||
resize(rectangle, handle, move);
|
||||
const element = h.elements[0];
|
||||
expect([element.width, element.height]).toEqual(dimensions);
|
||||
expect([element.x, element.y]).toEqual(topLeft);
|
||||
});
|
||||
|
||||
it.each`
|
||||
handle | move | dimensions | topLeft
|
||||
${"n"} | ${[_, -100]} | ${[200, 200]} | ${[-50, -100]}
|
||||
${"nw"} | ${[-300, -200]} | ${[400, 400]} | ${[-300, -300]}
|
||||
${"sw"} | ${[40, -20]} | ${[80, 80]} | ${[20, 0]}
|
||||
`(
|
||||
"resizes with fixed side ratios on handle $handle",
|
||||
({ handle, move, dimensions, topLeft }) => {
|
||||
render(<App />);
|
||||
const rectangle = UI.createElement("rectangle", elemData);
|
||||
resize(rectangle, handle, move, { shift: true });
|
||||
const element = h.elements[0];
|
||||
expect([element.width, element.height]).toEqual(dimensions);
|
||||
expect([element.x, element.y]).toEqual(topLeft);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
handle | move | dimensions | topLeft
|
||||
${"nw"} | ${[0, 120]} | ${[100, 100]} | ${[0, 100]}
|
||||
${"ne"} | ${[-120, 0]} | ${[100, 100]} | ${[-100, 0]}
|
||||
${"sw"} | ${[200, -200]} | ${[100, 100]} | ${[100, -100]}
|
||||
${"n"} | ${[_, 150]} | ${[50, 50]} | ${[25, 100]}
|
||||
`(
|
||||
"Flips while resizing and keeping side ratios on handle $handle",
|
||||
({ handle, move, dimensions, topLeft }) => {
|
||||
render(<App />);
|
||||
const rectangle = UI.createElement("rectangle", elemData);
|
||||
resize(rectangle, handle, move, { shift: true });
|
||||
const element = h.elements[0];
|
||||
expect([element.width, element.height]).toEqual(dimensions);
|
||||
expect([element.x, element.y]).toEqual(topLeft);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
handle | move | dimensions | topLeft
|
||||
${"ne"} | ${[50, -100]} | ${[200, 300]} | ${[-50, -100]}
|
||||
${"s"} | ${[_, -20]} | ${[100, 60]} | ${[0, 20]}
|
||||
`(
|
||||
"Resizes from center on handle $handle",
|
||||
({ handle, move, dimensions, topLeft }) => {
|
||||
render(<App />);
|
||||
const rectangle = UI.createElement("rectangle", elemData);
|
||||
resize(rectangle, handle, move, { alt: true });
|
||||
const element = h.elements[0];
|
||||
expect([element.width, element.height]).toEqual(dimensions);
|
||||
expect([element.x, element.y]).toEqual(topLeft);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
handle | move | dimensions | topLeft
|
||||
${"nw"} | ${[100, 120]} | ${[140, 140]} | ${[-20, -20]}
|
||||
${"e"} | ${[-130, _]} | ${[160, 160]} | ${[-30, -30]}
|
||||
`(
|
||||
"Resizes from center, flips and keeps side rations on handle $handle",
|
||||
({ handle, move, dimensions, topLeft }) => {
|
||||
render(<App />);
|
||||
const rectangle = UI.createElement("rectangle", elemData);
|
||||
resize(rectangle, handle, move, { alt: true, shift: true });
|
||||
const element = h.elements[0];
|
||||
expect([element.width, element.height]).toEqual(dimensions);
|
||||
expect([element.x, element.y]).toEqual(topLeft);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("resize element with aspect ratio when SHIFT is clicked", () => {
|
||||
it("rectangle", async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
|
||||
const rectangle = UI.createElement("rectangle", {
|
||||
x: 0,
|
||||
width: 30,
|
||||
height: 50,
|
||||
});
|
||||
|
||||
mouse.select(rectangle);
|
||||
|
||||
const se = getTransformHandles(rectangle, h.state.zoom, "mouse").se!;
|
||||
const clientX = se[0] + se[2] / 2;
|
||||
const clientY = se[1] + se[3] / 2;
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.reset();
|
||||
mouse.down(clientX, clientY);
|
||||
mouse.move(1, 1);
|
||||
mouse.up();
|
||||
});
|
||||
expect([h.elements[0].width, h.elements[0].height]).toEqual([51, 51]);
|
||||
function resize(
|
||||
element: ExcalidrawElement,
|
||||
handleDir: TransformHandleDirection,
|
||||
mouseMove: [number, number],
|
||||
keyboardModifiers: KeyboardModifiers = {},
|
||||
) {
|
||||
mouse.select(element);
|
||||
const handle = getTransformHandles(element, h.state.zoom, "mouse")[
|
||||
handleDir
|
||||
]!;
|
||||
const clientX = handle[0] + handle[2] / 2;
|
||||
const clientY = handle[1] + handle[3] / 2;
|
||||
Keyboard.withModifierKeys(keyboardModifiers, () => {
|
||||
mouse.reset();
|
||||
mouse.down(clientX, clientY);
|
||||
mouse.move(mouseMove[0], mouseMove[1]);
|
||||
mouse.up();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue