transformHandles.ts 7.1 KB

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