collision.test.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { arrayToMap } from "@excalidraw/common";
  2. import { type GlobalPoint, type LocalPoint, pointFrom } from "@excalidraw/math";
  3. import { Excalidraw } from "@excalidraw/excalidraw";
  4. import { API } from "@excalidraw/excalidraw/tests/helpers/api";
  5. import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
  6. import "@excalidraw/utils/test-utils";
  7. import { render } from "@excalidraw/excalidraw/tests/test-utils";
  8. import * as distance from "../src/distance";
  9. import { hitElementItself } from "../src/collision";
  10. describe("check rotated elements can be hit:", () => {
  11. beforeEach(async () => {
  12. localStorage.clear();
  13. await render(<Excalidraw handleKeyboardGlobally={true} />);
  14. });
  15. it("arrow", () => {
  16. UI.createElement("arrow", {
  17. x: 0,
  18. y: 0,
  19. width: 124,
  20. height: 302,
  21. angle: 1.8700426423973724,
  22. points: [
  23. [0, 0],
  24. [120, -198],
  25. [-4, -302],
  26. ] as LocalPoint[],
  27. });
  28. const hit = hitElementItself({
  29. point: pointFrom<GlobalPoint>(88, -68),
  30. element: window.h.elements[0],
  31. threshold: 10,
  32. elementsMap: window.h.scene.getNonDeletedElementsMap(),
  33. });
  34. expect(hit).toBe(true);
  35. });
  36. });
  37. describe("frame hit testing", () => {
  38. it.each(["transparent", "#ffffff"])(
  39. "does not hit frame inside regardless of background color (%s)",
  40. (backgroundColor) => {
  41. const element = API.createElement({
  42. type: "frame",
  43. x: 0,
  44. y: 0,
  45. width: 100,
  46. height: 100,
  47. backgroundColor,
  48. });
  49. const elementsMap = arrayToMap([element]);
  50. expect(
  51. hitElementItself({
  52. point: pointFrom<GlobalPoint>(50, 50),
  53. element,
  54. threshold: 10,
  55. elementsMap,
  56. }),
  57. ).toBe(false);
  58. },
  59. );
  60. it("hits frame outline", () => {
  61. const element = API.createElement({
  62. type: "frame",
  63. x: 0,
  64. y: 0,
  65. width: 100,
  66. height: 100,
  67. backgroundColor: "#ffffff",
  68. });
  69. const elementsMap = arrayToMap([element]);
  70. expect(
  71. hitElementItself({
  72. point: pointFrom<GlobalPoint>(0, 50),
  73. element,
  74. threshold: 1,
  75. elementsMap,
  76. }),
  77. ).toBe(true);
  78. });
  79. });
  80. describe("hitElementItself cache", () => {
  81. beforeEach(async () => {
  82. // reset cache
  83. hitElementItself({
  84. point: pointFrom<GlobalPoint>(50, 50),
  85. element: API.createElement({
  86. type: "rectangle",
  87. x: 0,
  88. y: 0,
  89. width: 100,
  90. height: 100,
  91. backgroundColor: "#ffffff",
  92. }),
  93. threshold: Infinity,
  94. elementsMap: new Map([]),
  95. });
  96. localStorage.clear();
  97. await render(<Excalidraw handleKeyboardGlobally={true} />);
  98. });
  99. it("reuses cached result when threshold increases", () => {
  100. const element = API.createElement({
  101. type: "rectangle",
  102. x: 0,
  103. y: 0,
  104. width: 100,
  105. height: 100,
  106. backgroundColor: "#ffffff",
  107. });
  108. const elementsMap = arrayToMap([element]);
  109. const point = pointFrom<GlobalPoint>(100.5, 50);
  110. const distanceSpy = jest.spyOn(distance, "distanceToElement");
  111. expect(
  112. hitElementItself({
  113. point,
  114. element,
  115. threshold: 1,
  116. elementsMap,
  117. }),
  118. ).toBe(true);
  119. expect(distanceSpy).toHaveBeenCalledTimes(1);
  120. expect(
  121. hitElementItself({
  122. point,
  123. element,
  124. threshold: 10,
  125. elementsMap,
  126. }),
  127. ).toBe(true);
  128. expect(distanceSpy).toHaveBeenCalledTimes(1);
  129. distanceSpy.mockRestore();
  130. });
  131. it("does not reuse cache when threshold decreases", () => {
  132. const element = API.createElement({
  133. type: "rectangle",
  134. x: 0,
  135. y: 0,
  136. width: 100,
  137. height: 100,
  138. backgroundColor: "transparent",
  139. });
  140. const elementsMap = arrayToMap([element]);
  141. const point = pointFrom<GlobalPoint>(105, 50);
  142. const distanceSpy = jest.spyOn(distance, "distanceToElement");
  143. expect(
  144. hitElementItself({
  145. point,
  146. element,
  147. threshold: 10,
  148. elementsMap,
  149. }),
  150. ).toBe(true);
  151. expect(distanceSpy).toHaveBeenCalledTimes(1);
  152. expect(
  153. hitElementItself({
  154. point,
  155. element,
  156. threshold: 6,
  157. elementsMap,
  158. }),
  159. ).toBe(true);
  160. expect(distanceSpy).toHaveBeenCalledTimes(2);
  161. distanceSpy.mockRestore();
  162. });
  163. it("invalidates cache when element version changes", () => {
  164. const element = API.createElement({
  165. type: "rectangle",
  166. x: 0,
  167. y: 0,
  168. width: 100,
  169. height: 100,
  170. backgroundColor: "#ffffff",
  171. });
  172. const elementsMap = arrayToMap([element]);
  173. const point = pointFrom<GlobalPoint>(100.5, 50);
  174. const distanceSpy = jest.spyOn(distance, "distanceToElement");
  175. expect(
  176. hitElementItself({
  177. point,
  178. element,
  179. threshold: 1,
  180. elementsMap,
  181. }),
  182. ).toBe(true);
  183. expect(distanceSpy).toHaveBeenCalledTimes(1);
  184. const movedElement = {
  185. ...element,
  186. version: element.version + 1,
  187. versionNonce: element.versionNonce + 1,
  188. };
  189. expect(
  190. hitElementItself({
  191. point,
  192. element: movedElement,
  193. threshold: 1,
  194. elementsMap,
  195. }),
  196. ).toBe(true);
  197. expect(distanceSpy).toHaveBeenCalledTimes(2);
  198. distanceSpy.mockRestore();
  199. });
  200. it("override does not affect caching", () => {
  201. const element = API.createElement({
  202. type: "rectangle",
  203. x: 0,
  204. y: 0,
  205. width: 100,
  206. height: 100,
  207. backgroundColor: "transparent",
  208. });
  209. const elementsMap = arrayToMap([element]);
  210. const point = pointFrom<GlobalPoint>(50, 50);
  211. const distanceSpy = jest.spyOn(distance, "distanceToElement");
  212. expect(
  213. hitElementItself({
  214. point,
  215. element,
  216. threshold: 10,
  217. elementsMap,
  218. }),
  219. ).toBe(false);
  220. expect(distanceSpy).toHaveBeenCalledTimes(1);
  221. expect(
  222. hitElementItself({
  223. point,
  224. element,
  225. threshold: 10,
  226. elementsMap,
  227. overrideShouldTestInside: true,
  228. }),
  229. ).toBe(true);
  230. });
  231. });