transformHandles.ts 6.5 KB

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