resize.test.tsx 34 KB

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