diff options
Diffstat (limited to 'packages/utils/withinBounds.ts')
| -rw-r--r-- | packages/utils/withinBounds.ts | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/packages/utils/withinBounds.ts b/packages/utils/withinBounds.ts new file mode 100644 index 0000000..8d52eb3 --- /dev/null +++ b/packages/utils/withinBounds.ts @@ -0,0 +1,228 @@ +import type { + ExcalidrawElement, + ExcalidrawFreeDrawElement, + ExcalidrawLinearElement, + NonDeletedExcalidrawElement, +} from "@excalidraw/excalidraw/element/types"; +import { + isArrowElement, + isExcalidrawElement, + isFreeDrawElement, + isLinearElement, + isTextElement, +} from "@excalidraw/excalidraw/element/typeChecks"; +import type { Bounds } from "@excalidraw/excalidraw/element/bounds"; +import { getElementBounds } from "@excalidraw/excalidraw/element/bounds"; +import { arrayToMap } from "@excalidraw/excalidraw/utils"; +import type { LocalPoint } from "@excalidraw/math"; +import { + rangeIncludesValue, + pointFrom, + pointRotateRads, + rangeInclusive, +} from "@excalidraw/math"; + +type Element = NonDeletedExcalidrawElement; +type Elements = readonly NonDeletedExcalidrawElement[]; + +type Points = readonly LocalPoint[]; + +/** @returns vertices relative to element's top-left [0,0] position */ +const getNonLinearElementRelativePoints = ( + element: Exclude< + Element, + ExcalidrawLinearElement | ExcalidrawFreeDrawElement + >, +): [ + TopLeft: LocalPoint, + TopRight: LocalPoint, + BottomRight: LocalPoint, + BottomLeft: LocalPoint, +] => { + if (element.type === "diamond") { + return [ + pointFrom(element.width / 2, 0), + pointFrom(element.width, element.height / 2), + pointFrom(element.width / 2, element.height), + pointFrom(0, element.height / 2), + ]; + } + return [ + pointFrom(0, 0), + pointFrom(0 + element.width, 0), + pointFrom(0 + element.width, element.height), + pointFrom(0, element.height), + ]; +}; + +/** @returns vertices relative to element's top-left [0,0] position */ +const getElementRelativePoints = (element: ExcalidrawElement): Points => { + if (isLinearElement(element) || isFreeDrawElement(element)) { + return element.points; + } + return getNonLinearElementRelativePoints(element); +}; + +const getMinMaxPoints = (points: Points) => { + const ret = points.reduce( + (limits, [x, y]) => { + limits.minY = Math.min(limits.minY, y); + limits.minX = Math.min(limits.minX, x); + + limits.maxX = Math.max(limits.maxX, x); + limits.maxY = Math.max(limits.maxY, y); + + return limits; + }, + { + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity, + cx: 0, + cy: 0, + }, + ); + + ret.cx = (ret.maxX + ret.minX) / 2; + ret.cy = (ret.maxY + ret.minY) / 2; + + return ret; +}; + +const getRotatedBBox = (element: Element): Bounds => { + const points = getElementRelativePoints(element); + + const { cx, cy } = getMinMaxPoints(points); + const centerPoint = pointFrom<LocalPoint>(cx, cy); + + const rotatedPoints = points.map((p) => + pointRotateRads(p, centerPoint, element.angle), + ); + const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints); + + return [ + minX + element.x, + minY + element.y, + maxX + element.x, + maxY + element.y, + ]; +}; + +export const isElementInsideBBox = ( + element: Element, + bbox: Bounds, + eitherDirection = false, +): boolean => { + const elementBBox = getRotatedBBox(element); + + const elementInsideBbox = + bbox[0] <= elementBBox[0] && + bbox[2] >= elementBBox[2] && + bbox[1] <= elementBBox[1] && + bbox[3] >= elementBBox[3]; + + if (!eitherDirection) { + return elementInsideBbox; + } + + if (elementInsideBbox) { + return true; + } + + return ( + elementBBox[0] <= bbox[0] && + elementBBox[2] >= bbox[2] && + elementBBox[1] <= bbox[1] && + elementBBox[3] >= bbox[3] + ); +}; + +export const elementPartiallyOverlapsWithOrContainsBBox = ( + element: Element, + bbox: Bounds, +): boolean => { + const elementBBox = getRotatedBBox(element); + + return ( + (rangeIncludesValue(elementBBox[0], rangeInclusive(bbox[0], bbox[2])) || + rangeIncludesValue( + bbox[0], + rangeInclusive(elementBBox[0], elementBBox[2]), + )) && + (rangeIncludesValue(elementBBox[1], rangeInclusive(bbox[1], bbox[3])) || + rangeIncludesValue( + bbox[1], + rangeInclusive(elementBBox[1], elementBBox[3]), + )) + ); +}; + +export const elementsOverlappingBBox = ({ + elements, + bounds, + type, + errorMargin = 0, +}: { + elements: Elements; + bounds: Bounds | ExcalidrawElement; + /** safety offset. Defaults to 0. */ + errorMargin?: number; + /** + * - overlap: elements overlapping or inside bounds + * - contain: elements inside bounds or bounds inside elements + * - inside: elements inside bounds + **/ + type: "overlap" | "contain" | "inside"; +}) => { + if (isExcalidrawElement(bounds)) { + bounds = getElementBounds(bounds, arrayToMap(elements)); + } + const adjustedBBox: Bounds = [ + bounds[0] - errorMargin, + bounds[1] - errorMargin, + bounds[2] + errorMargin, + bounds[3] + errorMargin, + ]; + + const includedElementSet = new Set<string>(); + + for (const element of elements) { + if (includedElementSet.has(element.id)) { + continue; + } + + const isOverlaping = + type === "overlap" + ? elementPartiallyOverlapsWithOrContainsBBox(element, adjustedBBox) + : type === "inside" + ? isElementInsideBBox(element, adjustedBBox) + : isElementInsideBBox(element, adjustedBBox, true); + + if (isOverlaping) { + includedElementSet.add(element.id); + + if (element.boundElements) { + for (const boundElement of element.boundElements) { + includedElementSet.add(boundElement.id); + } + } + + if (isTextElement(element) && element.containerId) { + includedElementSet.add(element.containerId); + } + + if (isArrowElement(element)) { + if (element.startBinding) { + includedElementSet.add(element.startBinding.elementId); + } + + if (element.endBinding) { + includedElementSet.add(element.endBinding?.elementId); + } + } + } + } + + return elements.filter((element) => includedElementSet.has(element.id)); +}; |
