resize.test.tsx 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353
  1. import { pointFrom } from "@excalidraw/math";
  2. import { Excalidraw } from "@excalidraw/excalidraw";
  3. import {
  4. KEYS,
  5. getSizeFromPoints,
  6. reseed,
  7. arrayToMap,
  8. } from "@excalidraw/common";
  9. import { API } from "@excalidraw/excalidraw/tests/helpers/api";
  10. import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
  11. import {
  12. render,
  13. unmountComponent,
  14. } from "@excalidraw/excalidraw/tests/test-utils";
  15. import type { LocalPoint } from "@excalidraw/math";
  16. import { isLinearElement } from "../src/typeChecks";
  17. import { resizeSingleElement } from "../src/resizeElements";
  18. import { LinearElementEditor } from "../src/linearElementEditor";
  19. import { getElementPointsCoords } from "../src/bounds";
  20. import type { Bounds } from "../src/bounds";
  21. import type {
  22. ExcalidrawElbowArrowElement,
  23. ExcalidrawFreeDrawElement,
  24. ExcalidrawLinearElement,
  25. } from "../src/types";
  26. unmountComponent();
  27. const { h } = window;
  28. const mouse = new Pointer("mouse");
  29. const getBoundsFromPoints = (
  30. element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
  31. ): Bounds => {
  32. if (isLinearElement(element)) {
  33. return getElementPointsCoords(element, element.points);
  34. }
  35. const { x, y, points } = element;
  36. const pointsX = points.map(([x]) => x);
  37. const pointsY = points.map(([, y]) => y);
  38. return [
  39. Math.min(...pointsX) + x,
  40. Math.min(...pointsY) + y,
  41. Math.max(...pointsX) + x,
  42. Math.max(...pointsY) + y,
  43. ];
  44. };
  45. beforeEach(async () => {
  46. localStorage.clear();
  47. reseed(7);
  48. mouse.reset();
  49. await render(<Excalidraw handleKeyboardGlobally={true} />);
  50. h.state.width = 1000;
  51. h.state.height = 1000;
  52. // The bounds of hand-drawn linear elements may change after flipping, so
  53. // removing this style for testing
  54. UI.clickTool("arrow");
  55. UI.clickByTitle("Architect");
  56. UI.clickTool("selection");
  57. });
  58. describe("generic element", () => {
  59. // = rectangle/diamond/ellipse
  60. describe("resizes", () => {
  61. it.each`
  62. handle | move | size | xy
  63. ${"n"} | ${[10, -27]} | ${[200, 127]} | ${[0, -27]}
  64. ${"e"} | ${[67, -45]} | ${[267, 100]} | ${[0, 0]}
  65. ${"s"} | ${[-50, -39]} | ${[200, 61]} | ${[0, 0]}
  66. ${"w"} | ${[20, 90]} | ${[180, 100]} | ${[20, 0]}
  67. ${"ne"} | ${[5, -33]} | ${[205, 133]} | ${[0, -33]}
  68. ${"se"} | ${[-30, -81]} | ${[170, 19]} | ${[0, 0]}
  69. ${"sw"} | ${[37, 25]} | ${[163, 125]} | ${[37, 0]}
  70. ${"nw"} | ${[-34, 42]} | ${[234, 58]} | ${[-34, 42]}
  71. `(
  72. "with handle $handle",
  73. async ({ handle, move, size: [width, height], xy: [x, y] }) => {
  74. const rectangle = UI.createElement("rectangle", {
  75. width: 200,
  76. height: 100,
  77. });
  78. UI.resize(rectangle, handle, move);
  79. expect(rectangle.x).toBeCloseTo(x);
  80. expect(rectangle.y).toBeCloseTo(y);
  81. expect(rectangle.width).toBeCloseTo(width);
  82. expect(rectangle.height).toBeCloseTo(height);
  83. expect(rectangle.angle).toBeCloseTo(0);
  84. },
  85. );
  86. });
  87. describe("flips while resizing", () => {
  88. it.each`
  89. handle | move | size | xy
  90. ${"n"} | ${[15, 139]} | ${[200, 39]} | ${[0, 100]}
  91. ${"e"} | ${[-245, 67]} | ${[45, 100]} | ${[-45, 0]}
  92. ${"s"} | ${[-26, -210]} | ${[200, 110]} | ${[0, -110]}
  93. ${"w"} | ${[241, 0]} | ${[41, 100]} | ${[200, 0]}
  94. ${"ne"} | ${[-250, 125]} | ${[50, 25]} | ${[-50, 100]}
  95. ${"se"} | ${[-283, -58]} | ${[83, 42]} | ${[-83, 0]}
  96. ${"sw"} | ${[40, -123]} | ${[160, 23]} | ${[40, -23]}
  97. ${"nw"} | ${[270, 133]} | ${[70, 33]} | ${[200, 100]}
  98. `(
  99. "with handle $handle",
  100. async ({ handle, move, size: [width, height], xy: [x, y] }) => {
  101. const rectangle = UI.createElement("rectangle", {
  102. width: 200,
  103. height: 100,
  104. });
  105. UI.resize(rectangle, handle, move);
  106. expect(rectangle.x).toBeCloseTo(x);
  107. expect(rectangle.y).toBeCloseTo(y);
  108. expect(rectangle.width).toBeCloseTo(width);
  109. expect(rectangle.height).toBeCloseTo(height);
  110. expect(rectangle.angle).toBeCloseTo(0);
  111. },
  112. );
  113. });
  114. it("resizes with locked aspect ratio", async () => {
  115. const rectangle = UI.createElement("rectangle", {
  116. width: 200,
  117. height: 100,
  118. });
  119. UI.resize(rectangle, "se", [100, 10], { shift: true });
  120. expect(rectangle.x).toBeCloseTo(0);
  121. expect(rectangle.y).toBeCloseTo(0);
  122. expect(rectangle.width).toBeCloseTo(300);
  123. expect(rectangle.height).toBeCloseTo(150);
  124. expect(rectangle.angle).toBeCloseTo(0);
  125. UI.resize(rectangle, "n", [30, 50], { shift: true });
  126. expect(rectangle.x).toBeCloseTo(50);
  127. expect(rectangle.y).toBeCloseTo(50);
  128. expect(rectangle.width).toBeCloseTo(200);
  129. expect(rectangle.height).toBeCloseTo(100);
  130. expect(rectangle.angle).toBeCloseTo(0);
  131. });
  132. it("resizes from center", async () => {
  133. const rectangle = UI.createElement("rectangle", {
  134. width: 200,
  135. height: 100,
  136. });
  137. UI.resize(rectangle, "nw", [20, 10], { alt: true });
  138. expect(rectangle.x).toBeCloseTo(20);
  139. expect(rectangle.y).toBeCloseTo(10);
  140. expect(rectangle.width).toBeCloseTo(160);
  141. expect(rectangle.height).toBeCloseTo(80);
  142. expect(rectangle.angle).toBeCloseTo(0);
  143. UI.resize(rectangle, "e", [15, 43], { alt: true });
  144. expect(rectangle.x).toBeCloseTo(5);
  145. expect(rectangle.y).toBeCloseTo(10);
  146. expect(rectangle.width).toBeCloseTo(190);
  147. expect(rectangle.height).toBeCloseTo(80);
  148. expect(rectangle.angle).toBeCloseTo(0);
  149. });
  150. it("resizes with bound arrow", async () => {
  151. const rectangle = UI.createElement("rectangle", {
  152. width: 200,
  153. height: 100,
  154. });
  155. const arrow = UI.createElement("arrow", {
  156. x: -30,
  157. y: 50,
  158. width: 28,
  159. height: 5,
  160. });
  161. expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
  162. UI.resize(rectangle, "e", [40, 0]);
  163. expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
  164. UI.resize(rectangle, "w", [50, 0]);
  165. expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
  166. expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
  167. });
  168. it("resizes with a label", async () => {
  169. const rectangle = UI.createElement("rectangle", {
  170. width: 200,
  171. height: 100,
  172. });
  173. const label = await UI.editText(rectangle, "Hello world");
  174. UI.resize(rectangle, "se", [50, 50]);
  175. expect(label.x + label.width / 2).toBeCloseTo(
  176. rectangle.x + rectangle.width / 2,
  177. );
  178. expect(label.y + label.height / 2).toBeCloseTo(
  179. rectangle.y + rectangle.height / 2,
  180. );
  181. expect(label.angle).toBeCloseTo(rectangle.angle);
  182. expect(label.fontSize).toEqual(20);
  183. UI.resize(rectangle, "w", [190, 0]);
  184. expect(label.x + label.width / 2).toBeCloseTo(
  185. rectangle.x + rectangle.width / 2,
  186. );
  187. expect(label.y + label.height / 2).toBeCloseTo(
  188. rectangle.y + rectangle.height / 2,
  189. );
  190. expect(label.angle).toBeCloseTo(rectangle.angle);
  191. expect(label.fontSize).toEqual(20);
  192. });
  193. });
  194. describe.each(["line", "freedraw"] as const)("%s element", (type) => {
  195. const points: Record<typeof type, LocalPoint[]> = {
  196. line: [
  197. pointFrom(0, 0),
  198. pointFrom(60, -20),
  199. pointFrom(20, 40),
  200. pointFrom(-40, 0),
  201. ],
  202. freedraw: [
  203. pointFrom(0, 0),
  204. pointFrom(-2.474600807561444, 41.021700699972),
  205. pointFrom(3.6627956000014024, 47.84174560617245),
  206. pointFrom(40.495224145598115, 47.15909710753482),
  207. ],
  208. };
  209. it("resizes", async () => {
  210. const element = UI.createElement("freedraw", { points: points.freedraw });
  211. const bounds = getBoundsFromPoints(element);
  212. UI.resize(element, "ne", [30, -60]);
  213. const newBounds = getBoundsFromPoints(element);
  214. expect(newBounds[0]).toBeCloseTo(bounds[0]);
  215. expect(newBounds[1]).toBeCloseTo(bounds[1] - 60);
  216. expect(newBounds[2]).toBeCloseTo(bounds[2] + 30);
  217. expect(newBounds[3]).toBeCloseTo(bounds[3]);
  218. expect(element.angle).toBeCloseTo(0);
  219. });
  220. it("flips while resizing", async () => {
  221. const element = UI.createElement("freedraw", { points: points.freedraw });
  222. const bounds = getBoundsFromPoints(element);
  223. UI.resize(element, "sw", [140, -80]);
  224. const newBounds = getBoundsFromPoints(element);
  225. expect(newBounds[0]).toBeCloseTo(bounds[2]);
  226. expect(newBounds[1]).toBeCloseTo(bounds[3] - 80);
  227. expect(newBounds[2]).toBeCloseTo(bounds[0] + 140);
  228. expect(newBounds[3]).toBeCloseTo(bounds[1]);
  229. expect(element.angle).toBeCloseTo(0);
  230. });
  231. it("resizes with locked aspect ratio", async () => {
  232. const element = UI.createElement("freedraw", { points: points.freedraw });
  233. const bounds = getBoundsFromPoints(element);
  234. UI.resize(element, "ne", [30, -60], { shift: true });
  235. const newBounds = getBoundsFromPoints(element);
  236. const scale = 1 + 60 / (bounds[3] - bounds[1]);
  237. expect(newBounds[0]).toBeCloseTo(bounds[0]);
  238. expect(newBounds[1]).toBeCloseTo(bounds[1] - 60);
  239. expect(newBounds[2]).toBeCloseTo(
  240. bounds[0] + (bounds[2] - bounds[0]) * scale,
  241. );
  242. expect(newBounds[3]).toBeCloseTo(bounds[3]);
  243. expect(element.angle).toBeCloseTo(0);
  244. });
  245. it("resizes from center", async () => {
  246. const element = UI.createElement("freedraw", { points: points.freedraw });
  247. const bounds = getBoundsFromPoints(element);
  248. UI.resize(element, "nw", [-20, -30], { alt: true });
  249. const newBounds = getBoundsFromPoints(element);
  250. expect(newBounds[0]).toBeCloseTo(bounds[0] - 20);
  251. expect(newBounds[1]).toBeCloseTo(bounds[1] - 30);
  252. expect(newBounds[2]).toBeCloseTo(bounds[2] + 20);
  253. expect(newBounds[3]).toBeCloseTo(bounds[3] + 30);
  254. expect(element.angle).toBeCloseTo(0);
  255. });
  256. });
  257. describe("line element", () => {
  258. const points: LocalPoint[] = [
  259. pointFrom(0, 0),
  260. pointFrom(60, -20),
  261. pointFrom(20, 40),
  262. pointFrom(-40, 0),
  263. ];
  264. it("resizes", async () => {
  265. UI.createElement("line", { points });
  266. const element = h.elements[0] as ExcalidrawLinearElement;
  267. const {
  268. x: prevX,
  269. y: prevY,
  270. width: prevWidth,
  271. height: prevHeight,
  272. } = element;
  273. const nextWidth = prevWidth + 30;
  274. const nextHeight = prevHeight + 30;
  275. resizeSingleElement(
  276. nextWidth,
  277. nextHeight,
  278. element,
  279. element,
  280. h.app.scene.getNonDeletedElementsMap(),
  281. h.app.scene.getNonDeletedElementsMap(),
  282. "ne",
  283. );
  284. expect(element.x).not.toBe(prevX);
  285. expect(element.y).not.toBe(prevY);
  286. expect(element.width).toBe(nextWidth);
  287. expect(element.height).toBe(nextHeight);
  288. expect(element.points[0]).toEqual([0, 0]);
  289. const { width, height } = getSizeFromPoints(element.points);
  290. expect(width).toBe(element.width);
  291. expect(height).toBe(element.height);
  292. });
  293. it("flips while resizing", async () => {
  294. UI.createElement("line", { points });
  295. const element = h.elements[0] as ExcalidrawLinearElement;
  296. const {
  297. width: prevWidth,
  298. height: prevHeight,
  299. points: prevPoints,
  300. } = element;
  301. const nextWidth = prevWidth * -1;
  302. const nextHeight = prevHeight * -1;
  303. resizeSingleElement(
  304. nextWidth,
  305. nextHeight,
  306. element,
  307. element,
  308. h.app.scene.getNonDeletedElementsMap(),
  309. h.app.scene.getNonDeletedElementsMap(),
  310. "se",
  311. );
  312. expect(element.width).toBe(prevWidth);
  313. expect(element.height).toBe(prevHeight);
  314. element.points.forEach((point, idx) => {
  315. expect(point[0]).toBeCloseTo(prevPoints[idx][0] * -1);
  316. expect(point[1]).toBeCloseTo(prevPoints[idx][1] * -1);
  317. });
  318. });
  319. it("resizes with locked aspect ratio", async () => {
  320. UI.createElement("line", { points });
  321. const element = h.elements[0] as ExcalidrawLinearElement;
  322. const { width: prevWidth, height: prevHeight } = element;
  323. UI.resize(element, "ne", [30, -60], { shift: true });
  324. const scaleHeight = element.width / prevWidth;
  325. const scaleWidth = element.height / prevHeight;
  326. expect(scaleHeight).toBeCloseTo(scaleWidth);
  327. });
  328. it("resizes from center", async () => {
  329. UI.createElement("line", {
  330. points: [
  331. pointFrom(0, 0),
  332. pointFrom(338.05644048727373, -180.4761618151104),
  333. pointFrom(338.05644048727373, 180.4761618151104),
  334. pointFrom(-338.05644048727373, 180.4761618151104),
  335. pointFrom(-338.05644048727373, -180.4761618151104),
  336. ],
  337. });
  338. const element = h.elements[0] as ExcalidrawLinearElement;
  339. const {
  340. x: prevX,
  341. y: prevY,
  342. width: prevWidth,
  343. height: prevHeight,
  344. } = element;
  345. const prevSmallestX = Math.min(...element.points.map((p) => p[0]));
  346. const prevBiggestX = Math.max(...element.points.map((p) => p[0]));
  347. resizeSingleElement(
  348. prevWidth + 20,
  349. prevHeight,
  350. element,
  351. element,
  352. h.app.scene.getNonDeletedElementsMap(),
  353. h.app.scene.getNonDeletedElementsMap(),
  354. "e",
  355. {
  356. shouldResizeFromCenter: true,
  357. },
  358. );
  359. expect(element.width).toBeCloseTo(prevWidth + 20);
  360. expect(element.height).toBeCloseTo(prevHeight);
  361. expect(element.x).toBeCloseTo(prevX);
  362. expect(element.y).toBeCloseTo(prevY);
  363. const smallestX = Math.min(...element.points.map((p) => p[0]));
  364. const biggestX = Math.max(...element.points.map((p) => p[0]));
  365. expect(prevSmallestX - smallestX).toBeCloseTo(10);
  366. expect(biggestX - prevBiggestX).toBeCloseTo(10);
  367. });
  368. });
  369. describe("arrow element", () => {
  370. it("resizes with a label", async () => {
  371. const arrow = UI.createElement("arrow", {
  372. points: [
  373. pointFrom(0, 0),
  374. pointFrom(40, 140),
  375. pointFrom(80, 60), // label's anchor
  376. pointFrom(180, 20),
  377. pointFrom(200, 120),
  378. ],
  379. });
  380. const label = await UI.editText(arrow, "Hello");
  381. const elementsMap = arrayToMap(h.elements);
  382. UI.resize(arrow, "se", [50, 30]);
  383. let labelPos = LinearElementEditor.getBoundTextElementPosition(
  384. arrow,
  385. label,
  386. elementsMap,
  387. );
  388. expect(labelPos.x + label.width / 2).toBeCloseTo(
  389. arrow.x + arrow.points[2][0],
  390. );
  391. expect(labelPos.y + label.height / 2).toBeCloseTo(
  392. arrow.y + arrow.points[2][1],
  393. );
  394. expect(label.angle).toBeCloseTo(0);
  395. expect(label.fontSize).toEqual(20);
  396. UI.resize(arrow, "w", [20, 0]);
  397. labelPos = LinearElementEditor.getBoundTextElementPosition(
  398. arrow,
  399. label,
  400. elementsMap,
  401. );
  402. expect(labelPos.x + label.width / 2).toBeCloseTo(
  403. arrow.x + arrow.points[2][0],
  404. );
  405. expect(labelPos.y + label.height / 2).toBeCloseTo(
  406. arrow.y + arrow.points[2][1],
  407. );
  408. expect(label.angle).toBeCloseTo(0);
  409. expect(label.fontSize).toEqual(20);
  410. });
  411. it("flips the fixed point binding on negative resize for single bindable", () => {
  412. const rectangle = UI.createElement("rectangle", {
  413. x: -100,
  414. y: -75,
  415. width: 95,
  416. height: 100,
  417. });
  418. UI.clickTool("arrow");
  419. UI.clickOnTestId("elbow-arrow");
  420. mouse.reset();
  421. mouse.moveTo(-5, 0);
  422. mouse.click();
  423. mouse.moveTo(120, 200);
  424. mouse.click();
  425. const arrow = h.scene.getSelectedElements(
  426. h.state,
  427. )[0] as ExcalidrawElbowArrowElement;
  428. expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
  429. expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
  430. UI.resize(rectangle, "se", [-200, -150]);
  431. expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
  432. expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
  433. });
  434. it("flips the fixed point binding on negative resize for group selection", () => {
  435. const rectangle = UI.createElement("rectangle", {
  436. x: -100,
  437. y: -75,
  438. width: 95,
  439. height: 100,
  440. });
  441. UI.clickTool("arrow");
  442. UI.clickOnTestId("elbow-arrow");
  443. mouse.reset();
  444. mouse.moveTo(-5, 0);
  445. mouse.click();
  446. mouse.moveTo(120, 200);
  447. mouse.click();
  448. const arrow = h.scene.getSelectedElements(
  449. h.state,
  450. )[0] as ExcalidrawElbowArrowElement;
  451. expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
  452. expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
  453. UI.resize([rectangle, arrow], "nw", [300, 350]);
  454. expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
  455. expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
  456. });
  457. });
  458. describe("text element", () => {
  459. it("resizes", async () => {
  460. const text = UI.createElement("text");
  461. await UI.editText(text, "hello\nworld");
  462. const { width, height, fontSize } = text;
  463. const scale = 40 / height + 1;
  464. UI.resize(text, "se", [30, 40]);
  465. expect(text.x).toBeCloseTo(0);
  466. expect(text.y).toBeCloseTo(0);
  467. expect(text.width).toBeCloseTo(width * scale);
  468. expect(text.height).toBeCloseTo(height * scale);
  469. expect(text.angle).toBeCloseTo(0);
  470. expect(text.fontSize).toBeCloseTo(fontSize * scale);
  471. });
  472. // TODO enable this test after adding single text element flipping
  473. it.skip("flips while resizing", async () => {
  474. const text = UI.createElement("text");
  475. await UI.editText(text, "hello\nworld");
  476. const { width, height, fontSize } = text;
  477. const scale = 100 / width - 1;
  478. UI.resize(text, "nw", [100, 80]);
  479. expect(text.x).toBeCloseTo(width);
  480. expect(text.y).toBeCloseTo(height);
  481. expect(text.width).toBeCloseTo(width * scale);
  482. expect(text.height).toBeCloseTo(height * scale);
  483. expect(text.angle).toBeCloseTo(0);
  484. expect(text.fontSize).toBeCloseTo(fontSize * scale);
  485. });
  486. // TODO enable this test after fixing text resizing from center
  487. it.skip("resizes from center", async () => {
  488. const text = UI.createElement("text");
  489. await UI.editText(text, "hello\nworld");
  490. const { x, y, width, height, fontSize } = text;
  491. const scale = 80 / height + 1;
  492. UI.resize(text, "nw", [-25, -40], { alt: true });
  493. expect(text.x).toBeCloseTo(x - ((scale - 1) * width) / 2);
  494. expect(text.y).toBeCloseTo(y - 40);
  495. expect(text.width).toBeCloseTo(width * scale);
  496. expect(text.height).toBeCloseTo(height * scale);
  497. expect(text.angle).toBeCloseTo(0);
  498. expect(text.fontSize).toBeCloseTo(fontSize * scale);
  499. });
  500. it("resizes with bound arrow", async () => {
  501. const text = UI.createElement("text");
  502. await UI.editText(text, "hello\nworld");
  503. const boundArrow = UI.createElement("arrow", {
  504. x: -30,
  505. y: 25,
  506. width: 28,
  507. height: 5,
  508. });
  509. expect(boundArrow.endBinding?.elementId).toEqual(text.id);
  510. UI.resize(text, "ne", [40, 0]);
  511. expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(30);
  512. const textWidth = text.width;
  513. const scale = 20 / text.height;
  514. UI.resize(text, "nw", [50, 20]);
  515. expect(boundArrow.endBinding?.elementId).toEqual(text.id);
  516. expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(
  517. 30 + textWidth * scale,
  518. );
  519. });
  520. it("updates font size via keyboard", async () => {
  521. const text = UI.createElement("text");
  522. await UI.editText(text, "abc");
  523. const { fontSize } = text;
  524. mouse.select(text);
  525. Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => {
  526. Keyboard.keyDown(KEYS.CHEVRON_RIGHT);
  527. expect(text.fontSize).toBe(fontSize * 1.1);
  528. Keyboard.keyDown(KEYS.CHEVRON_LEFT);
  529. expect(text.fontSize).toBe(fontSize);
  530. });
  531. });
  532. // text can be resized from sides
  533. it("can be resized from e", async () => {
  534. const text = UI.createElement("text");
  535. await UI.editText(text, "Excalidraw\nEditor");
  536. const width = text.width;
  537. const height = text.height;
  538. UI.resize(text, "e", [30, 0]);
  539. expect(text.width).toBe(width + 30);
  540. expect(text.height).toBe(height);
  541. UI.resize(text, "e", [-30, 0]);
  542. expect(text.width).toBe(width);
  543. expect(text.height).toBe(height);
  544. });
  545. it("can be resized from w", async () => {
  546. const text = UI.createElement("text");
  547. await UI.editText(text, "Excalidraw\nEditor");
  548. const width = text.width;
  549. const height = text.height;
  550. UI.resize(text, "w", [-50, 0]);
  551. expect(text.width).toBe(width + 50);
  552. expect(text.height).toBe(height);
  553. UI.resize(text, "w", [50, 0]);
  554. expect(text.width).toBe(width);
  555. expect(text.height).toBe(height);
  556. });
  557. it("wraps when width is narrower than texts inside", async () => {
  558. const text = UI.createElement("text");
  559. await UI.editText(text, "Excalidraw\nEditor");
  560. const prevWidth = text.width;
  561. const prevHeight = text.height;
  562. const prevText = text.text;
  563. UI.resize(text, "w", [50, 0]);
  564. expect(text.width).toBe(prevWidth - 50);
  565. expect(text.height).toBeGreaterThan(prevHeight);
  566. expect(text.text).not.toEqual(prevText);
  567. expect(text.autoResize).toBe(false);
  568. UI.resize(text, "w", [-50, 0]);
  569. expect(text.width).toBe(prevWidth);
  570. expect(text.height).toEqual(prevHeight);
  571. expect(text.text).toEqual(prevText);
  572. expect(text.autoResize).toBe(false);
  573. UI.resize(text, "e", [-20, 0]);
  574. expect(text.width).toBe(prevWidth - 20);
  575. expect(text.height).toBeGreaterThan(prevHeight);
  576. expect(text.text).not.toEqual(prevText);
  577. expect(text.autoResize).toBe(false);
  578. UI.resize(text, "e", [20, 0]);
  579. expect(text.width).toBe(prevWidth);
  580. expect(text.height).toEqual(prevHeight);
  581. expect(text.text).toEqual(prevText);
  582. expect(text.autoResize).toBe(false);
  583. });
  584. it("keeps properties when wrapped", async () => {
  585. const text = UI.createElement("text");
  586. await UI.editText(text, "Excalidraw\nEditor");
  587. const alignment = text.textAlign;
  588. const fontSize = text.fontSize;
  589. const fontFamily = text.fontFamily;
  590. UI.resize(text, "e", [-60, 0]);
  591. expect(text.textAlign).toBe(alignment);
  592. expect(text.fontSize).toBe(fontSize);
  593. expect(text.fontFamily).toBe(fontFamily);
  594. expect(text.autoResize).toBe(false);
  595. UI.resize(text, "e", [60, 0]);
  596. expect(text.textAlign).toBe(alignment);
  597. expect(text.fontSize).toBe(fontSize);
  598. expect(text.fontFamily).toBe(fontFamily);
  599. expect(text.autoResize).toBe(false);
  600. });
  601. it("has a minimum width when wrapped", async () => {
  602. const text = UI.createElement("text");
  603. await UI.editText(text, "Excalidraw\nEditor");
  604. const width = text.width;
  605. UI.resize(text, "e", [-width, 0]);
  606. expect(text.width).not.toEqual(0);
  607. UI.resize(text, "e", [width - text.width, 0]);
  608. expect(text.width).toEqual(width);
  609. expect(text.autoResize).toBe(false);
  610. UI.resize(text, "w", [width, 0]);
  611. expect(text.width).not.toEqual(0);
  612. UI.resize(text, "w", [text.width - width, 0]);
  613. expect(text.width).toEqual(width);
  614. expect(text.autoResize).toBe(false);
  615. });
  616. });
  617. describe("image element", () => {
  618. it("resizes", async () => {
  619. const image = API.createElement({ type: "image", width: 100, height: 100 });
  620. API.setElements([image]);
  621. UI.resize(image, "ne", [-20, -30]);
  622. expect(image.x).toBeCloseTo(0);
  623. expect(image.y).toBeCloseTo(-30);
  624. expect(image.width).toBeCloseTo(130);
  625. expect(image.height).toBeCloseTo(130);
  626. expect(image.angle).toBeCloseTo(0);
  627. expect(image.scale).toEqual([1, 1]);
  628. });
  629. it("flips while resizing", async () => {
  630. const image = API.createElement({ type: "image", width: 100, height: 100 });
  631. API.setElements([image]);
  632. UI.resize(image, "sw", [150, -150]);
  633. expect(image.x).toBeCloseTo(100);
  634. expect(image.y).toBeCloseTo(-50);
  635. expect(image.width).toBeCloseTo(50);
  636. expect(image.height).toBeCloseTo(50);
  637. expect(image.angle).toBeCloseTo(0);
  638. expect(image.scale).toEqual([-1, -1]);
  639. });
  640. it("resizes with locked/unlocked aspect ratio", async () => {
  641. const image = API.createElement({ type: "image", width: 100, height: 100 });
  642. API.setElements([image]);
  643. UI.resize(image, "ne", [30, -20]);
  644. expect(image.x).toBeCloseTo(0);
  645. expect(image.y).toBeCloseTo(-30);
  646. expect(image.width).toBeCloseTo(130);
  647. expect(image.height).toBeCloseTo(130);
  648. UI.resize(image, "ne", [-30, 50], { shift: true });
  649. expect(image.x).toBeCloseTo(0);
  650. expect(image.y).toBeCloseTo(20);
  651. expect(image.width).toBeCloseTo(100);
  652. expect(image.height).toBeCloseTo(80);
  653. });
  654. it("resizes from center", async () => {
  655. const image = API.createElement({ type: "image", width: 100, height: 100 });
  656. API.setElements([image]);
  657. UI.resize(image, "nw", [25, 15], { alt: true });
  658. expect(image.x).toBeCloseTo(15);
  659. expect(image.y).toBeCloseTo(15);
  660. expect(image.width).toBeCloseTo(70);
  661. expect(image.height).toBeCloseTo(70);
  662. expect(image.angle).toBeCloseTo(0);
  663. expect(image.scale).toEqual([1, 1]);
  664. });
  665. it("resizes with bound arrow", async () => {
  666. const image = API.createElement({
  667. type: "image",
  668. width: 100,
  669. height: 100,
  670. });
  671. API.setElements([image]);
  672. const arrow = UI.createElement("arrow", {
  673. x: -30,
  674. y: 50,
  675. width: 28,
  676. height: 5,
  677. });
  678. expect(arrow.endBinding?.elementId).toEqual(image.id);
  679. UI.resize(image, "ne", [40, 0]);
  680. expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0);
  681. const imageWidth = image.width;
  682. const scale = 20 / image.height;
  683. UI.resize(image, "nw", [50, 20]);
  684. expect(arrow.endBinding?.elementId).toEqual(image.id);
  685. expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
  686. 30 + imageWidth * scale,
  687. 0,
  688. );
  689. });
  690. });
  691. describe("multiple selection", () => {
  692. it("resizes with generic elements", async () => {
  693. const rectangle = UI.createElement("rectangle", {
  694. position: 0,
  695. width: 100,
  696. height: 80,
  697. });
  698. const rectLabel = await UI.editText(rectangle, "hello\nworld");
  699. const diamond = UI.createElement("diamond", {
  700. x: 140,
  701. y: 40,
  702. size: 80,
  703. });
  704. const ellipse = UI.createElement("ellipse", {
  705. x: 40,
  706. y: 100,
  707. width: 80,
  708. height: 60,
  709. });
  710. const selectionWidth = 220;
  711. const selectionHeight = 160;
  712. const move = [50, 30] as [number, number];
  713. const scale = Math.max(
  714. 1 + move[0] / selectionWidth,
  715. 1 + move[1] / selectionHeight,
  716. );
  717. UI.resize([rectangle, diamond, ellipse], "se", move, {
  718. shift: true,
  719. });
  720. expect(rectangle.x).toBeCloseTo(0);
  721. expect(rectangle.y).toBeCloseTo(0);
  722. expect(rectangle.width).toBeCloseTo(100 * scale);
  723. expect(rectangle.height).toBeCloseTo(80 * scale);
  724. expect(rectangle.angle).toEqual(0);
  725. expect(rectLabel.type).toEqual("text");
  726. expect(rectLabel.containerId).toEqual(rectangle.id);
  727. expect(rectLabel.x + rectLabel.width / 2).toBeCloseTo(
  728. rectangle.x + rectangle.width / 2,
  729. );
  730. expect(rectLabel.y + rectLabel.height / 2).toBeCloseTo(
  731. rectangle.y + rectangle.height / 2,
  732. );
  733. expect(rectLabel.angle).toEqual(0);
  734. expect(rectLabel.fontSize).toBeCloseTo(20 * scale, -1);
  735. expect(diamond.x).toBeCloseTo(140 * scale);
  736. expect(diamond.y).toBeCloseTo(40 * scale);
  737. expect(diamond.width).toBeCloseTo(80 * scale);
  738. expect(diamond.height).toBeCloseTo(80 * scale);
  739. expect(diamond.angle).toEqual(0);
  740. expect(ellipse.x).toBeCloseTo(40 * scale);
  741. expect(ellipse.y).toBeCloseTo(100 * scale);
  742. expect(ellipse.width).toBeCloseTo(80 * scale);
  743. expect(ellipse.height).toBeCloseTo(60 * scale);
  744. expect(ellipse.angle).toEqual(0);
  745. });
  746. it("resizes with linear elements > 2 points", async () => {
  747. UI.clickTool("line");
  748. UI.clickByTitle("Sharp");
  749. const line = UI.createElement("line", {
  750. x: 60,
  751. y: 40,
  752. points: [
  753. pointFrom(0, 0),
  754. pointFrom(-40, 40),
  755. pointFrom(-60, 0),
  756. pointFrom(0, -40),
  757. pointFrom(40, 20),
  758. pointFrom(0, 40),
  759. ],
  760. });
  761. const freedraw = UI.createElement("freedraw", {
  762. x: 63.56072661326618,
  763. y: 100,
  764. points: [
  765. pointFrom(0, 0),
  766. pointFrom(-43.56072661326618, 18.15048126846341),
  767. pointFrom(-43.56072661326618, 29.041198460587566),
  768. pointFrom(-38.115368017204105, 42.652452795512204),
  769. pointFrom(-19.964886748740696, 66.24829266003775),
  770. pointFrom(19.056612930986716, 77.1390098521619),
  771. ],
  772. });
  773. const selectionWidth = 100;
  774. const selectionHeight = 177.1390098521619;
  775. const move = [-25, -25] as [number, number];
  776. const scale = Math.max(
  777. 1 + move[0] / selectionWidth,
  778. 1 + move[1] / selectionHeight,
  779. );
  780. UI.resize([line, freedraw], "se", move, {
  781. shift: true,
  782. });
  783. expect(line.x).toBeCloseTo(60 * scale);
  784. expect(line.y).toBeCloseTo(40 * scale);
  785. expect(line.width).toBeCloseTo(100 * scale);
  786. expect(line.height).toBeCloseTo(80 * scale);
  787. expect(line.angle).toEqual(0);
  788. expect(freedraw.x).toBeCloseTo(63.56072661326618 * scale);
  789. expect(freedraw.y).toBeCloseTo(100 * scale);
  790. expect(freedraw.width).toBeCloseTo(62.6173395442529 * scale);
  791. expect(freedraw.height).toBeCloseTo(77.1390098521619 * scale);
  792. expect(freedraw.angle).toEqual(0);
  793. });
  794. it("resizes with 2-point lines", async () => {
  795. const horizLine = UI.createElement("line", {
  796. position: 0,
  797. width: 120,
  798. height: 0,
  799. });
  800. const vertLine = UI.createElement("line", {
  801. x: 0,
  802. y: 20,
  803. width: 0,
  804. height: 80,
  805. });
  806. const diagLine = UI.createElement("line", {
  807. position: 40,
  808. size: 60,
  809. });
  810. const selectionWidth = 120;
  811. const selectionHeight = 100;
  812. const move = [40, 40] as [number, number];
  813. const scale = Math.max(
  814. 1 - move[0] / selectionWidth,
  815. 1 - move[1] / selectionHeight,
  816. );
  817. UI.resize([horizLine, vertLine, diagLine], "nw", move, {
  818. shift: true,
  819. });
  820. expect(horizLine.x).toBeCloseTo(selectionWidth * (1 - scale));
  821. expect(horizLine.y).toBeCloseTo(selectionHeight * (1 - scale));
  822. expect(horizLine.width).toBeCloseTo(120 * scale);
  823. expect(horizLine.height).toBeCloseTo(0);
  824. expect(horizLine.angle).toEqual(0);
  825. expect(vertLine.x).toBeCloseTo(selectionWidth * (1 - scale));
  826. expect(vertLine.y).toBeCloseTo((selectionHeight - 20) * (1 - scale) + 20);
  827. expect(vertLine.width).toBeCloseTo(0);
  828. expect(vertLine.height).toBeCloseTo(80 * scale);
  829. expect(vertLine.angle).toEqual(0);
  830. expect(diagLine.x).toBeCloseTo((selectionWidth - 40) * (1 - scale) + 40);
  831. expect(diagLine.y).toBeCloseTo((selectionHeight - 40) * (1 - scale) + 40);
  832. expect(diagLine.width).toBeCloseTo(60 * scale);
  833. expect(diagLine.height).toBeCloseTo(60 * scale);
  834. expect(diagLine.angle).toEqual(0);
  835. });
  836. it("resizes with bound arrows", async () => {
  837. const rectangle = UI.createElement("rectangle", {
  838. position: 0,
  839. size: 100,
  840. });
  841. const leftBoundArrow = UI.createElement("arrow", {
  842. x: -110,
  843. y: 50,
  844. width: 100,
  845. height: 0,
  846. });
  847. const rightBoundArrow = UI.createElement("arrow", {
  848. x: 210,
  849. y: 50,
  850. width: -100,
  851. height: 0,
  852. });
  853. const selectionWidth = 210;
  854. const selectionHeight = 100;
  855. const move = [40, 40] as [number, number];
  856. const scale = Math.max(
  857. 1 - move[0] / selectionWidth,
  858. 1 - move[1] / selectionHeight,
  859. );
  860. const leftArrowBinding = { ...leftBoundArrow.endBinding };
  861. const rightArrowBinding = { ...rightBoundArrow.endBinding };
  862. delete rightArrowBinding.gap;
  863. UI.resize([rectangle, rightBoundArrow], "nw", move, {
  864. shift: true,
  865. });
  866. expect(leftBoundArrow.x).toBeCloseTo(-110);
  867. expect(leftBoundArrow.y).toBeCloseTo(50);
  868. expect(leftBoundArrow.width).toBeCloseTo(143, 0);
  869. expect(leftBoundArrow.height).toBeCloseTo(7, 0);
  870. expect(leftBoundArrow.angle).toEqual(0);
  871. expect(leftBoundArrow.startBinding).toBeNull();
  872. expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
  873. expect(leftBoundArrow.endBinding?.elementId).toBe(
  874. leftArrowBinding.elementId,
  875. );
  876. expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
  877. expect(rightBoundArrow.x).toBeCloseTo(210);
  878. expect(rightBoundArrow.y).toBeCloseTo(
  879. (selectionHeight - 50) * (1 - scale) + 50,
  880. );
  881. expect(rightBoundArrow.width).toBeCloseTo(100 * scale);
  882. expect(rightBoundArrow.height).toBeCloseTo(0);
  883. expect(rightBoundArrow.angle).toEqual(0);
  884. expect(rightBoundArrow.startBinding).toBeNull();
  885. expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
  886. expect(rightBoundArrow.endBinding?.elementId).toBe(
  887. rightArrowBinding.elementId,
  888. );
  889. expect(rightBoundArrow.endBinding?.focus).toBeCloseTo(
  890. rightArrowBinding.focus!,
  891. );
  892. });
  893. it("resizes with labeled arrows", async () => {
  894. const topArrow = UI.createElement("arrow", {
  895. x: 0,
  896. y: 20,
  897. width: 220,
  898. height: 0,
  899. });
  900. const topArrowLabel = await UI.editText(topArrow.get(), "lorem ipsum");
  901. UI.clickTool("text");
  902. UI.clickByTitle("Large");
  903. const bottomArrow = UI.createElement("arrow", {
  904. x: 0,
  905. y: 80,
  906. width: 220,
  907. height: 0,
  908. });
  909. const bottomArrowLabel = await UI.editText(
  910. bottomArrow.get(),
  911. "dolor\nsit amet",
  912. );
  913. const selectionWidth = 220;
  914. const selectionTop = 20 - topArrowLabel.height / 2;
  915. const move = [80, 0] as [number, number];
  916. const scale = move[0] / selectionWidth + 1;
  917. const elementsMap = arrayToMap(h.elements);
  918. UI.resize([topArrow.get(), bottomArrow.get()], "se", move, {
  919. shift: true,
  920. });
  921. const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
  922. topArrow,
  923. topArrowLabel,
  924. elementsMap,
  925. );
  926. const bottomArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
  927. bottomArrow,
  928. bottomArrowLabel,
  929. elementsMap,
  930. );
  931. expect(topArrow.x).toBeCloseTo(0);
  932. expect(topArrow.y).toBeCloseTo(selectionTop + (20 - selectionTop) * scale);
  933. expect(topArrow.width).toBeCloseTo(300);
  934. expect(topArrow.points).toEqual([
  935. [0, 0],
  936. [300, 0],
  937. ]);
  938. expect(topArrowLabelPos.x + topArrowLabel.width / 2).toBeCloseTo(
  939. topArrow.width / 2,
  940. );
  941. expect(topArrowLabelPos.y + topArrowLabel.height / 2).toBeCloseTo(
  942. topArrow.y,
  943. );
  944. expect(topArrowLabel.fontSize).toBeCloseTo(20 * scale);
  945. expect(bottomArrow.x).toBeCloseTo(0);
  946. expect(bottomArrow.y).toBeCloseTo(
  947. selectionTop + (80 - selectionTop) * scale,
  948. );
  949. expect(bottomArrow.width).toBeCloseTo(300);
  950. expect(topArrow.points).toEqual([
  951. [0, 0],
  952. [300, 0],
  953. ]);
  954. expect(bottomArrowLabelPos.x + bottomArrowLabel.width / 2).toBeCloseTo(
  955. bottomArrow.width / 2,
  956. );
  957. expect(bottomArrowLabelPos.y + bottomArrowLabel.height / 2).toBeCloseTo(
  958. bottomArrow.y,
  959. );
  960. expect(bottomArrowLabel.fontSize).toBeCloseTo(28 * scale);
  961. });
  962. it("resizes with text elements", async () => {
  963. const topText = UI.createElement("text", { position: 0 });
  964. await UI.editText(topText, "lorem ipsum");
  965. UI.clickTool("text");
  966. UI.clickByTitle("Large");
  967. const bottomText = UI.createElement("text", { position: 40 });
  968. await UI.editText(bottomText, "dolor\nsit amet");
  969. const selectionWidth = 40 + bottomText.width;
  970. const selectionHeight = 40 + bottomText.height;
  971. const move = [30, -40] as [number, number];
  972. const scale = Math.max(
  973. 1 + move[0] / selectionWidth,
  974. 1 - move[1] / selectionHeight,
  975. );
  976. UI.resize([topText, bottomText], "ne", move, { shift: true });
  977. expect(topText.x).toBeCloseTo(0);
  978. expect(topText.y).toBeCloseTo(-selectionHeight * (scale - 1));
  979. expect(topText.fontSize).toBeCloseTo(20 * scale);
  980. expect(topText.angle).toEqual(0);
  981. expect(bottomText.x).toBeCloseTo(40 * scale);
  982. expect(bottomText.y).toBeCloseTo(40 - (selectionHeight - 40) * (scale - 1));
  983. expect(bottomText.fontSize).toBeCloseTo(28 * scale);
  984. expect(bottomText.angle).toEqual(0);
  985. });
  986. it("resizes with images (proportional)", () => {
  987. const topImage = API.createElement({
  988. type: "image",
  989. x: 0,
  990. y: 0,
  991. width: 200,
  992. height: 100,
  993. });
  994. const bottomImage = API.createElement({
  995. type: "image",
  996. x: 30,
  997. y: 150,
  998. width: 120,
  999. height: 80,
  1000. });
  1001. API.setElements([topImage, bottomImage]);
  1002. const selectionWidth = 200;
  1003. const selectionHeight = 230;
  1004. const move = [-50, -50] as [number, number];
  1005. const scale = Math.max(
  1006. 1 + move[0] / selectionWidth,
  1007. 1 + move[1] / selectionHeight,
  1008. );
  1009. UI.resize([topImage, bottomImage], "se", move);
  1010. expect(topImage.x).toBeCloseTo(0);
  1011. expect(topImage.y).toBeCloseTo(0);
  1012. expect(topImage.width).toBeCloseTo(200 * scale);
  1013. expect(topImage.height).toBeCloseTo(100 * scale);
  1014. expect(topImage.angle).toEqual(0);
  1015. expect(topImage.scale).toEqual([1, 1]);
  1016. expect(bottomImage.x).toBeCloseTo(30 * scale);
  1017. expect(bottomImage.y).toBeCloseTo(150 * scale);
  1018. expect(bottomImage.width).toBeCloseTo(120 * scale);
  1019. expect(bottomImage.height).toBeCloseTo(80 * scale);
  1020. expect(bottomImage.angle).toEqual(0);
  1021. expect(bottomImage.scale).toEqual([1, 1]);
  1022. });
  1023. it("resizes from center", () => {
  1024. const rectangle = UI.createElement("rectangle", {
  1025. x: -200,
  1026. y: -140,
  1027. width: 120,
  1028. height: 100,
  1029. });
  1030. const ellipse = UI.createElement("ellipse", {
  1031. position: 60,
  1032. width: 140,
  1033. height: 80,
  1034. });
  1035. const selectionWidth = 400;
  1036. const selectionHeight = 280;
  1037. const move = [-80, -80] as [number, number];
  1038. const scale = Math.max(
  1039. 1 + (2 * move[0]) / selectionWidth,
  1040. 1 + (2 * move[1]) / selectionHeight,
  1041. );
  1042. UI.resize([rectangle, ellipse], "se", move, { shift: true, alt: true });
  1043. expect(rectangle.x).toBeCloseTo(-200 * scale);
  1044. expect(rectangle.y).toBeCloseTo(-140 * scale);
  1045. expect(rectangle.width).toBeCloseTo(120 * scale);
  1046. expect(rectangle.height).toBeCloseTo(100 * scale);
  1047. expect(rectangle.angle).toEqual(0);
  1048. expect(ellipse.x).toBeCloseTo(60 * scale);
  1049. expect(ellipse.y).toBeCloseTo(60 * scale);
  1050. expect(ellipse.width).toBeCloseTo(140 * scale);
  1051. expect(ellipse.height).toBeCloseTo(80 * scale);
  1052. expect(ellipse.angle).toEqual(0);
  1053. });
  1054. it("flips while resizing", async () => {
  1055. const image = API.createElement({
  1056. type: "image",
  1057. x: 60,
  1058. y: 100,
  1059. width: 100,
  1060. height: 100,
  1061. angle: (Math.PI * 7) / 6,
  1062. });
  1063. API.setElements([image]);
  1064. const line = UI.createElement("line", {
  1065. x: 60,
  1066. y: 0,
  1067. points: [
  1068. pointFrom(0, 0),
  1069. pointFrom(-40, 40),
  1070. pointFrom(-20, 60),
  1071. pointFrom(20, 20),
  1072. pointFrom(40, 40),
  1073. pointFrom(-20, 100),
  1074. pointFrom(-60, 60),
  1075. ],
  1076. });
  1077. const rectangle = UI.createElement("rectangle", {
  1078. x: 180,
  1079. y: 60,
  1080. width: 160,
  1081. height: 80,
  1082. angle: Math.PI / 6,
  1083. });
  1084. const rectLabel = await UI.editText(rectangle, "hello\nworld");
  1085. const boundArrow = UI.createElement("arrow", {
  1086. x: 380,
  1087. y: 240,
  1088. width: -60,
  1089. height: -80,
  1090. });
  1091. const arrowLabel = await UI.editText(boundArrow, "test");
  1092. const selectionWidth = 380;
  1093. const move = [-800, 0] as [number, number];
  1094. const scaleX = move[0] / selectionWidth + 1;
  1095. const scaleY = -scaleX;
  1096. const lineOrigBounds = getBoundsFromPoints(line);
  1097. const elementsMap = arrayToMap(h.elements);
  1098. UI.resize([line, image, rectangle, boundArrow], "se", move, {
  1099. shift: true,
  1100. });
  1101. const lineNewBounds = getBoundsFromPoints(line);
  1102. const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
  1103. boundArrow,
  1104. arrowLabel,
  1105. elementsMap,
  1106. );
  1107. expect(line.x).toBeCloseTo(60 * scaleX);
  1108. expect(line.y).toBeCloseTo(0);
  1109. expect(lineNewBounds[0]).toBeCloseTo(
  1110. (lineOrigBounds[2] - lineOrigBounds[0]) * scaleX,
  1111. );
  1112. expect(lineNewBounds[1]).toBeCloseTo(0);
  1113. expect(lineNewBounds[3]).toBeCloseTo(
  1114. (lineOrigBounds[3] - lineOrigBounds[1]) * scaleY,
  1115. );
  1116. expect(lineNewBounds[2]).toBeCloseTo(0);
  1117. expect(line.angle).toEqual(0);
  1118. expect(image.x).toBeCloseTo((60 + 100) * scaleX);
  1119. expect(image.y).toBeCloseTo(100 * scaleY);
  1120. expect(image.width).toBeCloseTo(100 * -scaleX);
  1121. expect(image.height).toBeCloseTo(100 * scaleY);
  1122. expect(image.angle).toBeCloseTo((Math.PI * 5) / 6);
  1123. expect(image.scale).toEqual([-1, 1]);
  1124. expect(rectangle.x).toBeCloseTo((180 + 160) * scaleX);
  1125. expect(rectangle.y).toBeCloseTo(60 * scaleY);
  1126. expect(rectangle.width).toBeCloseTo(160 * -scaleX);
  1127. expect(rectangle.height).toBeCloseTo(80 * scaleY);
  1128. expect(rectangle.angle).toEqual((Math.PI * 11) / 6);
  1129. expect(rectLabel.x + rectLabel.width / 2).toBeCloseTo(
  1130. rectangle.x + rectangle.width / 2,
  1131. );
  1132. expect(rectLabel.y + rectLabel.height / 2).toBeCloseTo(
  1133. rectangle.y + rectangle.height / 2,
  1134. );
  1135. expect(rectLabel.angle).toBeCloseTo(rectangle.angle);
  1136. expect(rectLabel.fontSize).toBeCloseTo(20 * scaleY);
  1137. expect(boundArrow.x).toBeCloseTo(380 * scaleX);
  1138. expect(boundArrow.y).toBeCloseTo(240 * scaleY);
  1139. expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX);
  1140. expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY);
  1141. expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
  1142. boundArrow.x + boundArrow.points[1][0] / 2,
  1143. );
  1144. expect(arrowLabelPos.y + arrowLabel.height / 2).toBeCloseTo(
  1145. boundArrow.y + boundArrow.points[1][1] / 2,
  1146. );
  1147. expect(arrowLabel.angle).toEqual(0);
  1148. expect(arrowLabel.fontSize).toBeCloseTo(20 * scaleY);
  1149. });
  1150. });