withinBounds.ts 5.4 KB

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