import { isPathALoop, isPointWithinBounds } from "../math"; import type { ElementsMap, ExcalidrawElement, ExcalidrawRectangleElement, } from "./types"; import { getElementBounds } from "./bounds"; import type { FrameNameBounds } from "../types"; import type { Polygon, GeometricShape } from "../../utils/geometry/shape"; import { getPolygonShape } from "../../utils/geometry/shape"; import { isPointInShape, isPointOnShape } from "../../utils/collision"; import { isTransparent } from "../utils"; import { hasBoundTextElement, isIframeLikeElement, isImageElement, isTextElement, } from "./typeChecks"; export const shouldTestInside = (element: ExcalidrawElement) => { if (element.type === "arrow") { return false; } const isDraggableFromInside = !isTransparent(element.backgroundColor) || hasBoundTextElement(element) || isIframeLikeElement(element) || isTextElement(element); if (element.type === "line") { return isDraggableFromInside && isPathALoop(element.points); } if (element.type === "freedraw") { return isDraggableFromInside && isPathALoop(element.points); } return isDraggableFromInside || isImageElement(element); }; export type HitTestArgs = { x: number; y: number; element: ExcalidrawElement; shape: GeometricShape; threshold?: number; frameNameBound?: FrameNameBounds | null; }; export const hitElementItself = ({ x, y, element, shape, threshold = 10, frameNameBound = null, }: HitTestArgs) => { let hit = shouldTestInside(element) ? // Since `inShape` tests STRICTLY againt the insides of a shape // we would need `onShape` as well to include the "borders" isPointInShape([x, y], shape) || isPointOnShape([x, y], shape, threshold) : isPointOnShape([x, y], shape, threshold); // hit test against a frame's name if (!hit && frameNameBound) { hit = isPointInShape([x, y], { type: "polygon", data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement) .data as Polygon, }); } return hit; }; export const hitElementBoundingBox = ( x: number, y: number, element: ExcalidrawElement, elementsMap: ElementsMap, tolerance = 0, ) => { let [x1, y1, x2, y2] = getElementBounds(element, elementsMap); x1 -= tolerance; y1 -= tolerance; x2 += tolerance; y2 += tolerance; return isPointWithinBounds([x1, y1], [x, y], [x2, y2]); }; export const hitElementBoundingBoxOnly = ( hitArgs: HitTestArgs, elementsMap: ElementsMap, ) => { return ( !hitElementItself(hitArgs) && hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap) ); }; export const hitElementBoundText = ( x: number, y: number, textShape: GeometricShape | null, ) => { return textShape && isPointInShape([x, y], textShape); };