withinBounds.ts 5.3 KB

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