transformHandles.ts 6.2 KB

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