resizeElements.ts 24 KB

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