resizeElements.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. import { SHIFT_LOCKING_ANGLE } from "../constants";
  2. import { rescalePoints } from "../points";
  3. import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
  4. import {
  5. ExcalidrawLinearElement,
  6. ExcalidrawTextElement,
  7. NonDeletedExcalidrawElement,
  8. NonDeleted,
  9. } from "./types";
  10. import {
  11. getElementAbsoluteCoords,
  12. getCommonBounds,
  13. getResizedElementAbsoluteCoords,
  14. } from "./bounds";
  15. import { isLinearElement } from "./typeChecks";
  16. import { mutateElement } from "./mutateElement";
  17. import { getPerfectElementSize } from "./sizeHelpers";
  18. import {
  19. getCursorForResizingElement,
  20. normalizeTransformHandleType,
  21. } from "./resizeTest";
  22. import { measureText, getFontString } from "../utils";
  23. import { updateBoundElements } from "./binding";
  24. import {
  25. TransformHandleType,
  26. MaybeTransformHandleType,
  27. } from "./transformHandles";
  28. const normalizeAngle = (angle: number): number => {
  29. if (angle >= 2 * Math.PI) {
  30. return angle - 2 * Math.PI;
  31. }
  32. return angle;
  33. };
  34. // Returns true when transform (resizing/rotation) happened
  35. export const transformElements = (
  36. transformHandleType: MaybeTransformHandleType,
  37. setTransformHandle: (nextTransformHandle: MaybeTransformHandleType) => void,
  38. selectedElements: readonly NonDeletedExcalidrawElement[],
  39. resizeArrowDirection: "origin" | "end",
  40. isRotateWithDiscreteAngle: boolean,
  41. isResizeWithSidesSameLength: boolean,
  42. isResizeCenterPoint: boolean,
  43. pointerX: number,
  44. pointerY: number,
  45. centerX: number,
  46. centerY: number,
  47. originalElements: readonly NonDeletedExcalidrawElement[],
  48. ) => {
  49. if (selectedElements.length === 1) {
  50. const [element] = selectedElements;
  51. if (transformHandleType === "rotation") {
  52. rotateSingleElement(
  53. element,
  54. pointerX,
  55. pointerY,
  56. isRotateWithDiscreteAngle,
  57. );
  58. updateBoundElements(element);
  59. } else if (
  60. isLinearElement(element) &&
  61. element.points.length === 2 &&
  62. (transformHandleType === "nw" ||
  63. transformHandleType === "ne" ||
  64. transformHandleType === "sw" ||
  65. transformHandleType === "se")
  66. ) {
  67. reshapeSingleTwoPointElement(
  68. element,
  69. resizeArrowDirection,
  70. isRotateWithDiscreteAngle,
  71. pointerX,
  72. pointerY,
  73. );
  74. } else if (
  75. element.type === "text" &&
  76. (transformHandleType === "nw" ||
  77. transformHandleType === "ne" ||
  78. transformHandleType === "sw" ||
  79. transformHandleType === "se")
  80. ) {
  81. resizeSingleTextElement(
  82. element,
  83. transformHandleType,
  84. isResizeCenterPoint,
  85. pointerX,
  86. pointerY,
  87. );
  88. updateBoundElements(element);
  89. } else if (transformHandleType) {
  90. resizeSingleElement(
  91. element,
  92. transformHandleType,
  93. isResizeWithSidesSameLength,
  94. isResizeCenterPoint,
  95. pointerX,
  96. pointerY,
  97. );
  98. setTransformHandle(
  99. normalizeTransformHandleType(element, transformHandleType),
  100. );
  101. if (element.width < 0) {
  102. mutateElement(element, { width: -element.width });
  103. }
  104. if (element.height < 0) {
  105. mutateElement(element, { height: -element.height });
  106. }
  107. }
  108. // update cursor
  109. // FIXME it is not very nice to have this here
  110. document.documentElement.style.cursor = getCursorForResizingElement({
  111. element,
  112. transformHandleType,
  113. });
  114. return true;
  115. } else if (selectedElements.length > 1) {
  116. if (transformHandleType === "rotation") {
  117. rotateMultipleElements(
  118. selectedElements,
  119. pointerX,
  120. pointerY,
  121. isRotateWithDiscreteAngle,
  122. centerX,
  123. centerY,
  124. originalElements,
  125. );
  126. return true;
  127. } else if (
  128. transformHandleType === "nw" ||
  129. transformHandleType === "ne" ||
  130. transformHandleType === "sw" ||
  131. transformHandleType === "se"
  132. ) {
  133. resizeMultipleElements(
  134. selectedElements,
  135. transformHandleType,
  136. pointerX,
  137. pointerY,
  138. );
  139. return true;
  140. }
  141. }
  142. return false;
  143. };
  144. const rotateSingleElement = (
  145. element: NonDeletedExcalidrawElement,
  146. pointerX: number,
  147. pointerY: number,
  148. isRotateWithDiscreteAngle: boolean,
  149. ) => {
  150. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  151. const cx = (x1 + x2) / 2;
  152. const cy = (y1 + y2) / 2;
  153. let angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
  154. if (isRotateWithDiscreteAngle) {
  155. angle += SHIFT_LOCKING_ANGLE / 2;
  156. angle -= angle % SHIFT_LOCKING_ANGLE;
  157. }
  158. angle = normalizeAngle(angle);
  159. mutateElement(element, { angle });
  160. };
  161. // used in DEV only
  162. const validateTwoPointElementNormalized = (
  163. element: NonDeleted<ExcalidrawLinearElement>,
  164. ) => {
  165. if (
  166. element.points.length !== 2 ||
  167. element.points[0][0] !== 0 ||
  168. element.points[0][1] !== 0 ||
  169. Math.abs(element.points[1][0]) !== element.width ||
  170. Math.abs(element.points[1][1]) !== element.height
  171. ) {
  172. throw new Error("Two-point element is not normalized");
  173. }
  174. };
  175. const getPerfectElementSizeWithRotation = (
  176. elementType: string,
  177. width: number,
  178. height: number,
  179. angle: number,
  180. ): [number, number] => {
  181. const size = getPerfectElementSize(
  182. elementType,
  183. ...rotate(width, height, 0, 0, angle),
  184. );
  185. return rotate(size.width, size.height, 0, 0, -angle);
  186. };
  187. const reshapeSingleTwoPointElement = (
  188. element: NonDeleted<ExcalidrawLinearElement>,
  189. resizeArrowDirection: "origin" | "end",
  190. isRotateWithDiscreteAngle: boolean,
  191. pointerX: number,
  192. pointerY: number,
  193. ) => {
  194. if (process.env.NODE_ENV !== "production") {
  195. validateTwoPointElementNormalized(element);
  196. }
  197. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  198. const cx = (x1 + x2) / 2;
  199. const cy = (y1 + y2) / 2;
  200. // rotation pointer with reverse angle
  201. const [rotatedX, rotatedY] = rotate(
  202. pointerX,
  203. pointerY,
  204. cx,
  205. cy,
  206. -element.angle,
  207. );
  208. let [width, height] =
  209. resizeArrowDirection === "end"
  210. ? [rotatedX - element.x, rotatedY - element.y]
  211. : [
  212. element.x + element.points[1][0] - rotatedX,
  213. element.y + element.points[1][1] - rotatedY,
  214. ];
  215. if (isRotateWithDiscreteAngle) {
  216. [width, height] = getPerfectElementSizeWithRotation(
  217. element.type,
  218. width,
  219. height,
  220. element.angle,
  221. );
  222. }
  223. const [nextElementX, nextElementY] = adjustXYWithRotation(
  224. resizeArrowDirection === "end"
  225. ? { s: true, e: true }
  226. : { n: true, w: true },
  227. element.x,
  228. element.y,
  229. element.angle,
  230. 0,
  231. 0,
  232. (element.points[1][0] - width) / 2,
  233. (element.points[1][1] - height) / 2,
  234. );
  235. mutateElement(element, {
  236. x: nextElementX,
  237. y: nextElementY,
  238. points: [
  239. [0, 0],
  240. [width, height],
  241. ],
  242. });
  243. };
  244. const rescalePointsInElement = (
  245. element: NonDeletedExcalidrawElement,
  246. width: number,
  247. height: number,
  248. ) =>
  249. isLinearElement(element)
  250. ? {
  251. points: rescalePoints(
  252. 0,
  253. width,
  254. rescalePoints(1, height, element.points),
  255. ),
  256. }
  257. : {};
  258. const MIN_FONT_SIZE = 1;
  259. const measureFontSizeFromWH = (
  260. element: NonDeleted<ExcalidrawTextElement>,
  261. nextWidth: number,
  262. nextHeight: number,
  263. ): { size: number; baseline: number } | null => {
  264. // We only use width to scale font on resize
  265. const nextFontSize = element.fontSize * (nextWidth / element.width);
  266. if (nextFontSize < MIN_FONT_SIZE) {
  267. return null;
  268. }
  269. const metrics = measureText(
  270. element.text,
  271. getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
  272. );
  273. return {
  274. size: nextFontSize,
  275. baseline: metrics.baseline + (nextHeight - metrics.height),
  276. };
  277. };
  278. const getSidesForTransformHandle = (
  279. transformHandleType: TransformHandleType,
  280. isResizeFromCenter: boolean,
  281. ) => {
  282. return {
  283. n:
  284. /^(n|ne|nw)$/.test(transformHandleType) ||
  285. (isResizeFromCenter && /^(s|se|sw)$/.test(transformHandleType)),
  286. s:
  287. /^(s|se|sw)$/.test(transformHandleType) ||
  288. (isResizeFromCenter && /^(n|ne|nw)$/.test(transformHandleType)),
  289. w:
  290. /^(w|nw|sw)$/.test(transformHandleType) ||
  291. (isResizeFromCenter && /^(e|ne|se)$/.test(transformHandleType)),
  292. e:
  293. /^(e|ne|se)$/.test(transformHandleType) ||
  294. (isResizeFromCenter && /^(w|nw|sw)$/.test(transformHandleType)),
  295. };
  296. };
  297. const resizeSingleTextElement = (
  298. element: NonDeleted<ExcalidrawTextElement>,
  299. transformHandleType: "nw" | "ne" | "sw" | "se",
  300. isResizeFromCenter: boolean,
  301. pointerX: number,
  302. pointerY: number,
  303. ) => {
  304. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  305. const cx = (x1 + x2) / 2;
  306. const cy = (y1 + y2) / 2;
  307. // rotation pointer with reverse angle
  308. const [rotatedX, rotatedY] = rotate(
  309. pointerX,
  310. pointerY,
  311. cx,
  312. cy,
  313. -element.angle,
  314. );
  315. let scale;
  316. switch (transformHandleType) {
  317. case "se":
  318. scale = Math.max(
  319. (rotatedX - x1) / (x2 - x1),
  320. (rotatedY - y1) / (y2 - y1),
  321. );
  322. break;
  323. case "nw":
  324. scale = Math.max(
  325. (x2 - rotatedX) / (x2 - x1),
  326. (y2 - rotatedY) / (y2 - y1),
  327. );
  328. break;
  329. case "ne":
  330. scale = Math.max(
  331. (rotatedX - x1) / (x2 - x1),
  332. (y2 - rotatedY) / (y2 - y1),
  333. );
  334. break;
  335. case "sw":
  336. scale = Math.max(
  337. (x2 - rotatedX) / (x2 - x1),
  338. (rotatedY - y1) / (y2 - y1),
  339. );
  340. break;
  341. }
  342. if (scale > 0) {
  343. const nextWidth = element.width * scale;
  344. const nextHeight = element.height * scale;
  345. const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
  346. if (nextFont === null) {
  347. return;
  348. }
  349. const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
  350. element,
  351. nextWidth,
  352. nextHeight,
  353. );
  354. const deltaX1 = (x1 - nextX1) / 2;
  355. const deltaY1 = (y1 - nextY1) / 2;
  356. const deltaX2 = (x2 - nextX2) / 2;
  357. const deltaY2 = (y2 - nextY2) / 2;
  358. const [nextElementX, nextElementY] = adjustXYWithRotation(
  359. getSidesForTransformHandle(transformHandleType, isResizeFromCenter),
  360. element.x,
  361. element.y,
  362. element.angle,
  363. deltaX1,
  364. deltaY1,
  365. deltaX2,
  366. deltaY2,
  367. );
  368. mutateElement(element, {
  369. fontSize: nextFont.size,
  370. width: nextWidth,
  371. height: nextHeight,
  372. baseline: nextFont.baseline,
  373. x: nextElementX,
  374. y: nextElementY,
  375. });
  376. }
  377. };
  378. const resizeSingleElement = (
  379. element: NonDeletedExcalidrawElement,
  380. transformHandleType: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
  381. sidesWithSameLength: boolean,
  382. isResizeFromCenter: boolean,
  383. pointerX: number,
  384. pointerY: number,
  385. ) => {
  386. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  387. const cx = (x1 + x2) / 2;
  388. const cy = (y1 + y2) / 2;
  389. // rotation pointer with reverse angle
  390. const [rotatedX, rotatedY] = rotate(
  391. pointerX,
  392. pointerY,
  393. cx,
  394. cy,
  395. -element.angle,
  396. );
  397. let scaleX = 1;
  398. let scaleY = 1;
  399. if (
  400. transformHandleType === "e" ||
  401. transformHandleType === "ne" ||
  402. transformHandleType === "se"
  403. ) {
  404. scaleX = (rotatedX - x1) / (x2 - x1);
  405. }
  406. if (
  407. transformHandleType === "s" ||
  408. transformHandleType === "sw" ||
  409. transformHandleType === "se"
  410. ) {
  411. scaleY = (rotatedY - y1) / (y2 - y1);
  412. }
  413. if (
  414. transformHandleType === "w" ||
  415. transformHandleType === "nw" ||
  416. transformHandleType === "sw"
  417. ) {
  418. scaleX = (x2 - rotatedX) / (x2 - x1);
  419. }
  420. if (
  421. transformHandleType === "n" ||
  422. transformHandleType === "nw" ||
  423. transformHandleType === "ne"
  424. ) {
  425. scaleY = (y2 - rotatedY) / (y2 - y1);
  426. }
  427. let nextWidth = element.width * scaleX;
  428. let nextHeight = element.height * scaleY;
  429. if (sidesWithSameLength) {
  430. nextWidth = nextHeight = Math.max(nextWidth, nextHeight);
  431. }
  432. const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
  433. element,
  434. nextWidth,
  435. nextHeight,
  436. );
  437. const deltaX1 = (x1 - nextX1) / 2;
  438. const deltaY1 = (y1 - nextY1) / 2;
  439. const deltaX2 = (x2 - nextX2) / 2;
  440. const deltaY2 = (y2 - nextY2) / 2;
  441. const rescaledPoints = rescalePointsInElement(element, nextWidth, nextHeight);
  442. updateBoundElements(element, {
  443. newSize: { width: nextWidth, height: nextHeight },
  444. });
  445. const [finalX1, finalY1, finalX2, finalY2] = getResizedElementAbsoluteCoords(
  446. {
  447. ...element,
  448. ...rescaledPoints,
  449. },
  450. Math.abs(nextWidth),
  451. Math.abs(nextHeight),
  452. );
  453. const [flipDiffX, flipDiffY] = getFlipAdjustment(
  454. transformHandleType,
  455. nextWidth,
  456. nextHeight,
  457. nextX1,
  458. nextY1,
  459. nextX2,
  460. nextY2,
  461. finalX1,
  462. finalY1,
  463. finalX2,
  464. finalY2,
  465. isLinearElement(element),
  466. element.angle,
  467. );
  468. const [nextElementX, nextElementY] = adjustXYWithRotation(
  469. getSidesForTransformHandle(transformHandleType, isResizeFromCenter),
  470. element.x - flipDiffX,
  471. element.y - flipDiffY,
  472. element.angle,
  473. deltaX1,
  474. deltaY1,
  475. deltaX2,
  476. deltaY2,
  477. );
  478. if (
  479. nextWidth !== 0 &&
  480. nextHeight !== 0 &&
  481. Number.isFinite(nextElementX) &&
  482. Number.isFinite(nextElementY)
  483. ) {
  484. mutateElement(element, {
  485. width: nextWidth,
  486. height: nextHeight,
  487. x: nextElementX,
  488. y: nextElementY,
  489. ...rescaledPoints,
  490. });
  491. }
  492. };
  493. const resizeMultipleElements = (
  494. elements: readonly NonDeletedExcalidrawElement[],
  495. transformHandleType: "nw" | "ne" | "sw" | "se",
  496. pointerX: number,
  497. pointerY: number,
  498. ) => {
  499. const [x1, y1, x2, y2] = getCommonBounds(elements);
  500. let scale: number;
  501. let getNextXY: (
  502. element: NonDeletedExcalidrawElement,
  503. origCoords: readonly [number, number, number, number],
  504. finalCoords: readonly [number, number, number, number],
  505. ) => { x: number; y: number };
  506. switch (transformHandleType) {
  507. case "se":
  508. scale = Math.max(
  509. (pointerX - x1) / (x2 - x1),
  510. (pointerY - y1) / (y2 - y1),
  511. );
  512. getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
  513. const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
  514. const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
  515. return { x, y };
  516. };
  517. break;
  518. case "nw":
  519. scale = Math.max(
  520. (x2 - pointerX) / (x2 - x1),
  521. (y2 - pointerY) / (y2 - y1),
  522. );
  523. getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
  524. const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
  525. const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
  526. return { x, y };
  527. };
  528. break;
  529. case "ne":
  530. scale = Math.max(
  531. (pointerX - x1) / (x2 - x1),
  532. (y2 - pointerY) / (y2 - y1),
  533. );
  534. getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
  535. const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
  536. const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
  537. return { x, y };
  538. };
  539. break;
  540. case "sw":
  541. scale = Math.max(
  542. (x2 - pointerX) / (x2 - x1),
  543. (pointerY - y1) / (y2 - y1),
  544. );
  545. getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
  546. const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
  547. const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
  548. return { x, y };
  549. };
  550. break;
  551. }
  552. if (scale > 0) {
  553. const updates = elements.reduce(
  554. (prev, element) => {
  555. if (!prev) {
  556. return prev;
  557. }
  558. const width = element.width * scale;
  559. const height = element.height * scale;
  560. let font: { fontSize?: number; baseline?: number } = {};
  561. if (element.type === "text") {
  562. const nextFont = measureFontSizeFromWH(element, width, height);
  563. if (nextFont === null) {
  564. return null;
  565. }
  566. font = { fontSize: nextFont.size, baseline: nextFont.baseline };
  567. }
  568. const origCoords = getElementAbsoluteCoords(element);
  569. const rescaledPoints = rescalePointsInElement(element, width, height);
  570. updateBoundElements(element, {
  571. newSize: { width, height },
  572. simultaneouslyUpdated: elements,
  573. });
  574. const finalCoords = getResizedElementAbsoluteCoords(
  575. {
  576. ...element,
  577. ...rescaledPoints,
  578. },
  579. width,
  580. height,
  581. );
  582. const { x, y } = getNextXY(element, origCoords, finalCoords);
  583. return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
  584. },
  585. [] as
  586. | {
  587. width: number;
  588. height: number;
  589. x: number;
  590. y: number;
  591. points?: (readonly [number, number])[];
  592. fontSize?: number;
  593. baseline?: number;
  594. }[]
  595. | null,
  596. );
  597. if (updates) {
  598. elements.forEach((element, index) => {
  599. mutateElement(element, updates[index]);
  600. });
  601. }
  602. }
  603. };
  604. const rotateMultipleElements = (
  605. elements: readonly NonDeletedExcalidrawElement[],
  606. pointerX: number,
  607. pointerY: number,
  608. isRotateWithDiscreteAngle: boolean,
  609. centerX: number,
  610. centerY: number,
  611. originalElements: readonly NonDeletedExcalidrawElement[],
  612. ) => {
  613. let centerAngle =
  614. (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
  615. if (isRotateWithDiscreteAngle) {
  616. centerAngle += SHIFT_LOCKING_ANGLE / 2;
  617. centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
  618. }
  619. elements.forEach((element, index) => {
  620. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
  621. const cx = (x1 + x2) / 2;
  622. const cy = (y1 + y2) / 2;
  623. const [rotatedCX, rotatedCY] = rotate(
  624. cx,
  625. cy,
  626. centerX,
  627. centerY,
  628. centerAngle + originalElements[index].angle - element.angle,
  629. );
  630. mutateElement(element, {
  631. x: element.x + (rotatedCX - cx),
  632. y: element.y + (rotatedCY - cy),
  633. angle: normalizeAngle(centerAngle + originalElements[index].angle),
  634. });
  635. });
  636. };
  637. export const getResizeOffsetXY = (
  638. transformHandleType: MaybeTransformHandleType,
  639. selectedElements: NonDeletedExcalidrawElement[],
  640. x: number,
  641. y: number,
  642. ): [number, number] => {
  643. const [x1, y1, x2, y2] =
  644. selectedElements.length === 1
  645. ? getElementAbsoluteCoords(selectedElements[0])
  646. : getCommonBounds(selectedElements);
  647. const cx = (x1 + x2) / 2;
  648. const cy = (y1 + y2) / 2;
  649. const angle = selectedElements.length === 1 ? selectedElements[0].angle : 0;
  650. [x, y] = rotate(x, y, cx, cy, -angle);
  651. switch (transformHandleType) {
  652. case "n":
  653. return rotate(x - (x1 + x2) / 2, y - y1, 0, 0, angle);
  654. case "s":
  655. return rotate(x - (x1 + x2) / 2, y - y2, 0, 0, angle);
  656. case "w":
  657. return rotate(x - x1, y - (y1 + y2) / 2, 0, 0, angle);
  658. case "e":
  659. return rotate(x - x2, y - (y1 + y2) / 2, 0, 0, angle);
  660. case "nw":
  661. return rotate(x - x1, y - y1, 0, 0, angle);
  662. case "ne":
  663. return rotate(x - x2, y - y1, 0, 0, angle);
  664. case "sw":
  665. return rotate(x - x1, y - y2, 0, 0, angle);
  666. case "se":
  667. return rotate(x - x2, y - y2, 0, 0, angle);
  668. default:
  669. return [0, 0];
  670. }
  671. };
  672. export const getResizeArrowDirection = (
  673. transformHandleType: MaybeTransformHandleType,
  674. element: NonDeleted<ExcalidrawLinearElement>,
  675. ): "origin" | "end" => {
  676. const [, [px, py]] = element.points;
  677. const isResizeEnd =
  678. (transformHandleType === "nw" && (px < 0 || py < 0)) ||
  679. (transformHandleType === "ne" && px >= 0) ||
  680. (transformHandleType === "sw" && px <= 0) ||
  681. (transformHandleType === "se" && (px > 0 || py > 0));
  682. return isResizeEnd ? "end" : "origin";
  683. };