handlerRectangles.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import { ExcalidrawElement, PointerType } from "./types";
  2. import { getElementAbsoluteCoords, Bounds } from "./bounds";
  3. import { rotate } from "../math";
  4. type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se" | "rotation";
  5. export type Handlers = Partial<
  6. { [T in Sides]: [number, number, number, number] }
  7. >;
  8. const handleSizes: { [k in PointerType]: number } = {
  9. mouse: 8,
  10. pen: 16,
  11. touch: 28,
  12. };
  13. const ROTATION_HANDLER_GAP = 16;
  14. export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
  15. e: true,
  16. s: true,
  17. n: true,
  18. w: true,
  19. };
  20. const OMIT_SIDES_FOR_TEXT_ELEMENT = {
  21. e: true,
  22. s: true,
  23. n: true,
  24. w: true,
  25. };
  26. const OMIT_SIDES_FOR_LINE_SLASH = {
  27. e: true,
  28. s: true,
  29. n: true,
  30. w: true,
  31. nw: true,
  32. se: true,
  33. rotation: true,
  34. };
  35. const OMIT_SIDES_FOR_LINE_BACKSLASH = {
  36. e: true,
  37. s: true,
  38. n: true,
  39. w: true,
  40. ne: true,
  41. sw: true,
  42. rotation: true,
  43. };
  44. const generateHandler = (
  45. x: number,
  46. y: number,
  47. width: number,
  48. height: number,
  49. cx: number,
  50. cy: number,
  51. angle: number,
  52. ): [number, number, number, number] => {
  53. const [xx, yy] = rotate(x + width / 2, y + height / 2, cx, cy, angle);
  54. return [xx - width / 2, yy - height / 2, width, height];
  55. };
  56. export const handlerRectanglesFromCoords = (
  57. [x1, y1, x2, y2]: Bounds,
  58. angle: number,
  59. zoom: number,
  60. pointerType: PointerType = "mouse",
  61. omitSides: { [T in Sides]?: boolean } = {},
  62. ): Handlers => {
  63. const size = handleSizes[pointerType];
  64. const handlerWidth = size / zoom;
  65. const handlerHeight = size / zoom;
  66. const handlerMarginX = size / zoom;
  67. const handlerMarginY = size / zoom;
  68. const width = x2 - x1;
  69. const height = y2 - y1;
  70. const cx = (x1 + x2) / 2;
  71. const cy = (y1 + y2) / 2;
  72. const dashedLineMargin = 4 / zoom;
  73. const centeringOffset = (size - 8) / (2 * zoom);
  74. const handlers: Partial<
  75. { [T in Sides]: [number, number, number, number] }
  76. > = {
  77. nw: omitSides["nw"]
  78. ? undefined
  79. : generateHandler(
  80. x1 - dashedLineMargin - handlerMarginX + centeringOffset,
  81. y1 - dashedLineMargin - handlerMarginY + centeringOffset,
  82. handlerWidth,
  83. handlerHeight,
  84. cx,
  85. cy,
  86. angle,
  87. ),
  88. ne: omitSides["ne"]
  89. ? undefined
  90. : generateHandler(
  91. x2 + dashedLineMargin - centeringOffset,
  92. y1 - dashedLineMargin - handlerMarginY + centeringOffset,
  93. handlerWidth,
  94. handlerHeight,
  95. cx,
  96. cy,
  97. angle,
  98. ),
  99. sw: omitSides["sw"]
  100. ? undefined
  101. : generateHandler(
  102. x1 - dashedLineMargin - handlerMarginX + centeringOffset,
  103. y2 + dashedLineMargin - centeringOffset,
  104. handlerWidth,
  105. handlerHeight,
  106. cx,
  107. cy,
  108. angle,
  109. ),
  110. se: omitSides["se"]
  111. ? undefined
  112. : generateHandler(
  113. x2 + dashedLineMargin - centeringOffset,
  114. y2 + dashedLineMargin - centeringOffset,
  115. handlerWidth,
  116. handlerHeight,
  117. cx,
  118. cy,
  119. angle,
  120. ),
  121. rotation: omitSides["rotation"]
  122. ? undefined
  123. : generateHandler(
  124. x1 + width / 2 - handlerWidth / 2,
  125. y1 -
  126. dashedLineMargin -
  127. handlerMarginY +
  128. centeringOffset -
  129. ROTATION_HANDLER_GAP / zoom,
  130. handlerWidth,
  131. handlerHeight,
  132. cx,
  133. cy,
  134. angle,
  135. ),
  136. };
  137. // We only want to show height handlers (all cardinal directions) above a certain size
  138. const minimumSizeForEightHandlers = (5 * size) / zoom;
  139. if (Math.abs(width) > minimumSizeForEightHandlers) {
  140. if (!omitSides["n"]) {
  141. handlers["n"] = generateHandler(
  142. x1 + width / 2 - handlerWidth / 2,
  143. y1 - dashedLineMargin - handlerMarginY + centeringOffset,
  144. handlerWidth,
  145. handlerHeight,
  146. cx,
  147. cy,
  148. angle,
  149. );
  150. }
  151. if (!omitSides["s"]) {
  152. handlers["s"] = generateHandler(
  153. x1 + width / 2 - handlerWidth / 2,
  154. y2 + dashedLineMargin - centeringOffset,
  155. handlerWidth,
  156. handlerHeight,
  157. cx,
  158. cy,
  159. angle,
  160. );
  161. }
  162. }
  163. if (Math.abs(height) > minimumSizeForEightHandlers) {
  164. if (!omitSides["w"]) {
  165. handlers["w"] = generateHandler(
  166. x1 - dashedLineMargin - handlerMarginX + centeringOffset,
  167. y1 + height / 2 - handlerHeight / 2,
  168. handlerWidth,
  169. handlerHeight,
  170. cx,
  171. cy,
  172. angle,
  173. );
  174. }
  175. if (!omitSides["e"]) {
  176. handlers["e"] = generateHandler(
  177. x2 + dashedLineMargin - centeringOffset,
  178. y1 + height / 2 - handlerHeight / 2,
  179. handlerWidth,
  180. handlerHeight,
  181. cx,
  182. cy,
  183. angle,
  184. );
  185. }
  186. }
  187. return handlers;
  188. };
  189. export const handlerRectangles = (
  190. element: ExcalidrawElement,
  191. zoom: number,
  192. pointerType: PointerType = "mouse",
  193. ) => {
  194. let omitSides: { [T in Sides]?: boolean } = {};
  195. if (
  196. element.type === "arrow" ||
  197. element.type === "line" ||
  198. element.type === "draw"
  199. ) {
  200. if (element.points.length === 2) {
  201. // only check the last point because starting point is always (0,0)
  202. const [, p1] = element.points;
  203. if (p1[0] === 0 || p1[1] === 0) {
  204. omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
  205. } else if (p1[0] > 0 && p1[1] < 0) {
  206. omitSides = OMIT_SIDES_FOR_LINE_SLASH;
  207. } else if (p1[0] > 0 && p1[1] > 0) {
  208. omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
  209. } else if (p1[0] < 0 && p1[1] > 0) {
  210. omitSides = OMIT_SIDES_FOR_LINE_SLASH;
  211. } else if (p1[0] < 0 && p1[1] < 0) {
  212. omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
  213. }
  214. }
  215. } else if (element.type === "text") {
  216. omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
  217. }
  218. return handlerRectanglesFromCoords(
  219. getElementAbsoluteCoords(element),
  220. element.angle,
  221. zoom,
  222. pointerType,
  223. omitSides,
  224. );
  225. };