withinBounds.ts 5.3 KB

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