linearElementEditor.test.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. import ReactDOM from "react-dom";
  2. import { ExcalidrawLinearElement } from "../element/types";
  3. import ExcalidrawApp from "../excalidraw-app";
  4. import { centerPoint } from "../math";
  5. import { reseed } from "../random";
  6. import * as Renderer from "../renderer/renderScene";
  7. import { Keyboard, Pointer } from "./helpers/ui";
  8. import { screen, render, fireEvent } from "./test-utils";
  9. import { API } from "../tests/helpers/api";
  10. import { Point } from "../types";
  11. import { KEYS } from "../keys";
  12. import { LinearElementEditor } from "../element/linearElementEditor";
  13. const renderScene = jest.spyOn(Renderer, "renderScene");
  14. const { h } = window;
  15. describe(" Test Linear Elements", () => {
  16. let container: HTMLElement;
  17. let canvas: HTMLCanvasElement;
  18. beforeEach(async () => {
  19. // Unmount ReactDOM from root
  20. ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
  21. localStorage.clear();
  22. renderScene.mockClear();
  23. reseed(7);
  24. const comp = await render(<ExcalidrawApp />);
  25. container = comp.container;
  26. canvas = container.querySelector("canvas")!;
  27. canvas.width = 1000;
  28. canvas.height = 1000;
  29. });
  30. const p1: Point = [20, 20];
  31. const p2: Point = [60, 20];
  32. const midpoint = centerPoint(p1, p2);
  33. const delta = 50;
  34. const mouse = new Pointer("mouse");
  35. const createTwoPointerLinearElement = (
  36. type: ExcalidrawLinearElement["type"],
  37. strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
  38. roughness: ExcalidrawLinearElement["roughness"] = 0,
  39. ) => {
  40. h.elements = [
  41. API.createElement({
  42. x: p1[0],
  43. y: p1[1],
  44. width: p2[0] - p1[0],
  45. height: 0,
  46. type,
  47. roughness,
  48. points: [
  49. [0, 0],
  50. [p2[0] - p1[0], p2[1] - p1[1]],
  51. ],
  52. strokeSharpness,
  53. }),
  54. ];
  55. mouse.clickAt(p1[0], p1[1]);
  56. };
  57. const createThreePointerLinearElement = (
  58. type: ExcalidrawLinearElement["type"],
  59. strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
  60. roughness: ExcalidrawLinearElement["roughness"] = 0,
  61. ) => {
  62. //dragging line from midpoint
  63. const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]];
  64. h.elements = [
  65. API.createElement({
  66. x: p1[0],
  67. y: p1[1],
  68. width: p3[0] - p1[0],
  69. height: 0,
  70. type,
  71. roughness,
  72. points: [
  73. [0, 0],
  74. [p3[0], p3[1]],
  75. [p2[0] - p1[0], p2[1] - p1[1]],
  76. ],
  77. strokeSharpness,
  78. }),
  79. ];
  80. mouse.clickAt(p1[0], p1[1]);
  81. };
  82. const enterLineEditingMode = (line: ExcalidrawLinearElement) => {
  83. mouse.clickAt(p1[0], p1[1]);
  84. Keyboard.keyPress(KEYS.ENTER);
  85. expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
  86. };
  87. const drag = (startPoint: Point, endPoint: Point) => {
  88. fireEvent.pointerDown(canvas, {
  89. clientX: startPoint[0],
  90. clientY: startPoint[1],
  91. });
  92. fireEvent.pointerMove(canvas, {
  93. clientX: endPoint[0],
  94. clientY: endPoint[1],
  95. });
  96. fireEvent.pointerUp(canvas, {
  97. clientX: endPoint[0],
  98. clientY: endPoint[1],
  99. });
  100. };
  101. const deletePoint = (point: Point) => {
  102. fireEvent.pointerDown(canvas, {
  103. clientX: point[0],
  104. clientY: point[1],
  105. });
  106. fireEvent.pointerUp(canvas, {
  107. clientX: point[0],
  108. clientY: point[1],
  109. });
  110. Keyboard.keyPress(KEYS.DELETE);
  111. };
  112. it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => {
  113. createTwoPointerLinearElement("line");
  114. const line = h.elements[0] as ExcalidrawLinearElement;
  115. expect(renderScene).toHaveBeenCalledTimes(6);
  116. expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
  117. // drag line from midpoint
  118. drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
  119. expect(renderScene).toHaveBeenCalledTimes(9);
  120. expect(line.points.length).toEqual(3);
  121. expect(line.points).toMatchInlineSnapshot(`
  122. Array [
  123. Array [
  124. 0,
  125. 0,
  126. ],
  127. Array [
  128. 70,
  129. 50,
  130. ],
  131. Array [
  132. 40,
  133. 0,
  134. ],
  135. ]
  136. `);
  137. });
  138. describe("Inside editor", () => {
  139. it("should allow dragging line from midpoint in 2 pointer lines", async () => {
  140. createTwoPointerLinearElement("line");
  141. const line = h.elements[0] as ExcalidrawLinearElement;
  142. enterLineEditingMode(line);
  143. // drag line from midpoint
  144. drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
  145. expect(renderScene).toHaveBeenCalledTimes(13);
  146. expect(line.points.length).toEqual(3);
  147. expect(line.points).toMatchInlineSnapshot(`
  148. Array [
  149. Array [
  150. 0,
  151. 0,
  152. ],
  153. Array [
  154. 70,
  155. 50,
  156. ],
  157. Array [
  158. 40,
  159. 0,
  160. ],
  161. ]
  162. `);
  163. });
  164. it("should update the midpoints when element sharpness changed", async () => {
  165. createThreePointerLinearElement("line");
  166. const line = h.elements[0] as ExcalidrawLinearElement;
  167. expect(line.points.length).toEqual(3);
  168. enterLineEditingMode(line);
  169. const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
  170. line,
  171. h.state,
  172. );
  173. // update sharpness
  174. fireEvent.click(screen.getByTitle("Round"));
  175. expect(renderScene).toHaveBeenCalledTimes(11);
  176. const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
  177. h.elements[0] as ExcalidrawLinearElement,
  178. h.state,
  179. );
  180. expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
  181. expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
  182. expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
  183. Array [
  184. Array [
  185. 55.9697848965255,
  186. 47.442326230998205,
  187. ],
  188. Array [
  189. 76.08587175006699,
  190. 43.294165939653226,
  191. ],
  192. ]
  193. `);
  194. });
  195. it("should update all the midpoints when element position changed", async () => {
  196. createThreePointerLinearElement("line", "round");
  197. const line = h.elements[0] as ExcalidrawLinearElement;
  198. expect(line.points.length).toEqual(3);
  199. enterLineEditingMode(line);
  200. const points = LinearElementEditor.getPointsGlobalCoordinates(line);
  201. expect([line.x, line.y]).toEqual(points[0]);
  202. const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
  203. const startPoint = centerPoint(points[0], midPoints[0] as Point);
  204. const deltaX = 50;
  205. const deltaY = 20;
  206. const endPoint: Point = [startPoint[0] + deltaX, startPoint[1] + deltaY];
  207. // Move the element
  208. drag(startPoint, endPoint);
  209. expect(renderScene).toHaveBeenCalledTimes(14);
  210. expect([line.x, line.y]).toEqual([
  211. points[0][0] + deltaX,
  212. points[0][1] + deltaY,
  213. ]);
  214. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  215. line,
  216. h.state,
  217. );
  218. expect(midPoints[0]).not.toEqual(newMidPoints[0]);
  219. expect(midPoints[1]).not.toEqual(newMidPoints[1]);
  220. expect(newMidPoints).toMatchInlineSnapshot(`
  221. Array [
  222. Array [
  223. 105.96978489652551,
  224. 67.4423262309982,
  225. ],
  226. Array [
  227. 126.08587175006699,
  228. 63.294165939653226,
  229. ],
  230. ]
  231. `);
  232. });
  233. describe("When edges are sharp", () => {
  234. // This is the expected midpoint for line with sharp edge
  235. // hence hardcoding it so if later some bug is introduced
  236. // this will fail and we can fix it
  237. const firstSegmentMidpoint: Point = [55, 45];
  238. const lastSegmentMidpoint: Point = [75, 40];
  239. let line: ExcalidrawLinearElement;
  240. beforeEach(() => {
  241. createThreePointerLinearElement("line");
  242. line = h.elements[0] as ExcalidrawLinearElement;
  243. expect(line.points.length).toEqual(3);
  244. enterLineEditingMode(line);
  245. });
  246. it("should allow dragging lines from midpoints in between segments", async () => {
  247. // drag line via first segment midpoint
  248. drag(firstSegmentMidpoint, [
  249. firstSegmentMidpoint[0] + delta,
  250. firstSegmentMidpoint[1] + delta,
  251. ]);
  252. expect(line.points.length).toEqual(4);
  253. // drag line from last segment midpoint
  254. drag(lastSegmentMidpoint, [
  255. lastSegmentMidpoint[0] + delta,
  256. lastSegmentMidpoint[1] + delta,
  257. ]);
  258. expect(renderScene).toHaveBeenCalledTimes(18);
  259. expect(line.points.length).toEqual(5);
  260. expect((h.elements[0] as ExcalidrawLinearElement).points)
  261. .toMatchInlineSnapshot(`
  262. Array [
  263. Array [
  264. 0,
  265. 0,
  266. ],
  267. Array [
  268. 85,
  269. 75,
  270. ],
  271. Array [
  272. 70,
  273. 50,
  274. ],
  275. Array [
  276. 105,
  277. 75,
  278. ],
  279. Array [
  280. 40,
  281. 0,
  282. ],
  283. ]
  284. `);
  285. });
  286. it("should update only the first segment midpoint when its point is dragged", async () => {
  287. const points = LinearElementEditor.getPointsGlobalCoordinates(line);
  288. const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
  289. const hitCoords: Point = [points[0][0], points[0][1]];
  290. // Drag from first point
  291. drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
  292. expect(renderScene).toHaveBeenCalledTimes(14);
  293. const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
  294. expect([newPoints[0][0], newPoints[0][1]]).toEqual([
  295. points[0][0] - delta,
  296. points[0][1] - delta,
  297. ]);
  298. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  299. line,
  300. h.state,
  301. );
  302. expect(midPoints[0]).not.toEqual(newMidPoints[0]);
  303. expect(midPoints[1]).toEqual(newMidPoints[1]);
  304. });
  305. it("should hide midpoints in the segment when points moved close", async () => {
  306. const points = LinearElementEditor.getPointsGlobalCoordinates(line);
  307. const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
  308. const hitCoords: Point = [points[0][0], points[0][1]];
  309. // Drag from first point
  310. drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
  311. expect(renderScene).toHaveBeenCalledTimes(14);
  312. const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
  313. expect([newPoints[0][0], newPoints[0][1]]).toEqual([
  314. points[0][0] + delta,
  315. points[0][1] + delta,
  316. ]);
  317. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  318. line,
  319. h.state,
  320. );
  321. // This midpoint is hidden since the points are too close
  322. expect(newMidPoints[0]).toBeNull();
  323. expect(midPoints[1]).toEqual(newMidPoints[1]);
  324. });
  325. it("should remove the midpoint when one of the points in the segment is deleted", async () => {
  326. const line = h.elements[0] as ExcalidrawLinearElement;
  327. enterLineEditingMode(line);
  328. const points = LinearElementEditor.getPointsGlobalCoordinates(line);
  329. // dragging line from last segment midpoint
  330. drag(lastSegmentMidpoint, [
  331. lastSegmentMidpoint[0] + 50,
  332. lastSegmentMidpoint[1] + 50,
  333. ]);
  334. expect(line.points.length).toEqual(4);
  335. const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
  336. // delete 3rd point
  337. deletePoint(points[2]);
  338. expect(line.points.length).toEqual(3);
  339. expect(renderScene).toHaveBeenCalledTimes(19);
  340. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  341. line,
  342. h.state,
  343. );
  344. expect(newMidPoints.length).toEqual(2);
  345. expect(midPoints[0]).toEqual(newMidPoints[0]);
  346. expect(midPoints[1]).toEqual(newMidPoints[1]);
  347. });
  348. });
  349. describe("When edges are round", () => {
  350. // This is the expected midpoint for line with round edge
  351. // hence hardcoding it so if later some bug is introduced
  352. // this will fail and we can fix it
  353. const firstSegmentMidpoint: Point = [
  354. 55.9697848965255, 47.442326230998205,
  355. ];
  356. const lastSegmentMidpoint: Point = [
  357. 76.08587175006699, 43.294165939653226,
  358. ];
  359. let line: ExcalidrawLinearElement;
  360. beforeEach(() => {
  361. createThreePointerLinearElement("line", "round");
  362. line = h.elements[0] as ExcalidrawLinearElement;
  363. expect(line.points.length).toEqual(3);
  364. enterLineEditingMode(line);
  365. });
  366. it("should allow dragging lines from midpoints in between segments", async () => {
  367. // drag line from first segment midpoint
  368. drag(firstSegmentMidpoint, [
  369. firstSegmentMidpoint[0] + delta,
  370. firstSegmentMidpoint[1] + delta,
  371. ]);
  372. expect(line.points.length).toEqual(4);
  373. // drag line from last segment midpoint
  374. drag(lastSegmentMidpoint, [
  375. lastSegmentMidpoint[0] + delta,
  376. lastSegmentMidpoint[1] + delta,
  377. ]);
  378. expect(renderScene).toHaveBeenCalledTimes(18);
  379. expect(line.points.length).toEqual(5);
  380. expect((h.elements[0] as ExcalidrawLinearElement).points)
  381. .toMatchInlineSnapshot(`
  382. Array [
  383. Array [
  384. 0,
  385. 0,
  386. ],
  387. Array [
  388. 85.96978489652551,
  389. 77.4423262309982,
  390. ],
  391. Array [
  392. 70,
  393. 50,
  394. ],
  395. Array [
  396. 104.58050066266131,
  397. 74.24758482724201,
  398. ],
  399. Array [
  400. 40,
  401. 0,
  402. ],
  403. ]
  404. `);
  405. });
  406. it("should update all the midpoints when its point is dragged", async () => {
  407. const points = LinearElementEditor.getPointsGlobalCoordinates(line);
  408. const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
  409. const hitCoords: Point = [points[0][0], points[0][1]];
  410. // Drag from first point
  411. drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
  412. const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
  413. expect([newPoints[0][0], newPoints[0][1]]).toEqual([
  414. points[0][0] - delta,
  415. points[0][1] - delta,
  416. ]);
  417. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  418. line,
  419. h.state,
  420. );
  421. expect(midPoints[0]).not.toEqual(newMidPoints[0]);
  422. expect(midPoints[1]).not.toEqual(newMidPoints[1]);
  423. expect(newMidPoints).toMatchInlineSnapshot(`
  424. Array [
  425. Array [
  426. 31.884084517616053,
  427. 23.13275505472383,
  428. ],
  429. Array [
  430. 77.74792546875662,
  431. 44.57840982272327,
  432. ],
  433. ]
  434. `);
  435. });
  436. it("should hide midpoints in the segment when points moved close", async () => {
  437. const points = LinearElementEditor.getPointsGlobalCoordinates(line);
  438. const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
  439. const hitCoords: Point = [points[0][0], points[0][1]];
  440. // Drag from first point
  441. drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
  442. expect(renderScene).toHaveBeenCalledTimes(14);
  443. const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
  444. expect([newPoints[0][0], newPoints[0][1]]).toEqual([
  445. points[0][0] + delta,
  446. points[0][1] + delta,
  447. ]);
  448. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  449. line,
  450. h.state,
  451. );
  452. // This mid point is hidden due to point being too close
  453. expect(newMidPoints[0]).toBeNull();
  454. expect(newMidPoints[1]).not.toEqual(midPoints[1]);
  455. });
  456. it("should update all the midpoints when a point is deleted", async () => {
  457. drag(lastSegmentMidpoint, [
  458. lastSegmentMidpoint[0] + delta,
  459. lastSegmentMidpoint[1] + delta,
  460. ]);
  461. expect(line.points.length).toEqual(4);
  462. const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
  463. const points = LinearElementEditor.getPointsGlobalCoordinates(line);
  464. // delete 3rd point
  465. deletePoint(points[2]);
  466. expect(line.points.length).toEqual(3);
  467. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  468. line,
  469. h.state,
  470. );
  471. expect(newMidPoints.length).toEqual(2);
  472. expect(midPoints[0]).not.toEqual(newMidPoints[0]);
  473. expect(midPoints[1]).not.toEqual(newMidPoints[1]);
  474. expect(newMidPoints).toMatchInlineSnapshot(`
  475. Array [
  476. Array [
  477. 55.9697848965255,
  478. 47.442326230998205,
  479. ],
  480. Array [
  481. 76.08587175006699,
  482. 43.294165939653226,
  483. ],
  484. ]
  485. `);
  486. });
  487. });
  488. });
  489. });