resizeElements.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. import { SHIFT_LOCKING_ANGLE } from "../constants";
  2. import { rescalePoints } from "../points";
  3. import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
  4. import {
  5. ExcalidrawLinearElement,
  6. NonDeletedExcalidrawElement,
  7. NonDeleted,
  8. } from "./types";
  9. import {
  10. getElementAbsoluteCoords,
  11. getCommonBounds,
  12. getResizedElementAbsoluteCoords,
  13. } from "./bounds";
  14. import { isLinearElement } from "./typeChecks";
  15. import { mutateElement } from "./mutateElement";
  16. import { getPerfectElementSize } from "./sizeHelpers";
  17. import {
  18. resizeTest,
  19. getCursorForResizingElement,
  20. normalizeResizeHandle,
  21. } from "./resizeTest";
  22. import {
  23. getResizeCenterPointKey,
  24. getResizeWithSidesSameLengthKey,
  25. } from "../keys";
  26. type ResizeTestType = ReturnType<typeof resizeTest>;
  27. export const resizeElements = (
  28. resizeHandle: ResizeTestType,
  29. setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
  30. selectedElements: NonDeletedExcalidrawElement[],
  31. resizeArrowDirection: "origin" | "end",
  32. event: PointerEvent, // XXX we want to make it independent?
  33. pointerX: number,
  34. pointerY: number,
  35. ) => {
  36. if (selectedElements.length === 1) {
  37. const [element] = selectedElements;
  38. if (resizeHandle === "rotation") {
  39. rotateSingleElement(element, pointerX, pointerY, event.shiftKey);
  40. } else if (
  41. isLinearElement(element) &&
  42. element.points.length === 2 &&
  43. (resizeHandle === "nw" ||
  44. resizeHandle === "ne" ||
  45. resizeHandle === "sw" ||
  46. resizeHandle === "se")
  47. ) {
  48. resizeSingleTwoPointElement(
  49. element,
  50. resizeArrowDirection,
  51. event.shiftKey,
  52. pointerX,
  53. pointerY,
  54. );
  55. } else if (resizeHandle) {
  56. resizeSingleElement(
  57. element,
  58. resizeHandle,
  59. getResizeWithSidesSameLengthKey(event),
  60. getResizeCenterPointKey(event),
  61. pointerX,
  62. pointerY,
  63. );
  64. setResizeHandle(normalizeResizeHandle(element, resizeHandle));
  65. if (element.width < 0) {
  66. mutateElement(element, { width: -element.width });
  67. }
  68. if (element.height < 0) {
  69. mutateElement(element, { height: -element.height });
  70. }
  71. }
  72. // update cursor
  73. // FIXME it is not very nice to have this here
  74. document.documentElement.style.cursor = getCursorForResizingElement({
  75. element,
  76. resizeHandle,
  77. });
  78. return true;
  79. } else if (
  80. selectedElements.length > 1 &&
  81. (resizeHandle === "nw" ||
  82. resizeHandle === "ne" ||
  83. resizeHandle === "sw" ||
  84. resizeHandle === "se")
  85. ) {
  86. resizeMultipleElements(selectedElements, resizeHandle, pointerX, pointerY);
  87. return true;
  88. }
  89. return false;
  90. };
  91. const rotateSingleElement = (
  92. element: NonDeletedExcalidrawElement,
  93. pointerX: number,
  94. pointerY: number,
  95. isAngleLocking: boolean,
  96. ) => {
  97. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  98. const cx = (x1 + x2) / 2;
  99. const cy = (y1 + y2) / 2;
  100. let angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
  101. if (isAngleLocking) {
  102. angle += SHIFT_LOCKING_ANGLE / 2;
  103. angle -= angle % SHIFT_LOCKING_ANGLE;
  104. }
  105. if (angle >= 2 * Math.PI) {
  106. angle -= 2 * Math.PI;
  107. }
  108. mutateElement(element, { angle });
  109. };
  110. const resizeSingleTwoPointElement = (
  111. element: NonDeleted<ExcalidrawLinearElement>,
  112. resizeArrowDirection: "origin" | "end",
  113. isAngleLocking: boolean,
  114. pointerX: number,
  115. pointerY: number,
  116. ) => {
  117. const pointOrigin = element.points[0]; // can assume always [0, 0]?
  118. const pointEnd = element.points[1];
  119. if (resizeArrowDirection === "end") {
  120. if (isAngleLocking) {
  121. const { width, height } = getPerfectElementSize(
  122. element.type,
  123. pointerX - element.x,
  124. pointerY - element.y,
  125. );
  126. mutateElement(element, {
  127. points: [pointOrigin, [width, height]],
  128. });
  129. } else {
  130. mutateElement(element, {
  131. points: [
  132. pointOrigin,
  133. [
  134. pointerX - pointOrigin[0] - element.x,
  135. pointerY - pointOrigin[1] - element.y,
  136. ],
  137. ],
  138. });
  139. }
  140. } else {
  141. // resizeArrowDirection === "origin"
  142. if (isAngleLocking) {
  143. const { width, height } = getPerfectElementSize(
  144. element.type,
  145. element.x + pointEnd[0] - pointOrigin[0] - pointerX,
  146. element.y + pointEnd[1] - pointOrigin[1] - pointerY,
  147. );
  148. mutateElement(element, {
  149. x: element.x + pointEnd[0] - pointOrigin[0] - width,
  150. y: element.y + pointEnd[1] - pointOrigin[1] - height,
  151. points: [pointOrigin, [width, height]],
  152. });
  153. } else {
  154. mutateElement(element, {
  155. x: pointerX,
  156. y: pointerY,
  157. points: [
  158. pointOrigin,
  159. [
  160. pointEnd[0] - (pointerX - pointOrigin[0] - element.x),
  161. pointEnd[1] - (pointerY - pointOrigin[1] - element.y),
  162. ],
  163. ],
  164. });
  165. }
  166. }
  167. };
  168. const rescalePointsInElement = (
  169. element: NonDeletedExcalidrawElement,
  170. width: number,
  171. height: number,
  172. ) =>
  173. isLinearElement(element)
  174. ? {
  175. points: rescalePoints(
  176. 0,
  177. width,
  178. rescalePoints(1, height, element.points),
  179. ),
  180. }
  181. : {};
  182. const resizeSingleElement = (
  183. element: NonDeletedExcalidrawElement,
  184. resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
  185. sidesWithSameLength: boolean,
  186. isResizeFromCenter: boolean,
  187. pointerX: number,
  188. pointerY: number,
  189. ) => {
  190. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  191. const cx = (x1 + x2) / 2;
  192. const cy = (y1 + y2) / 2;
  193. // rotation pointer with reverse angle
  194. const [rotatedX, rotatedY] = rotate(
  195. pointerX,
  196. pointerY,
  197. cx,
  198. cy,
  199. -element.angle,
  200. );
  201. let scaleX = 1;
  202. let scaleY = 1;
  203. if (resizeHandle === "e" || resizeHandle === "ne" || resizeHandle === "se") {
  204. scaleX = (rotatedX - x1) / (x2 - x1);
  205. }
  206. if (resizeHandle === "s" || resizeHandle === "sw" || resizeHandle === "se") {
  207. scaleY = (rotatedY - y1) / (y2 - y1);
  208. }
  209. if (resizeHandle === "w" || resizeHandle === "nw" || resizeHandle === "sw") {
  210. scaleX = (x2 - rotatedX) / (x2 - x1);
  211. }
  212. if (resizeHandle === "n" || resizeHandle === "nw" || resizeHandle === "ne") {
  213. scaleY = (y2 - rotatedY) / (y2 - y1);
  214. }
  215. let nextWidth = element.width * scaleX;
  216. let nextHeight = element.height * scaleY;
  217. if (sidesWithSameLength) {
  218. nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
  219. }
  220. const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
  221. element,
  222. nextWidth,
  223. nextHeight,
  224. );
  225. const deltaX1 = (x1 - nextX1) / 2;
  226. const deltaY1 = (y1 - nextY1) / 2;
  227. const deltaX2 = (x2 - nextX2) / 2;
  228. const deltaY2 = (y2 - nextY2) / 2;
  229. const rescaledPoints = rescalePointsInElement(element, nextWidth, nextHeight);
  230. const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords(
  231. {
  232. ...element,
  233. ...rescaledPoints,
  234. },
  235. Math.abs(nextWidth),
  236. Math.abs(nextHeight),
  237. );
  238. const [flipDiffX, flipDiffY] = getFlipAdjustment(
  239. resizeHandle,
  240. nextWidth,
  241. nextHeight,
  242. nextX1,
  243. nextY1,
  244. nextX2,
  245. nextY2,
  246. finalX1,
  247. finalY1,
  248. finalX2,
  249. finalY2,
  250. isLinearElement(element),
  251. element.angle,
  252. );
  253. const [nextElementX, nextElementY] = adjustXYWithRotation(
  254. resizeHandle,
  255. element.x - flipDiffX,
  256. element.y - flipDiffY,
  257. element.angle,
  258. deltaX1,
  259. deltaY1,
  260. deltaX2,
  261. deltaY2,
  262. isResizeFromCenter,
  263. );
  264. if (
  265. nextWidth !== 0 &&
  266. nextHeight !== 0 &&
  267. Number.isFinite(nextElementX) &&
  268. Number.isFinite(nextElementY)
  269. ) {
  270. mutateElement(element, {
  271. width: nextWidth,
  272. height: nextHeight,
  273. x: nextElementX,
  274. y: nextElementY,
  275. ...rescaledPoints,
  276. });
  277. }
  278. };
  279. const resizeMultipleElements = (
  280. elements: readonly NonDeletedExcalidrawElement[],
  281. resizeHandle: "nw" | "ne" | "sw" | "se",
  282. pointerX: number,
  283. pointerY: number,
  284. ) => {
  285. const [x1, y1, x2, y2] = getCommonBounds(elements);
  286. switch (resizeHandle) {
  287. case "se": {
  288. const scale = Math.max(
  289. (pointerX - x1) / (x2 - x1),
  290. (pointerY - y1) / (y2 - y1),
  291. );
  292. if (scale > 0) {
  293. elements.forEach((element) => {
  294. const width = element.width * scale;
  295. const height = element.height * scale;
  296. const [origX1, origY1] = getElementAbsoluteCoords(element);
  297. const rescaledPoints = rescalePointsInElement(element, width, height);
  298. const [finalX1, finalY1] = getResizedElementAbsoluteCoords(
  299. {
  300. ...element,
  301. ...rescaledPoints,
  302. },
  303. width,
  304. height,
  305. );
  306. const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
  307. const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
  308. mutateElement(element, { width, height, x, y, ...rescaledPoints });
  309. });
  310. }
  311. break;
  312. }
  313. case "nw": {
  314. const scale = Math.max(
  315. (x2 - pointerX) / (x2 - x1),
  316. (y2 - pointerY) / (y2 - y1),
  317. );
  318. if (scale > 0) {
  319. elements.forEach((element) => {
  320. const width = element.width * scale;
  321. const height = element.height * scale;
  322. const [, , origX2, origY2] = getElementAbsoluteCoords(element);
  323. const rescaledPoints = rescalePointsInElement(element, width, height);
  324. const [, , finalX2, finalY2] = getResizedElementAbsoluteCoords(
  325. {
  326. ...element,
  327. ...rescaledPoints,
  328. },
  329. width,
  330. height,
  331. );
  332. const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
  333. const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
  334. mutateElement(element, { width, height, x, y, ...rescaledPoints });
  335. });
  336. }
  337. break;
  338. }
  339. case "ne": {
  340. const scale = Math.max(
  341. (pointerX - x1) / (x2 - x1),
  342. (y2 - pointerY) / (y2 - y1),
  343. );
  344. if (scale > 0) {
  345. elements.forEach((element) => {
  346. const width = element.width * scale;
  347. const height = element.height * scale;
  348. const [origX1, , , origY2] = getElementAbsoluteCoords(element);
  349. const rescaledPoints = rescalePointsInElement(element, width, height);
  350. const [finalX1, , , finalY2] = getResizedElementAbsoluteCoords(
  351. {
  352. ...element,
  353. ...rescaledPoints,
  354. },
  355. width,
  356. height,
  357. );
  358. const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
  359. const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
  360. mutateElement(element, { width, height, x, y, ...rescaledPoints });
  361. });
  362. }
  363. break;
  364. }
  365. case "sw": {
  366. const scale = Math.max(
  367. (x2 - pointerX) / (x2 - x1),
  368. (pointerY - y1) / (y2 - y1),
  369. );
  370. if (scale > 0) {
  371. elements.forEach((element) => {
  372. const width = element.width * scale;
  373. const height = element.height * scale;
  374. const [, origY1, origX2] = getElementAbsoluteCoords(element);
  375. const rescaledPoints = rescalePointsInElement(element, width, height);
  376. const [, finalY1, finalX2] = getResizedElementAbsoluteCoords(
  377. {
  378. ...element,
  379. ...rescaledPoints,
  380. },
  381. width,
  382. height,
  383. );
  384. const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
  385. const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
  386. mutateElement(element, { width, height, x, y, ...rescaledPoints });
  387. });
  388. }
  389. break;
  390. }
  391. }
  392. };
  393. export const canResizeMutlipleElements = (
  394. elements: readonly NonDeletedExcalidrawElement[],
  395. ) => {
  396. return elements.every(
  397. (element) =>
  398. ["rectangle", "diamond", "ellipse"].includes(element.type) ||
  399. isLinearElement(element),
  400. );
  401. };
  402. export const getResizeOffsetXY = (
  403. resizeHandle: ResizeTestType,
  404. selectedElements: NonDeletedExcalidrawElement[],
  405. x: number,
  406. y: number,
  407. ): [number, number] => {
  408. const [x1, y1, x2, y2] =
  409. selectedElements.length === 1
  410. ? getElementAbsoluteCoords(selectedElements[0])
  411. : getCommonBounds(selectedElements);
  412. const cx = (x1 + x2) / 2;
  413. const cy = (y1 + y2) / 2;
  414. const angle = selectedElements.length === 1 ? selectedElements[0].angle : 0;
  415. [x, y] = rotate(x, y, cx, cy, -angle);
  416. switch (resizeHandle) {
  417. case "n":
  418. return rotate(x - (x1 + x2) / 2, y - y1, 0, 0, angle);
  419. case "s":
  420. return rotate(x - (x1 + x2) / 2, y - y2, 0, 0, angle);
  421. case "w":
  422. return rotate(x - x1, y - (y1 + y2) / 2, 0, 0, angle);
  423. case "e":
  424. return rotate(x - x2, y - (y1 + y2) / 2, 0, 0, angle);
  425. case "nw":
  426. return rotate(x - x1, y - y1, 0, 0, angle);
  427. case "ne":
  428. return rotate(x - x2, y - y1, 0, 0, angle);
  429. case "sw":
  430. return rotate(x - x1, y - y2, 0, 0, angle);
  431. case "se":
  432. return rotate(x - x2, y - y2, 0, 0, angle);
  433. default:
  434. return [0, 0];
  435. }
  436. };
  437. export const getResizeArrowDirection = (
  438. resizeHandle: ResizeTestType,
  439. element: NonDeleted<ExcalidrawLinearElement>,
  440. ): "origin" | "end" => {
  441. const [, [px, py]] = element.points;
  442. const isResizeEnd =
  443. (resizeHandle === "nw" && (px < 0 || py < 0)) ||
  444. (resizeHandle === "ne" && px >= 0) ||
  445. (resizeHandle === "sw" && px <= 0) ||
  446. (resizeHandle === "se" && (px > 0 || py > 0));
  447. return isResizeEnd ? "end" : "origin";
  448. };