collision.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { distanceBetweenPointAndSegment } from "../math";
  2. import { ExcalidrawElement } from "./types";
  3. import {
  4. getArrowPoints,
  5. getDiamondPoints,
  6. getElementAbsoluteCoords,
  7. getLinePoints,
  8. } from "./bounds";
  9. function isElementDraggableFromInside(element: ExcalidrawElement): boolean {
  10. return element.backgroundColor !== "transparent" || element.isSelected;
  11. }
  12. export function hitTest(
  13. element: ExcalidrawElement,
  14. x: number,
  15. y: number,
  16. ): boolean {
  17. // For shapes that are composed of lines, we only enable point-selection when the distance
  18. // of the click is less than x pixels of any of the lines that the shape is composed of
  19. const lineThreshold = 10;
  20. if (element.type === "ellipse") {
  21. // https://stackoverflow.com/a/46007540/232122
  22. const px = Math.abs(x - element.x - element.width / 2);
  23. const py = Math.abs(y - element.y - element.height / 2);
  24. let tx = 0.707;
  25. let ty = 0.707;
  26. const a = Math.abs(element.width) / 2;
  27. const b = Math.abs(element.height) / 2;
  28. [0, 1, 2, 3].forEach(x => {
  29. const xx = a * tx;
  30. const yy = b * ty;
  31. const ex = ((a * a - b * b) * tx ** 3) / a;
  32. const ey = ((b * b - a * a) * ty ** 3) / b;
  33. const rx = xx - ex;
  34. const ry = yy - ey;
  35. const qx = px - ex;
  36. const qy = py - ey;
  37. const r = Math.hypot(ry, rx);
  38. const q = Math.hypot(qy, qx);
  39. tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
  40. ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
  41. const t = Math.hypot(ty, tx);
  42. tx /= t;
  43. ty /= t;
  44. });
  45. if (isElementDraggableFromInside(element)) {
  46. return (
  47. a * tx - (px - lineThreshold) >= 0 && b * ty - (py - lineThreshold) >= 0
  48. );
  49. } else {
  50. return Math.hypot(a * tx - px, b * ty - py) < lineThreshold;
  51. }
  52. } else if (element.type === "rectangle") {
  53. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  54. if (isElementDraggableFromInside(element)) {
  55. return (
  56. x > x1 - lineThreshold &&
  57. x < x2 + lineThreshold &&
  58. y > y1 - lineThreshold &&
  59. y < y2 + lineThreshold
  60. );
  61. }
  62. // (x1, y1) --A-- (x2, y1)
  63. // |D |B
  64. // (x1, y2) --C-- (x2, y2)
  65. return (
  66. distanceBetweenPointAndSegment(x, y, x1, y1, x2, y1) < lineThreshold || // A
  67. distanceBetweenPointAndSegment(x, y, x2, y1, x2, y2) < lineThreshold || // B
  68. distanceBetweenPointAndSegment(x, y, x2, y2, x1, y2) < lineThreshold || // C
  69. distanceBetweenPointAndSegment(x, y, x1, y2, x1, y1) < lineThreshold // D
  70. );
  71. } else if (element.type === "diamond") {
  72. x -= element.x;
  73. y -= element.y;
  74. let [
  75. topX,
  76. topY,
  77. rightX,
  78. rightY,
  79. bottomX,
  80. bottomY,
  81. leftX,
  82. leftY,
  83. ] = getDiamondPoints(element);
  84. if (isElementDraggableFromInside(element)) {
  85. // TODO: remove this when we normalize coordinates globally
  86. if (topY > bottomY) [bottomY, topY] = [topY, bottomY];
  87. if (rightX < leftX) [leftX, rightX] = [rightX, leftX];
  88. topY -= lineThreshold;
  89. bottomY += lineThreshold;
  90. leftX -= lineThreshold;
  91. rightX += lineThreshold;
  92. // all deltas should be < 0. Delta > 0 indicates it's on the outside side
  93. // of the line.
  94. //
  95. // (topX, topY)
  96. // D / \ A
  97. // / \
  98. // (leftX, leftY) (rightX, rightY)
  99. // C \ / B
  100. // \ /
  101. // (bottomX, bottomY)
  102. //
  103. // https://stackoverflow.com/a/2752753/927631
  104. return (
  105. // delta from line D
  106. (leftX - topX) * (y - leftY) - (leftX - x) * (topY - leftY) <= 0 &&
  107. // delta from line A
  108. (topX - rightX) * (y - rightY) - (x - rightX) * (topY - rightY) <= 0 &&
  109. // delta from line B
  110. (rightX - bottomX) * (y - bottomY) -
  111. (x - bottomX) * (rightY - bottomY) <=
  112. 0 &&
  113. // delta from line C
  114. (bottomX - leftX) * (y - leftY) - (x - leftX) * (bottomY - leftY) <= 0
  115. );
  116. }
  117. return (
  118. distanceBetweenPointAndSegment(x, y, topX, topY, rightX, rightY) <
  119. lineThreshold ||
  120. distanceBetweenPointAndSegment(x, y, rightX, rightY, bottomX, bottomY) <
  121. lineThreshold ||
  122. distanceBetweenPointAndSegment(x, y, bottomX, bottomY, leftX, leftY) <
  123. lineThreshold ||
  124. distanceBetweenPointAndSegment(x, y, leftX, leftY, topX, topY) <
  125. lineThreshold
  126. );
  127. } else if (element.type === "arrow") {
  128. let [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
  129. // The computation is done at the origin, we need to add a translation
  130. x -= element.x;
  131. y -= element.y;
  132. return (
  133. // \
  134. distanceBetweenPointAndSegment(x, y, x3, y3, x2, y2) < lineThreshold ||
  135. // -----
  136. distanceBetweenPointAndSegment(x, y, x1, y1, x2, y2) < lineThreshold ||
  137. // /
  138. distanceBetweenPointAndSegment(x, y, x4, y4, x2, y2) < lineThreshold
  139. );
  140. } else if (element.type === "line") {
  141. const [x1, y1, x2, y2] = getLinePoints(element);
  142. // The computation is done at the origin, we need to add a translation
  143. x -= element.x;
  144. y -= element.y;
  145. return distanceBetweenPointAndSegment(x, y, x1, y1, x2, y2) < lineThreshold;
  146. } else if (element.type === "text") {
  147. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  148. return x >= x1 && x <= x2 && y >= y1 && y <= y2;
  149. } else if (element.type === "selection") {
  150. console.warn("This should not happen, we need to investigate why it does.");
  151. return false;
  152. } else {
  153. throw new Error("Unimplemented type " + element.type);
  154. }
  155. }