resizeElements.ts 23 KB

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