withinBounds.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import type {
  2. ExcalidrawElement,
  3. ExcalidrawFreeDrawElement,
  4. ExcalidrawLinearElement,
  5. NonDeletedExcalidrawElement,
  6. } from "../excalidraw/element/types";
  7. import {
  8. isArrowElement,
  9. isExcalidrawElement,
  10. isFreeDrawElement,
  11. isLinearElement,
  12. isTextElement,
  13. } from "../excalidraw/element/typeChecks";
  14. import type { Bounds } from "../excalidraw/element/bounds";
  15. import { getElementBounds } from "../excalidraw/element/bounds";
  16. import { arrayToMap } from "../excalidraw/utils";
  17. import type { LocalPoint } from "../math";
  18. import {
  19. rangeIncludesValue,
  20. pointFrom,
  21. pointRotateRads,
  22. rangeInclusive,
  23. } from "../math";
  24. type Element = NonDeletedExcalidrawElement;
  25. type Elements = readonly NonDeletedExcalidrawElement[];
  26. type Points = readonly LocalPoint[];
  27. /** @returns vertices relative to element's top-left [0,0] position */
  28. const getNonLinearElementRelativePoints = (
  29. element: Exclude<
  30. Element,
  31. ExcalidrawLinearElement | ExcalidrawFreeDrawElement
  32. >,
  33. ): [
  34. TopLeft: LocalPoint,
  35. TopRight: LocalPoint,
  36. BottomRight: LocalPoint,
  37. BottomLeft: LocalPoint,
  38. ] => {
  39. if (element.type === "diamond") {
  40. return [
  41. pointFrom(element.width / 2, 0),
  42. pointFrom(element.width, element.height / 2),
  43. pointFrom(element.width / 2, element.height),
  44. pointFrom(0, element.height / 2),
  45. ];
  46. }
  47. return [
  48. pointFrom(0, 0),
  49. pointFrom(0 + element.width, 0),
  50. pointFrom(0 + element.width, element.height),
  51. pointFrom(0, element.height),
  52. ];
  53. };
  54. /** @returns vertices relative to element's top-left [0,0] position */
  55. const getElementRelativePoints = (element: ExcalidrawElement): Points => {
  56. if (isLinearElement(element) || isFreeDrawElement(element)) {
  57. return element.points;
  58. }
  59. return getNonLinearElementRelativePoints(element);
  60. };
  61. const getMinMaxPoints = (points: Points) => {
  62. const ret = points.reduce(
  63. (limits, [x, y]) => {
  64. limits.minY = Math.min(limits.minY, y);
  65. limits.minX = Math.min(limits.minX, x);
  66. limits.maxX = Math.max(limits.maxX, x);
  67. limits.maxY = Math.max(limits.maxY, y);
  68. return limits;
  69. },
  70. {
  71. minX: Infinity,
  72. minY: Infinity,
  73. maxX: -Infinity,
  74. maxY: -Infinity,
  75. cx: 0,
  76. cy: 0,
  77. },
  78. );
  79. ret.cx = (ret.maxX + ret.minX) / 2;
  80. ret.cy = (ret.maxY + ret.minY) / 2;
  81. return ret;
  82. };
  83. const getRotatedBBox = (element: Element): Bounds => {
  84. const points = getElementRelativePoints(element);
  85. const { cx, cy } = getMinMaxPoints(points);
  86. const centerPoint = pointFrom<LocalPoint>(cx, cy);
  87. const rotatedPoints = points.map((p) =>
  88. pointRotateRads(p, centerPoint, element.angle),
  89. );
  90. const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints);
  91. return [
  92. minX + element.x,
  93. minY + element.y,
  94. maxX + element.x,
  95. maxY + element.y,
  96. ];
  97. };
  98. export const isElementInsideBBox = (
  99. element: Element,
  100. bbox: Bounds,
  101. eitherDirection = false,
  102. ): boolean => {
  103. const elementBBox = getRotatedBBox(element);
  104. const elementInsideBbox =
  105. bbox[0] <= elementBBox[0] &&
  106. bbox[2] >= elementBBox[2] &&
  107. bbox[1] <= elementBBox[1] &&
  108. bbox[3] >= elementBBox[3];
  109. if (!eitherDirection) {
  110. return elementInsideBbox;
  111. }
  112. if (elementInsideBbox) {
  113. return true;
  114. }
  115. return (
  116. elementBBox[0] <= bbox[0] &&
  117. elementBBox[2] >= bbox[2] &&
  118. elementBBox[1] <= bbox[1] &&
  119. elementBBox[3] >= bbox[3]
  120. );
  121. };
  122. export const elementPartiallyOverlapsWithOrContainsBBox = (
  123. element: Element,
  124. bbox: Bounds,
  125. ): boolean => {
  126. const elementBBox = getRotatedBBox(element);
  127. return (
  128. (rangeIncludesValue(elementBBox[0], rangeInclusive(bbox[0], bbox[2])) ||
  129. rangeIncludesValue(
  130. bbox[0],
  131. rangeInclusive(elementBBox[0], elementBBox[2]),
  132. )) &&
  133. (rangeIncludesValue(elementBBox[1], rangeInclusive(bbox[1], bbox[3])) ||
  134. rangeIncludesValue(
  135. bbox[1],
  136. rangeInclusive(elementBBox[1], elementBBox[3]),
  137. ))
  138. );
  139. };
  140. export const elementsOverlappingBBox = ({
  141. elements,
  142. bounds,
  143. type,
  144. errorMargin = 0,
  145. }: {
  146. elements: Elements;
  147. bounds: Bounds | ExcalidrawElement;
  148. /** safety offset. Defaults to 0. */
  149. errorMargin?: number;
  150. /**
  151. * - overlap: elements overlapping or inside bounds
  152. * - contain: elements inside bounds or bounds inside elements
  153. * - inside: elements inside bounds
  154. **/
  155. type: "overlap" | "contain" | "inside";
  156. }) => {
  157. if (isExcalidrawElement(bounds)) {
  158. bounds = getElementBounds(bounds, arrayToMap(elements));
  159. }
  160. const adjustedBBox: Bounds = [
  161. bounds[0] - errorMargin,
  162. bounds[1] - errorMargin,
  163. bounds[2] + errorMargin,
  164. bounds[3] + errorMargin,
  165. ];
  166. const includedElementSet = new Set<string>();
  167. for (const element of elements) {
  168. if (includedElementSet.has(element.id)) {
  169. continue;
  170. }
  171. const isOverlaping =
  172. type === "overlap"
  173. ? elementPartiallyOverlapsWithOrContainsBBox(element, adjustedBBox)
  174. : type === "inside"
  175. ? isElementInsideBBox(element, adjustedBBox)
  176. : isElementInsideBBox(element, adjustedBBox, true);
  177. if (isOverlaping) {
  178. includedElementSet.add(element.id);
  179. if (element.boundElements) {
  180. for (const boundElement of element.boundElements) {
  181. includedElementSet.add(boundElement.id);
  182. }
  183. }
  184. if (isTextElement(element) && element.containerId) {
  185. includedElementSet.add(element.containerId);
  186. }
  187. if (isArrowElement(element)) {
  188. if (element.startBinding) {
  189. includedElementSet.add(element.startBinding.elementId);
  190. }
  191. if (element.endBinding) {
  192. includedElementSet.add(element.endBinding?.elementId);
  193. }
  194. }
  195. }
  196. }
  197. return elements.filter((element) => includedElementSet.has(element.id));
  198. };