ab9cd5460b
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
272 lines
6.6 KiB
TypeScript
272 lines
6.6 KiB
TypeScript
import { arrayToMap, reseed } 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();
|
|
reseed(7);
|
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
});
|
|
|
|
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<GlobalPoint>(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<GlobalPoint>(50, 50),
|
|
element: API.createElement({
|
|
type: "rectangle",
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
backgroundColor: "#ffffff",
|
|
}),
|
|
threshold: Infinity,
|
|
elementsMap: new Map([]),
|
|
});
|
|
|
|
localStorage.clear();
|
|
reseed(7);
|
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
});
|
|
|
|
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<GlobalPoint>(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<GlobalPoint>(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<GlobalPoint>(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<GlobalPoint>(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);
|
|
});
|
|
});
|
|
|
|
describe("freedraw collision matches the rendered stroke width", () => {
|
|
// A straight, horizontal stroke centered on y === 0.
|
|
const points = Array.from({ length: 21 }, (_, i) =>
|
|
pointFrom<LocalPoint>(i * 5, 0),
|
|
);
|
|
|
|
const createFreeDraw = (variability: "variable" | "constant") =>
|
|
API.createElement({
|
|
type: "freedraw",
|
|
x: 0,
|
|
y: 0,
|
|
strokeWidth: 10,
|
|
points,
|
|
strokeOptions: { variability, streamline: 0.5 },
|
|
});
|
|
|
|
const distanceAt = (
|
|
element: ReturnType<typeof createFreeDraw>,
|
|
x: number,
|
|
y: number,
|
|
) =>
|
|
distance.distanceToElement(
|
|
element,
|
|
arrayToMap([element]),
|
|
pointFrom<GlobalPoint>(x, y),
|
|
);
|
|
|
|
it("treats a point on the centerline as a direct hit for both modes", () => {
|
|
expect(distanceAt(createFreeDraw("variable"), 50, 0)).toBe(0);
|
|
expect(distanceAt(createFreeDraw("constant"), 50, 0)).toBe(0);
|
|
});
|
|
|
|
it("hits across the body of a thick stroke, not just near the centerline", () => {
|
|
expect(distanceAt(createFreeDraw("variable"), 50, 20)).toBe(0);
|
|
});
|
|
|
|
it("uses a wider hit area for variable-width than for constant-width strokes", () => {
|
|
const offset = 20;
|
|
const variableDistance = distanceAt(createFreeDraw("variable"), 50, offset);
|
|
const constantDistance = distanceAt(createFreeDraw("constant"), 50, offset);
|
|
|
|
expect(variableDistance).toBe(0);
|
|
expect(constantDistance).toBeGreaterThan(0);
|
|
expect(variableDistance).toBeLessThan(constantDistance);
|
|
});
|
|
|
|
it("does not hit points clearly outside even the widest stroke", () => {
|
|
expect(distanceAt(createFreeDraw("variable"), 50, 45)).toBeGreaterThan(0);
|
|
});
|
|
});
|