import { arrayToMap } from "@excalidraw/common"; import { type GlobalPoint, type LocalPoint, pointFrom } from "@excalidraw/math"; import { Excalidraw } from "@excalidraw/excalidraw"; import { API } from "@excalidraw/excalidraw/tests/helpers/api"; import { UI } from "@excalidraw/excalidraw/tests/helpers/ui"; import "@excalidraw/utils/test-utils"; import { render } from "@excalidraw/excalidraw/tests/test-utils"; import * as distance from "../src/distance"; import { hitElementItself } from "../src/collision"; describe("check rotated elements can be hit:", () => { beforeEach(async () => { localStorage.clear(); await render(); }); it("arrow", () => { UI.createElement("arrow", { x: 0, y: 0, width: 124, height: 302, angle: 1.8700426423973724, points: [ [0, 0], [120, -198], [-4, -302], ] as LocalPoint[], }); const hit = hitElementItself({ point: pointFrom(88, -68), element: window.h.elements[0], threshold: 10, elementsMap: window.h.scene.getNonDeletedElementsMap(), }); expect(hit).toBe(true); }); }); describe("hitElementItself cache", () => { beforeEach(async () => { // reset cache hitElementItself({ point: pointFrom(50, 50), element: API.createElement({ type: "rectangle", x: 0, y: 0, width: 100, height: 100, backgroundColor: "#ffffff", }), threshold: Infinity, elementsMap: new Map([]), }); localStorage.clear(); await render(); }); it("reuses cached result when threshold increases", () => { const element = API.createElement({ type: "rectangle", x: 0, y: 0, width: 100, height: 100, backgroundColor: "#ffffff", }); const elementsMap = arrayToMap([element]); const point = pointFrom(100.5, 50); const distanceSpy = jest.spyOn(distance, "distanceToElement"); expect( hitElementItself({ point, element, threshold: 1, elementsMap, }), ).toBe(true); expect(distanceSpy).toHaveBeenCalledTimes(1); expect( hitElementItself({ point, element, threshold: 10, elementsMap, }), ).toBe(true); expect(distanceSpy).toHaveBeenCalledTimes(1); distanceSpy.mockRestore(); }); it("does not reuse cache when threshold decreases", () => { const element = API.createElement({ type: "rectangle", x: 0, y: 0, width: 100, height: 100, backgroundColor: "transparent", }); const elementsMap = arrayToMap([element]); const point = pointFrom(105, 50); const distanceSpy = jest.spyOn(distance, "distanceToElement"); expect( hitElementItself({ point, element, threshold: 10, elementsMap, }), ).toBe(true); expect(distanceSpy).toHaveBeenCalledTimes(1); expect( hitElementItself({ point, element, threshold: 6, elementsMap, }), ).toBe(true); expect(distanceSpy).toHaveBeenCalledTimes(2); distanceSpy.mockRestore(); }); it("invalidates cache when element version changes", () => { const element = API.createElement({ type: "rectangle", x: 0, y: 0, width: 100, height: 100, backgroundColor: "#ffffff", }); const elementsMap = arrayToMap([element]); const point = pointFrom(100.5, 50); const distanceSpy = jest.spyOn(distance, "distanceToElement"); expect( hitElementItself({ point, element, threshold: 1, elementsMap, }), ).toBe(true); expect(distanceSpy).toHaveBeenCalledTimes(1); const movedElement = { ...element, version: element.version + 1, versionNonce: element.versionNonce + 1, }; expect( hitElementItself({ point, element: movedElement, threshold: 1, elementsMap, }), ).toBe(true); expect(distanceSpy).toHaveBeenCalledTimes(2); distanceSpy.mockRestore(); }); it("override does not affect caching", () => { const element = API.createElement({ type: "rectangle", x: 0, y: 0, width: 100, height: 100, backgroundColor: "transparent", }); const elementsMap = arrayToMap([element]); const point = pointFrom(50, 50); const distanceSpy = jest.spyOn(distance, "distanceToElement"); expect( hitElementItself({ point, element, threshold: 10, elementsMap, }), ).toBe(false); expect(distanceSpy).toHaveBeenCalledTimes(1); expect( hitElementItself({ point, element, threshold: 10, elementsMap, overrideShouldTestInside: true, }), ).toBe(true); }); });