@excalidraw/excalidraw 0.17.1-c0b80a0 → 0.17.1-c329470

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/browser/dev/excalidraw-assets-dev/{chunk-JGDL4H2X.js → chunk-3DLVY5XU.js} +8272 -6864
  2. package/dist/browser/dev/excalidraw-assets-dev/chunk-3DLVY5XU.js.map +7 -0
  3. package/dist/browser/dev/excalidraw-assets-dev/{chunk-V7NFEZA6.js → chunk-NOAEU4NM.js} +9 -2
  4. package/dist/browser/dev/excalidraw-assets-dev/chunk-NOAEU4NM.js.map +7 -0
  5. package/dist/browser/dev/excalidraw-assets-dev/{en-ZSVWGT55.js → en-7IBTMWBG.js} +2 -2
  6. package/dist/browser/dev/excalidraw-assets-dev/{image-RJG3J34Y.js → image-N5AC7SEK.js} +2 -6
  7. package/dist/browser/dev/index.css +85 -50
  8. package/dist/browser/dev/index.css.map +3 -3
  9. package/dist/browser/dev/index.js +4375 -3766
  10. package/dist/browser/dev/index.js.map +4 -4
  11. package/dist/browser/prod/excalidraw-assets/{chunk-LDVEIXGO.js → chunk-7CSIPVOW.js} +2 -2
  12. package/dist/browser/prod/excalidraw-assets/chunk-TX3BU7T2.js +47 -0
  13. package/dist/browser/prod/excalidraw-assets/{en-UPNEHLDS.js → en-LOGQBETY.js} +1 -1
  14. package/dist/browser/prod/excalidraw-assets/image-3V4U7GZE.js +1 -0
  15. package/dist/browser/prod/index.css +1 -1
  16. package/dist/browser/prod/index.js +40 -40
  17. package/dist/dev/index.css +85 -50
  18. package/dist/dev/index.css.map +3 -3
  19. package/dist/dev/index.js +8688 -6706
  20. package/dist/dev/index.js.map +4 -4
  21. package/dist/{prod/locales/en-ZXYG7GCR.json → dev/locales/en-V6KXFSCK.json} +8 -1
  22. package/dist/excalidraw/actions/actionAlign.d.ts +7 -6
  23. package/dist/excalidraw/actions/actionAlign.js +14 -14
  24. package/dist/excalidraw/actions/actionClipboard.d.ts +7 -3
  25. package/dist/excalidraw/actions/actionDeleteSelected.d.ts +7 -3
  26. package/dist/excalidraw/actions/actionDeleteSelected.js +103 -34
  27. package/dist/excalidraw/actions/actionDuplicateSelection.js +105 -95
  28. package/dist/excalidraw/actions/actionFlip.js +16 -7
  29. package/dist/excalidraw/actions/actionFrame.d.ts +493 -0
  30. package/dist/excalidraw/actions/actionFrame.js +45 -2
  31. package/dist/excalidraw/actions/actionGroup.js +6 -4
  32. package/dist/excalidraw/actions/actionProperties.js +145 -116
  33. package/dist/excalidraw/actions/actionSelectAll.js +4 -3
  34. package/dist/excalidraw/actions/shortcuts.d.ts +1 -1
  35. package/dist/excalidraw/actions/shortcuts.js +1 -0
  36. package/dist/excalidraw/actions/types.d.ts +1 -1
  37. package/dist/excalidraw/align.d.ts +2 -1
  38. package/dist/excalidraw/align.js +15 -6
  39. package/dist/excalidraw/clipboard.d.ts +27 -5
  40. package/dist/excalidraw/clipboard.js +55 -28
  41. package/dist/excalidraw/components/Actions.d.ts +2 -1
  42. package/dist/excalidraw/components/Actions.js +4 -2
  43. package/dist/excalidraw/components/ActiveConfirmDialog.d.ts +1 -1
  44. package/dist/excalidraw/components/ActiveConfirmDialog.js +2 -3
  45. package/dist/excalidraw/components/App.d.ts +1 -0
  46. package/dist/excalidraw/components/App.js +216 -111
  47. package/dist/excalidraw/components/ColorPicker/ColorInput.js +2 -3
  48. package/dist/excalidraw/components/ColorPicker/ColorPicker.js +2 -3
  49. package/dist/excalidraw/components/ColorPicker/CustomColorList.js +1 -1
  50. package/dist/excalidraw/components/ColorPicker/Picker.js +1 -1
  51. package/dist/excalidraw/components/ColorPicker/PickerColorList.js +1 -1
  52. package/dist/excalidraw/components/ColorPicker/ShadeList.js +1 -1
  53. package/dist/excalidraw/components/ColorPicker/colorPickerUtils.d.ts +1 -1
  54. package/dist/excalidraw/components/ColorPicker/colorPickerUtils.js +1 -1
  55. package/dist/excalidraw/components/CommandPalette/CommandPalette.js +3 -3
  56. package/dist/excalidraw/components/ConfirmDialog.js +17 -5
  57. package/dist/excalidraw/components/Dialog.js +2 -3
  58. package/dist/excalidraw/components/EyeDropper.d.ts +1 -1
  59. package/dist/excalidraw/components/EyeDropper.js +1 -1
  60. package/dist/excalidraw/components/IconPicker.d.ts +2 -2
  61. package/dist/excalidraw/components/IconPicker.js +56 -53
  62. package/dist/excalidraw/components/LayerUI.js +6 -6
  63. package/dist/excalidraw/components/LibraryMenu.d.ts +2 -16
  64. package/dist/excalidraw/components/LibraryMenu.js +70 -28
  65. package/dist/excalidraw/components/LibraryMenuHeaderContent.js +4 -5
  66. package/dist/excalidraw/components/MobileMenu.js +1 -1
  67. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirm.js +2 -3
  68. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.d.ts +1 -1
  69. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.js +2 -3
  70. package/dist/excalidraw/components/Range.d.ts +9 -0
  71. package/dist/excalidraw/components/Range.js +24 -0
  72. package/dist/excalidraw/components/SearchMenu.d.ts +1 -1
  73. package/dist/excalidraw/components/SearchMenu.js +3 -4
  74. package/dist/excalidraw/components/Sidebar/Sidebar.d.ts +1 -1
  75. package/dist/excalidraw/components/Sidebar/Sidebar.js +2 -3
  76. package/dist/excalidraw/components/Stats/Collapsible.d.ts +2 -1
  77. package/dist/excalidraw/components/Stats/Collapsible.js +2 -2
  78. package/dist/excalidraw/components/Stats/Dimension.js +94 -8
  79. package/dist/excalidraw/components/Stats/MultiDimension.js +8 -5
  80. package/dist/excalidraw/components/Stats/Position.js +63 -3
  81. package/dist/excalidraw/components/Stats/index.js +21 -4
  82. package/dist/excalidraw/components/Stats/utils.d.ts +1 -1
  83. package/dist/excalidraw/components/Stats/utils.js +2 -55
  84. package/dist/excalidraw/components/TTDDialog/TTDDialog.js +1 -1
  85. package/dist/excalidraw/components/ToolButton.js +4 -9
  86. package/dist/excalidraw/components/hoc/withInternalFallback.js +3 -3
  87. package/dist/excalidraw/components/hyperlink/Hyperlink.js +6 -12
  88. package/dist/excalidraw/components/icons.d.ts +9 -0
  89. package/dist/excalidraw/components/icons.js +4 -4
  90. package/dist/excalidraw/components/main-menu/DefaultItems.js +2 -3
  91. package/dist/excalidraw/constants.d.ts +5 -1
  92. package/dist/excalidraw/constants.js +9 -1
  93. package/dist/excalidraw/context/tunnels.d.ts +2 -1
  94. package/dist/excalidraw/context/tunnels.js +3 -1
  95. package/dist/excalidraw/data/blob.d.ts +1 -0
  96. package/dist/excalidraw/data/blob.js +7 -3
  97. package/dist/excalidraw/data/filesystem.d.ts +2 -1
  98. package/dist/excalidraw/data/filesystem.js +1 -0
  99. package/dist/excalidraw/data/image.d.ts +0 -6
  100. package/dist/excalidraw/data/image.js +1 -43
  101. package/dist/excalidraw/data/index.js +6 -6
  102. package/dist/excalidraw/data/library.d.ts +9 -3
  103. package/dist/excalidraw/data/library.js +43 -6
  104. package/dist/excalidraw/data/restore.js +26 -8
  105. package/dist/excalidraw/data/url.d.ts +0 -1
  106. package/dist/excalidraw/data/url.js +2 -4
  107. package/dist/excalidraw/editor-jotai.d.ts +56 -0
  108. package/dist/excalidraw/editor-jotai.js +8 -0
  109. package/dist/excalidraw/element/binding.d.ts +9 -6
  110. package/dist/excalidraw/element/binding.js +124 -44
  111. package/dist/excalidraw/element/bounds.js +10 -0
  112. package/dist/excalidraw/element/cropElement.d.ts +5 -0
  113. package/dist/excalidraw/element/cropElement.js +28 -1
  114. package/dist/excalidraw/element/dragElements.js +13 -7
  115. package/dist/excalidraw/element/elbowArrow.d.ts +16 -0
  116. package/dist/excalidraw/element/elbowArrow.js +1268 -0
  117. package/dist/excalidraw/element/embeddable.js +4 -5
  118. package/dist/excalidraw/element/flowchart.d.ts +1 -1
  119. package/dist/excalidraw/element/flowchart.js +25 -9
  120. package/dist/excalidraw/element/heading.d.ts +5 -1
  121. package/dist/excalidraw/element/heading.js +5 -1
  122. package/dist/excalidraw/element/image.js +19 -5
  123. package/dist/excalidraw/element/linearElementEditor.d.ts +9 -10
  124. package/dist/excalidraw/element/linearElementEditor.js +97 -38
  125. package/dist/excalidraw/element/mutateElement.d.ts +3 -1
  126. package/dist/excalidraw/element/mutateElement.js +31 -4
  127. package/dist/excalidraw/element/newElement.d.ts +8 -12
  128. package/dist/excalidraw/element/newElement.js +36 -21
  129. package/dist/excalidraw/element/resizeElements.d.ts +20 -5
  130. package/dist/excalidraw/element/resizeElements.js +593 -361
  131. package/dist/excalidraw/element/sortElements.js +1 -4
  132. package/dist/excalidraw/element/types.d.ts +23 -1
  133. package/dist/excalidraw/fonts/Fonts.d.ts +0 -16
  134. package/dist/excalidraw/fonts/Fonts.js +6 -31
  135. package/dist/excalidraw/frame.d.ts +11 -5
  136. package/dist/excalidraw/frame.js +146 -35
  137. package/dist/excalidraw/groups.js +3 -0
  138. package/dist/excalidraw/hooks/useLibraryItemSvg.d.ts +1 -1
  139. package/dist/excalidraw/hooks/useLibraryItemSvg.js +2 -3
  140. package/dist/excalidraw/hooks/useScrollPosition.js +1 -1
  141. package/dist/excalidraw/i18n.js +3 -4
  142. package/dist/excalidraw/index.js +3 -4
  143. package/dist/excalidraw/locales/en.json +8 -1
  144. package/dist/excalidraw/renderer/interactiveScene.js +43 -32
  145. package/dist/excalidraw/renderer/staticScene.js +6 -4
  146. package/dist/excalidraw/renderer/staticSvgScene.js +1 -1
  147. package/dist/excalidraw/scene/Shape.js +40 -17
  148. package/dist/excalidraw/scene/comparisons.d.ts +0 -477
  149. package/dist/excalidraw/scene/comparisons.js +0 -37
  150. package/dist/excalidraw/scene/export.d.ts +7 -0
  151. package/dist/excalidraw/scene/export.js +107 -43
  152. package/dist/excalidraw/scene/index.d.ts +1 -1
  153. package/dist/excalidraw/scene/index.js +1 -1
  154. package/dist/excalidraw/scene/selection.js +4 -1
  155. package/dist/excalidraw/types.d.ts +15 -0
  156. package/dist/excalidraw/utility-types.d.ts +1 -0
  157. package/dist/excalidraw/utils.d.ts +8 -1
  158. package/dist/excalidraw/utils.js +9 -0
  159. package/dist/excalidraw/visualdebug.d.ts +8 -1
  160. package/dist/excalidraw/visualdebug.js +3 -0
  161. package/dist/math/line.d.ts +19 -0
  162. package/dist/math/line.js +32 -3
  163. package/dist/math/point.d.ts +10 -0
  164. package/dist/math/point.js +12 -1
  165. package/dist/prod/index.css +1 -1
  166. package/dist/prod/index.js +29 -44
  167. package/dist/{dev/locales/en-ZXYG7GCR.json → prod/locales/en-V6KXFSCK.json} +8 -1
  168. package/package.json +5 -2
  169. package/dist/browser/dev/excalidraw-assets-dev/chunk-JGDL4H2X.js.map +0 -7
  170. package/dist/browser/dev/excalidraw-assets-dev/chunk-V7NFEZA6.js.map +0 -7
  171. package/dist/browser/prod/excalidraw-assets/chunk-S2XKB3DE.js +0 -62
  172. package/dist/browser/prod/excalidraw-assets/image-OFI2YYMP.js +0 -1
  173. package/dist/excalidraw/element/routing.d.ts +0 -12
  174. package/dist/excalidraw/element/routing.js +0 -642
  175. package/dist/excalidraw/jotai.d.ts +0 -34
  176. package/dist/excalidraw/jotai.js +0 -18
  177. /package/dist/browser/dev/excalidraw-assets-dev/{en-ZSVWGT55.js.map → en-7IBTMWBG.js.map} +0 -0
  178. /package/dist/browser/dev/excalidraw-assets-dev/{image-RJG3J34Y.js.map → image-N5AC7SEK.js.map} +0 -0
@@ -1,49 +1,72 @@
1
1
  import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
2
2
  import { rescalePoints } from "../points";
3
- import { getElementAbsoluteCoords, getCommonBounds, getResizedElementAbsoluteCoords, getCommonBoundingBox, } from "./bounds";
3
+ import { getElementAbsoluteCoords, getCommonBounds, getResizedElementAbsoluteCoords, getCommonBoundingBox, getElementBounds, } from "./bounds";
4
4
  import { isArrowElement, isBoundToContainer, isElbowArrow, isFrameLikeElement, isFreeDrawElement, isImageElement, isLinearElement, isTextElement, } from "./typeChecks";
5
5
  import { mutateElement } from "./mutateElement";
6
6
  import { getFontString } from "../utils";
7
7
  import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
8
- import Scene from "../scene/Scene";
9
8
  import { getApproxMinLineWidth, getBoundTextElement, getBoundTextElementId, getContainerElement, handleBindTextResize, getBoundTextMaxWidth, getApproxMinLineHeight, measureText, getMinTextElementWidth, } from "./textElement";
10
9
  import { wrapText } from "./textWrapping";
11
10
  import { LinearElementEditor } from "./linearElementEditor";
12
11
  import { isInGroup } from "../groups";
13
- import { mutateElbowArrow } from "./routing";
14
12
  import { pointCenter, normalizeRadians, pointFrom, pointFromPair, pointRotateRads, } from "../../math";
15
13
  // Returns true when transform (resizing/rotation) happened
16
- export const transformElements = (originalElements, transformHandleType, selectedElements, elementsMap, shouldRotateWithDiscreteAngle, shouldResizeFromCenter, shouldMaintainAspectRatio, pointerX, pointerY, centerX, centerY) => {
14
+ export const transformElements = (originalElements, transformHandleType, selectedElements, elementsMap, scene, shouldRotateWithDiscreteAngle, shouldResizeFromCenter, shouldMaintainAspectRatio, pointerX, pointerY, centerX, centerY) => {
17
15
  if (selectedElements.length === 1) {
18
16
  const [element] = selectedElements;
19
17
  if (transformHandleType === "rotation") {
20
18
  if (!isElbowArrow(element)) {
21
- rotateSingleElement(element, elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle);
19
+ rotateSingleElement(element, elementsMap, scene, pointerX, pointerY, shouldRotateWithDiscreteAngle);
22
20
  updateBoundElements(element, elementsMap);
23
21
  }
24
22
  }
25
23
  else if (isTextElement(element) && transformHandleType) {
26
24
  resizeSingleTextElement(originalElements, element, elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, pointerY);
27
25
  updateBoundElements(element, elementsMap);
26
+ return true;
28
27
  }
29
28
  else if (transformHandleType) {
30
- resizeSingleElement(originalElements, shouldMaintainAspectRatio, element, elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, pointerY);
29
+ const elementId = selectedElements[0].id;
30
+ const latestElement = elementsMap.get(elementId);
31
+ const origElement = originalElements.get(elementId);
32
+ if (latestElement && origElement) {
33
+ const { nextWidth, nextHeight } = getNextSingleWidthAndHeightFromPointer(latestElement, origElement, elementsMap, originalElements, transformHandleType, pointerX, pointerY, {
34
+ shouldMaintainAspectRatio,
35
+ shouldResizeFromCenter,
36
+ });
37
+ resizeSingleElement(nextWidth, nextHeight, latestElement, origElement, elementsMap, originalElements, transformHandleType, {
38
+ shouldMaintainAspectRatio,
39
+ shouldResizeFromCenter,
40
+ });
41
+ }
31
42
  }
32
43
  return true;
33
44
  }
34
45
  else if (selectedElements.length > 1) {
35
46
  if (transformHandleType === "rotation") {
36
- rotateMultipleElements(originalElements, selectedElements, elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle, centerX, centerY);
47
+ rotateMultipleElements(originalElements, selectedElements, elementsMap, scene, pointerX, pointerY, shouldRotateWithDiscreteAngle, centerX, centerY);
37
48
  return true;
38
49
  }
39
50
  else if (transformHandleType) {
40
- resizeMultipleElements(originalElements, selectedElements, elementsMap, transformHandleType, shouldResizeFromCenter, shouldMaintainAspectRatio, pointerX, pointerY);
51
+ const { nextWidth, nextHeight, flipByX, flipByY, originalBoundingBox } = getNextMultipleWidthAndHeightFromPointer(selectedElements, originalElements, elementsMap, transformHandleType, pointerX, pointerY, {
52
+ shouldMaintainAspectRatio,
53
+ shouldResizeFromCenter,
54
+ });
55
+ resizeMultipleElements(selectedElements, elementsMap, transformHandleType, scene, originalElements, {
56
+ shouldResizeFromCenter,
57
+ shouldMaintainAspectRatio,
58
+ flipByX,
59
+ flipByY,
60
+ nextWidth,
61
+ nextHeight,
62
+ originalBoundingBox,
63
+ });
41
64
  return true;
42
65
  }
43
66
  }
44
67
  return false;
45
68
  };
46
- const rotateSingleElement = (element, elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle) => {
69
+ const rotateSingleElement = (element, elementsMap, scene, pointerX, pointerY, shouldRotateWithDiscreteAngle) => {
47
70
  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
48
71
  const cx = (x1 + x2) / 2;
49
72
  const cy = (y1 + y2) / 2;
@@ -63,7 +86,7 @@ const rotateSingleElement = (element, elementsMap, pointerX, pointerY, shouldRot
63
86
  const boundTextElementId = getBoundTextElementId(element);
64
87
  mutateElement(element, { angle });
65
88
  if (boundTextElementId) {
66
- const textElement = Scene.getScene(element)?.getElement(boundTextElementId);
89
+ const textElement = scene.getElement(boundTextElementId);
67
90
  if (textElement && !isArrowElement(element)) {
68
91
  mutateElement(textElement, { angle });
69
92
  }
@@ -211,67 +234,203 @@ const resizeSingleTextElement = (originalElements, element, elementsMap, transfo
211
234
  mutateElement(element, resizedElement);
212
235
  }
213
236
  };
214
- export const resizeSingleElement = (originalElements, shouldMaintainAspectRatio, element, elementsMap, transformHandleDirection, shouldResizeFromCenter, pointerX, pointerY) => {
215
- const stateAtResizeStart = originalElements.get(element.id);
216
- // Gets bounds corners
217
- const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(stateAtResizeStart, stateAtResizeStart.width, stateAtResizeStart.height, true);
218
- const startTopLeft = pointFrom(x1, y1);
219
- const startBottomRight = pointFrom(x2, y2);
220
- const startCenter = pointCenter(startTopLeft, startBottomRight);
221
- // Calculate new dimensions based on cursor position
222
- const rotatedPointer = pointRotateRads(pointFrom(pointerX, pointerY), startCenter, -stateAtResizeStart.angle);
223
- // Get bounds corners rendered on screen
224
- const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(element, element.width, element.height, true);
225
- const boundsCurrentWidth = esx2 - esx1;
226
- const boundsCurrentHeight = esy2 - esy1;
227
- // It's important we set the initial scale value based on the width and height at resize start,
228
- // otherwise previous dimensions affected by modifiers will be taken into account.
229
- const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
230
- const atStartBoundsHeight = startBottomRight[1] - startTopLeft[1];
231
- let scaleX = atStartBoundsWidth / boundsCurrentWidth;
232
- let scaleY = atStartBoundsHeight / boundsCurrentHeight;
233
- let boundTextFont = {};
234
- const boundTextElement = getBoundTextElement(element, elementsMap);
235
- if (transformHandleDirection.includes("e")) {
236
- scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
237
- }
238
- if (transformHandleDirection.includes("s")) {
239
- scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight;
237
+ const rotateMultipleElements = (originalElements, elements, elementsMap, scene, pointerX, pointerY, shouldRotateWithDiscreteAngle, centerX, centerY) => {
238
+ let centerAngle = (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
239
+ if (shouldRotateWithDiscreteAngle) {
240
+ centerAngle += SHIFT_LOCKING_ANGLE / 2;
241
+ centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
240
242
  }
241
- if (transformHandleDirection.includes("w")) {
242
- scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
243
+ for (const element of elements) {
244
+ if (!isFrameLikeElement(element)) {
245
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
246
+ const cx = (x1 + x2) / 2;
247
+ const cy = (y1 + y2) / 2;
248
+ const origAngle = originalElements.get(element.id)?.angle ?? element.angle;
249
+ const [rotatedCX, rotatedCY] = pointRotateRads(pointFrom(cx, cy), pointFrom(centerX, centerY), (centerAngle + origAngle - element.angle));
250
+ if (isElbowArrow(element)) {
251
+ // Needed to re-route the arrow
252
+ mutateElement(element, {
253
+ points: getArrowLocalFixedPoints(element, elementsMap),
254
+ });
255
+ }
256
+ else {
257
+ mutateElement(element, {
258
+ x: element.x + (rotatedCX - cx),
259
+ y: element.y + (rotatedCY - cy),
260
+ angle: normalizeRadians((centerAngle + origAngle)),
261
+ }, false);
262
+ }
263
+ updateBoundElements(element, elementsMap, {
264
+ simultaneouslyUpdated: elements,
265
+ });
266
+ const boundText = getBoundTextElement(element, elementsMap);
267
+ if (boundText && !isArrowElement(element)) {
268
+ mutateElement(boundText, {
269
+ x: boundText.x + (rotatedCX - cx),
270
+ y: boundText.y + (rotatedCY - cy),
271
+ angle: normalizeRadians((centerAngle + origAngle)),
272
+ }, false);
273
+ }
274
+ }
243
275
  }
244
- if (transformHandleDirection.includes("n")) {
245
- scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
276
+ scene.triggerUpdate();
277
+ };
278
+ export const getResizeOffsetXY = (transformHandleType, selectedElements, elementsMap, x, y) => {
279
+ const [x1, y1, x2, y2] = selectedElements.length === 1
280
+ ? getElementAbsoluteCoords(selectedElements[0], elementsMap)
281
+ : getCommonBounds(selectedElements);
282
+ const cx = (x1 + x2) / 2;
283
+ const cy = (y1 + y2) / 2;
284
+ const angle = (selectedElements.length === 1 ? selectedElements[0].angle : 0);
285
+ [x, y] = pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), -angle);
286
+ switch (transformHandleType) {
287
+ case "n":
288
+ return pointRotateRads(pointFrom(x - (x1 + x2) / 2, y - y1), pointFrom(0, 0), angle);
289
+ case "s":
290
+ return pointRotateRads(pointFrom(x - (x1 + x2) / 2, y - y2), pointFrom(0, 0), angle);
291
+ case "w":
292
+ return pointRotateRads(pointFrom(x - x1, y - (y1 + y2) / 2), pointFrom(0, 0), angle);
293
+ case "e":
294
+ return pointRotateRads(pointFrom(x - x2, y - (y1 + y2) / 2), pointFrom(0, 0), angle);
295
+ case "nw":
296
+ return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle);
297
+ case "ne":
298
+ return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle);
299
+ case "sw":
300
+ return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle);
301
+ case "se":
302
+ return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle);
303
+ default:
304
+ return [0, 0];
246
305
  }
247
- // Linear elements dimensions differ from bounds dimensions
248
- const eleInitialWidth = stateAtResizeStart.width;
249
- const eleInitialHeight = stateAtResizeStart.height;
250
- // We have to use dimensions of element on screen, otherwise the scaling of the
251
- // dimensions won't match the cursor for linear elements.
252
- let eleNewWidth = element.width * scaleX;
253
- let eleNewHeight = element.height * scaleY;
254
- // adjust dimensions for resizing from center
306
+ };
307
+ export const getResizeArrowDirection = (transformHandleType, element) => {
308
+ const [, [px, py]] = element.points;
309
+ const isResizeEnd = (transformHandleType === "nw" && (px < 0 || py < 0)) ||
310
+ (transformHandleType === "ne" && px >= 0) ||
311
+ (transformHandleType === "sw" && px <= 0) ||
312
+ (transformHandleType === "se" && (px > 0 || py > 0));
313
+ return isResizeEnd ? "end" : "origin";
314
+ };
315
+ const getResizeAnchor = (handleDirection, shouldMaintainAspectRatio, shouldResizeFromCenter) => {
255
316
  if (shouldResizeFromCenter) {
256
- eleNewWidth = 2 * eleNewWidth - eleInitialWidth;
257
- eleNewHeight = 2 * eleNewHeight - eleInitialHeight;
317
+ return "center";
258
318
  }
259
- // adjust dimensions to keep sides ratio
260
319
  if (shouldMaintainAspectRatio) {
261
- const widthRatio = Math.abs(eleNewWidth) / eleInitialWidth;
262
- const heightRatio = Math.abs(eleNewHeight) / eleInitialHeight;
263
- if (transformHandleDirection.length === 1) {
264
- eleNewHeight *= widthRatio;
265
- eleNewWidth *= heightRatio;
266
- }
267
- if (transformHandleDirection.length === 2) {
268
- const ratio = Math.max(widthRatio, heightRatio);
269
- eleNewWidth = eleInitialWidth * ratio * Math.sign(eleNewWidth);
270
- eleNewHeight = eleInitialHeight * ratio * Math.sign(eleNewHeight);
271
- }
320
+ switch (handleDirection) {
321
+ case "n":
322
+ return "south-side";
323
+ case "e": {
324
+ return "west-side";
325
+ }
326
+ case "s":
327
+ return "north-side";
328
+ case "w":
329
+ return "east-side";
330
+ case "ne":
331
+ return "bottom-left";
332
+ case "nw":
333
+ return "bottom-right";
334
+ case "se":
335
+ return "top-left";
336
+ case "sw":
337
+ return "top-right";
338
+ }
339
+ }
340
+ if (["e", "se", "s"].includes(handleDirection)) {
341
+ return "top-left";
342
+ }
343
+ else if (["n", "nw", "w"].includes(handleDirection)) {
344
+ return "bottom-right";
345
+ }
346
+ else if (handleDirection === "ne") {
347
+ return "bottom-left";
348
+ }
349
+ return "top-right";
350
+ };
351
+ const getResizedOrigin = (prevOrigin, prevWidth, prevHeight, newWidth, newHeight, angle, handleDirection, shouldMaintainAspectRatio, shouldResizeFromCenter) => {
352
+ const anchor = getResizeAnchor(handleDirection, shouldMaintainAspectRatio, shouldResizeFromCenter);
353
+ const [x, y] = prevOrigin;
354
+ switch (anchor) {
355
+ case "top-left":
356
+ return {
357
+ x: x +
358
+ (prevWidth - newWidth) / 2 +
359
+ ((newWidth - prevWidth) / 2) * Math.cos(angle) +
360
+ ((prevHeight - newHeight) / 2) * Math.sin(angle),
361
+ y: y +
362
+ (prevHeight - newHeight) / 2 +
363
+ ((newWidth - prevWidth) / 2) * Math.sin(angle) +
364
+ ((newHeight - prevHeight) / 2) * Math.cos(angle),
365
+ };
366
+ case "top-right":
367
+ return {
368
+ x: x +
369
+ ((prevWidth - newWidth) / 2) * (Math.cos(angle) + 1) +
370
+ ((prevHeight - newHeight) / 2) * Math.sin(angle),
371
+ y: y +
372
+ (prevHeight - newHeight) / 2 +
373
+ ((prevWidth - newWidth) / 2) * Math.sin(angle) +
374
+ ((newHeight - prevHeight) / 2) * Math.cos(angle),
375
+ };
376
+ case "bottom-left":
377
+ return {
378
+ x: x +
379
+ ((prevWidth - newWidth) / 2) * (1 - Math.cos(angle)) +
380
+ ((newHeight - prevHeight) / 2) * Math.sin(angle),
381
+ y: y +
382
+ ((prevHeight - newHeight) / 2) * (Math.cos(angle) + 1) +
383
+ ((newWidth - prevWidth) / 2) * Math.sin(angle),
384
+ };
385
+ case "bottom-right":
386
+ return {
387
+ x: x +
388
+ ((prevWidth - newWidth) / 2) * (Math.cos(angle) + 1) +
389
+ ((newHeight - prevHeight) / 2) * Math.sin(angle),
390
+ y: y +
391
+ ((prevHeight - newHeight) / 2) * (Math.cos(angle) + 1) +
392
+ ((prevWidth - newWidth) / 2) * Math.sin(angle),
393
+ };
394
+ case "center":
395
+ return {
396
+ x: x - (newWidth - prevWidth) / 2,
397
+ y: y - (newHeight - prevHeight) / 2,
398
+ };
399
+ case "east-side":
400
+ return {
401
+ x: x + ((prevWidth - newWidth) / 2) * (Math.cos(angle) + 1),
402
+ y: y +
403
+ ((prevWidth - newWidth) / 2) * Math.sin(angle) +
404
+ (prevHeight - newHeight) / 2,
405
+ };
406
+ case "west-side":
407
+ return {
408
+ x: x + ((prevWidth - newWidth) / 2) * (1 - Math.cos(angle)),
409
+ y: y +
410
+ ((newWidth - prevWidth) / 2) * Math.sin(angle) +
411
+ (prevHeight - newHeight) / 2,
412
+ };
413
+ case "north-side":
414
+ return {
415
+ x: x +
416
+ (prevWidth - newWidth) / 2 +
417
+ ((prevHeight - newHeight) / 2) * Math.sin(angle),
418
+ y: y + ((newHeight - prevHeight) / 2) * (Math.cos(angle) - 1),
419
+ };
420
+ case "south-side":
421
+ return {
422
+ x: x +
423
+ (prevWidth - newWidth) / 2 +
424
+ ((newHeight - prevHeight) / 2) * Math.sin(angle),
425
+ y: y + ((prevHeight - newHeight) / 2) * (Math.cos(angle) + 1),
426
+ };
272
427
  }
428
+ };
429
+ export const resizeSingleElement = (nextWidth, nextHeight, latestElement, origElement, elementsMap, originalElementsMap, handleDirection, { shouldInformMutation = true, shouldMaintainAspectRatio = false, shouldResizeFromCenter = false, } = {}) => {
430
+ let boundTextFont = {};
431
+ const boundTextElement = getBoundTextElement(latestElement, elementsMap);
273
432
  if (boundTextElement) {
274
- const stateOfBoundTextElementAtResize = originalElements.get(boundTextElement.id);
433
+ const stateOfBoundTextElementAtResize = originalElementsMap.get(boundTextElement.id);
275
434
  if (stateOfBoundTextElementAtResize) {
276
435
  boundTextFont = {
277
436
  fontSize: stateOfBoundTextElementAtResize.fontSize,
@@ -279,9 +438,9 @@ export const resizeSingleElement = (originalElements, shouldMaintainAspectRatio,
279
438
  }
280
439
  if (shouldMaintainAspectRatio) {
281
440
  const updatedElement = {
282
- ...element,
283
- width: eleNewWidth,
284
- height: eleNewHeight,
441
+ ...latestElement,
442
+ width: nextWidth,
443
+ height: nextHeight,
285
444
  };
286
445
  const nextFont = measureFontSizeFromWidth(boundTextElement, elementsMap, getBoundTextMaxWidth(updatedElement, boundTextElement));
287
446
  if (nextFont === null) {
@@ -294,147 +453,140 @@ export const resizeSingleElement = (originalElements, shouldMaintainAspectRatio,
294
453
  else {
295
454
  const minWidth = getApproxMinLineWidth(getFontString(boundTextElement), boundTextElement.lineHeight);
296
455
  const minHeight = getApproxMinLineHeight(boundTextElement.fontSize, boundTextElement.lineHeight);
297
- eleNewWidth = Math.max(eleNewWidth, minWidth);
298
- eleNewHeight = Math.max(eleNewHeight, minHeight);
299
- }
300
- }
301
- const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] = getResizedElementAbsoluteCoords(stateAtResizeStart, eleNewWidth, eleNewHeight, true);
302
- const newBoundsWidth = newBoundsX2 - newBoundsX1;
303
- const newBoundsHeight = newBoundsY2 - newBoundsY1;
304
- // Calculate new topLeft based on fixed corner during resize
305
- let newTopLeft = [...startTopLeft];
306
- if (["n", "w", "nw"].includes(transformHandleDirection)) {
307
- newTopLeft = [
308
- startBottomRight[0] - Math.abs(newBoundsWidth),
309
- startBottomRight[1] - Math.abs(newBoundsHeight),
310
- ];
311
- }
312
- if (transformHandleDirection === "ne") {
313
- const bottomLeft = [startTopLeft[0], startBottomRight[1]];
314
- newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
315
- }
316
- if (transformHandleDirection === "sw") {
317
- const topRight = [startBottomRight[0], startTopLeft[1]];
318
- newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
319
- }
320
- // Keeps opposite handle fixed during resize
321
- if (shouldMaintainAspectRatio) {
322
- if (["s", "n"].includes(transformHandleDirection)) {
323
- newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
324
- }
325
- if (["e", "w"].includes(transformHandleDirection)) {
326
- newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
327
- }
328
- }
329
- const flipX = eleNewWidth < 0;
330
- const flipY = eleNewHeight < 0;
331
- // Flip horizontally
332
- if (flipX) {
333
- if (transformHandleDirection.includes("e")) {
334
- newTopLeft[0] -= Math.abs(newBoundsWidth);
335
- }
336
- if (transformHandleDirection.includes("w")) {
337
- newTopLeft[0] += Math.abs(newBoundsWidth);
338
- }
339
- }
340
- // Flip vertically
341
- if (flipY) {
342
- if (transformHandleDirection.includes("s")) {
343
- newTopLeft[1] -= Math.abs(newBoundsHeight);
344
- }
345
- if (transformHandleDirection.includes("n")) {
346
- newTopLeft[1] += Math.abs(newBoundsHeight);
347
- }
348
- }
349
- if (shouldResizeFromCenter) {
350
- newTopLeft[0] = startCenter[0] - Math.abs(newBoundsWidth) / 2;
351
- newTopLeft[1] = startCenter[1] - Math.abs(newBoundsHeight) / 2;
352
- }
353
- // adjust topLeft to new rotation point
354
- const angle = stateAtResizeStart.angle;
355
- const rotatedTopLeft = pointRotateRads(pointFromPair(newTopLeft), startCenter, angle);
356
- const newCenter = pointFrom(newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2);
357
- const rotatedNewCenter = pointRotateRads(newCenter, startCenter, angle);
358
- newTopLeft = pointRotateRads(rotatedTopLeft, rotatedNewCenter, -angle);
359
- // For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
360
- // So we need to readjust (x,y) to be where the first point should be
361
- const newOrigin = [...newTopLeft];
362
- const linearElementXOffset = stateAtResizeStart.x - newBoundsX1;
363
- const linearElementYOffset = stateAtResizeStart.y - newBoundsY1;
364
- newOrigin[0] += linearElementXOffset;
365
- newOrigin[1] += linearElementYOffset;
366
- const nextX = newOrigin[0];
367
- const nextY = newOrigin[1];
368
- // Readjust points for linear elements
369
- let rescaledElementPointsY;
370
- let rescaledPoints;
371
- if (isLinearElement(element) || isFreeDrawElement(element)) {
372
- rescaledElementPointsY = rescalePoints(1, eleNewHeight, stateAtResizeStart.points, true);
373
- rescaledPoints = rescalePoints(0, eleNewWidth, rescaledElementPointsY, true);
374
- }
375
- const resizedElement = {
376
- width: Math.abs(eleNewWidth),
377
- height: Math.abs(eleNewHeight),
378
- x: nextX,
379
- y: nextY,
380
- points: rescaledPoints,
381
- };
382
- if ("scale" in element && "scale" in stateAtResizeStart) {
383
- mutateElement(element, {
456
+ nextWidth = Math.max(nextWidth, minWidth);
457
+ nextHeight = Math.max(nextHeight, minHeight);
458
+ }
459
+ }
460
+ const rescaledPoints = rescalePointsInElement(origElement, nextWidth, nextHeight, true);
461
+ let previousOrigin = pointFrom(origElement.x, origElement.y);
462
+ if (isLinearElement(origElement)) {
463
+ const [x1, y1] = getElementBounds(origElement, originalElementsMap);
464
+ previousOrigin = pointFrom(x1, y1);
465
+ }
466
+ const newOrigin = getResizedOrigin(previousOrigin, origElement.width, origElement.height, nextWidth, nextHeight, origElement.angle, handleDirection, shouldMaintainAspectRatio, shouldResizeFromCenter);
467
+ if (isLinearElement(origElement) && rescaledPoints.points) {
468
+ const offsetX = origElement.x - previousOrigin[0];
469
+ const offsetY = origElement.y - previousOrigin[1];
470
+ newOrigin.x += offsetX;
471
+ newOrigin.y += offsetY;
472
+ const scaledX = rescaledPoints.points[0][0];
473
+ const scaledY = rescaledPoints.points[0][1];
474
+ newOrigin.x += scaledX;
475
+ newOrigin.y += scaledY;
476
+ rescaledPoints.points = rescaledPoints.points.map((p) => pointFrom(p[0] - scaledX, p[1] - scaledY));
477
+ }
478
+ // flipping
479
+ if (nextWidth < 0) {
480
+ newOrigin.x = newOrigin.x + nextWidth;
481
+ }
482
+ if (nextHeight < 0) {
483
+ newOrigin.y = newOrigin.y + nextHeight;
484
+ }
485
+ if ("scale" in latestElement && "scale" in origElement) {
486
+ mutateElement(latestElement, {
384
487
  scale: [
385
488
  // defaulting because scaleX/Y can be 0/-0
386
- (Math.sign(newBoundsX2 - stateAtResizeStart.x) ||
387
- stateAtResizeStart.scale[0]) * stateAtResizeStart.scale[0],
388
- (Math.sign(newBoundsY2 - stateAtResizeStart.y) ||
389
- stateAtResizeStart.scale[1]) * stateAtResizeStart.scale[1],
489
+ (Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
490
+ (Math.sign(nextHeight) || origElement.scale[1]) * origElement.scale[1],
390
491
  ],
391
492
  });
392
493
  }
393
- if (isArrowElement(element) &&
494
+ if (isArrowElement(latestElement) &&
394
495
  boundTextElement &&
395
496
  shouldMaintainAspectRatio) {
396
- const fontSize = (resizedElement.width / element.width) * boundTextElement.fontSize;
497
+ const fontSize = (nextWidth / latestElement.width) * boundTextElement.fontSize;
397
498
  if (fontSize < MIN_FONT_SIZE) {
398
499
  return;
399
500
  }
400
501
  boundTextFont.fontSize = fontSize;
401
502
  }
402
- if (resizedElement.width !== 0 &&
403
- resizedElement.height !== 0 &&
404
- Number.isFinite(resizedElement.x) &&
405
- Number.isFinite(resizedElement.y)) {
406
- mutateElement(element, resizedElement);
407
- updateBoundElements(element, elementsMap, {
408
- newSize: {
409
- width: resizedElement.width,
410
- height: resizedElement.height,
411
- },
503
+ if (nextWidth !== 0 &&
504
+ nextHeight !== 0 &&
505
+ Number.isFinite(newOrigin.x) &&
506
+ Number.isFinite(newOrigin.y)) {
507
+ const updates = {
508
+ ...newOrigin,
509
+ width: Math.abs(nextWidth),
510
+ height: Math.abs(nextHeight),
511
+ ...rescaledPoints,
512
+ };
513
+ mutateElement(latestElement, updates, shouldInformMutation);
514
+ updateBoundElements(latestElement, elementsMap, {
515
+ // TODO: confirm with MARK if this actually makes sense
516
+ newSize: { width: nextWidth, height: nextHeight },
412
517
  });
413
518
  if (boundTextElement && boundTextFont != null) {
414
519
  mutateElement(boundTextElement, {
415
520
  fontSize: boundTextFont.fontSize,
416
521
  });
417
522
  }
418
- handleBindTextResize(element, elementsMap, transformHandleDirection, shouldMaintainAspectRatio);
523
+ handleBindTextResize(latestElement, elementsMap, handleDirection, shouldMaintainAspectRatio);
419
524
  }
420
525
  };
421
- export const resizeMultipleElements = (originalElements, selectedElements, elementsMap, transformHandleType, shouldResizeFromCenter, shouldMaintainAspectRatio, pointerX, pointerY) => {
422
- // map selected elements to the original elements. While it never should
423
- // happen that pointerDownState.originalElements won't contain the selected
424
- // elements during resize, this coupling isn't guaranteed, so to ensure
425
- // type safety we need to transform only those elements we filter.
426
- const targetElements = selectedElements.reduce((acc, element) => {
427
- const origElement = originalElements.get(element.id);
428
- if (origElement) {
429
- acc.push({ orig: origElement, latest: element });
526
+ const getNextSingleWidthAndHeightFromPointer = (latestElement, origElement, elementsMap, originalElementsMap, handleDirection, pointerX, pointerY, { shouldMaintainAspectRatio = false, shouldResizeFromCenter = false, } = {}) => {
527
+ // Gets bounds corners
528
+ const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(origElement, origElement.width, origElement.height, true);
529
+ const startTopLeft = pointFrom(x1, y1);
530
+ const startBottomRight = pointFrom(x2, y2);
531
+ const startCenter = pointCenter(startTopLeft, startBottomRight);
532
+ // Calculate new dimensions based on cursor position
533
+ const rotatedPointer = pointRotateRads(pointFrom(pointerX, pointerY), startCenter, -origElement.angle);
534
+ // Get bounds corners rendered on screen
535
+ const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(latestElement, latestElement.width, latestElement.height, true);
536
+ const boundsCurrentWidth = esx2 - esx1;
537
+ const boundsCurrentHeight = esy2 - esy1;
538
+ // It's important we set the initial scale value based on the width and height at resize start,
539
+ // otherwise previous dimensions affected by modifiers will be taken into account.
540
+ const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
541
+ const atStartBoundsHeight = startBottomRight[1] - startTopLeft[1];
542
+ let scaleX = atStartBoundsWidth / boundsCurrentWidth;
543
+ let scaleY = atStartBoundsHeight / boundsCurrentHeight;
544
+ if (handleDirection.includes("e")) {
545
+ scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
546
+ }
547
+ if (handleDirection.includes("s")) {
548
+ scaleY = (rotatedPointer[1] - startTopLeft[1]) / boundsCurrentHeight;
549
+ }
550
+ if (handleDirection.includes("w")) {
551
+ scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
552
+ }
553
+ if (handleDirection.includes("n")) {
554
+ scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
555
+ }
556
+ // We have to use dimensions of element on screen, otherwise the scaling of the
557
+ // dimensions won't match the cursor for linear elements.
558
+ let nextWidth = latestElement.width * scaleX;
559
+ let nextHeight = latestElement.height * scaleY;
560
+ if (shouldResizeFromCenter) {
561
+ nextWidth = 2 * nextWidth - origElement.width;
562
+ nextHeight = 2 * nextHeight - origElement.height;
563
+ }
564
+ // adjust dimensions to keep sides ratio
565
+ if (shouldMaintainAspectRatio) {
566
+ const widthRatio = Math.abs(nextWidth) / origElement.width;
567
+ const heightRatio = Math.abs(nextHeight) / origElement.height;
568
+ if (handleDirection.length === 1) {
569
+ nextHeight *= widthRatio;
570
+ nextWidth *= heightRatio;
430
571
  }
431
- return acc;
432
- }, []);
572
+ if (handleDirection.length === 2) {
573
+ const ratio = Math.max(widthRatio, heightRatio);
574
+ nextWidth = origElement.width * ratio * Math.sign(nextWidth);
575
+ nextHeight = origElement.height * ratio * Math.sign(nextHeight);
576
+ }
577
+ }
578
+ return {
579
+ nextWidth,
580
+ nextHeight,
581
+ };
582
+ };
583
+ const getNextMultipleWidthAndHeightFromPointer = (selectedElements, originalElementsMap, elementsMap, handleDirection, pointerX, pointerY, { shouldMaintainAspectRatio = false, shouldResizeFromCenter = false, } = {}) => {
584
+ const originalElementsArray = selectedElements.map((el) => originalElementsMap.get(el.id));
433
585
  // getCommonBoundingBox() uses getBoundTextElement() which returns null for
434
586
  // original elements from pointerDownState, so we have to find and add these
435
587
  // bound text elements manually. Additionally, the coordinates of bound text
436
588
  // elements aren't always up to date.
437
- const boundTextElements = targetElements.reduce((acc, { orig }) => {
589
+ const boundTextElements = originalElementsArray.reduce((acc, orig) => {
438
590
  if (!isLinearElement(orig)) {
439
591
  return acc;
440
592
  }
@@ -442,50 +594,48 @@ export const resizeMultipleElements = (originalElements, selectedElements, eleme
442
594
  if (!textId) {
443
595
  return acc;
444
596
  }
445
- const text = originalElements.get(textId) ?? null;
597
+ const text = originalElementsMap.get(textId) ?? null;
446
598
  if (!isBoundToContainer(text)) {
447
599
  return acc;
448
600
  }
449
- const xy = LinearElementEditor.getBoundTextElementPosition(orig, text, elementsMap);
450
- return [...acc, { ...text, ...xy }];
601
+ return [
602
+ ...acc,
603
+ {
604
+ ...text,
605
+ ...LinearElementEditor.getBoundTextElementPosition(orig, text, elementsMap),
606
+ },
607
+ ];
451
608
  }, []);
452
- const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(targetElements.map(({ orig }) => orig).concat(boundTextElements));
609
+ const originalBoundingBox = getCommonBoundingBox(originalElementsArray.map((orig) => orig).concat(boundTextElements));
610
+ const { minX, minY, maxX, maxY, midX, midY } = originalBoundingBox;
453
611
  const width = maxX - minX;
454
612
  const height = maxY - minY;
455
- const direction = transformHandleType;
456
613
  const anchorsMap = {
457
- ne: pointFrom(minX, maxY),
458
- se: pointFrom(minX, minY),
459
- sw: pointFrom(maxX, minY),
460
- nw: pointFrom(maxX, maxY),
461
- e: pointFrom(minX, minY + height / 2),
462
- w: pointFrom(maxX, minY + height / 2),
463
- n: pointFrom(minX + width / 2, maxY),
464
- s: pointFrom(minX + width / 2, minY),
614
+ ne: [minX, maxY],
615
+ se: [minX, minY],
616
+ sw: [maxX, minY],
617
+ nw: [maxX, maxY],
618
+ e: [minX, minY + height / 2],
619
+ w: [maxX, minY + height / 2],
620
+ n: [minX + width / 2, maxY],
621
+ s: [minX + width / 2, minY],
465
622
  };
466
623
  // anchor point must be on the opposite side of the dragged selection handle
467
624
  // or be the center of the selection if shouldResizeFromCenter
468
625
  const [anchorX, anchorY] = shouldResizeFromCenter
469
- ? pointFrom(midX, midY)
470
- : anchorsMap[direction];
626
+ ? [midX, midY]
627
+ : anchorsMap[handleDirection];
471
628
  const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
472
629
  const scale = Math.max(Math.abs(pointerX - anchorX) / width || 0, Math.abs(pointerY - anchorY) / height || 0) * resizeFromCenterScale;
473
- if (scale === 0) {
474
- return;
475
- }
476
- let scaleX = direction.includes("e") || direction.includes("w")
477
- ? (Math.abs(pointerX - anchorX) / width) * resizeFromCenterScale
478
- : 1;
479
- let scaleY = direction.includes("n") || direction.includes("s")
480
- ? (Math.abs(pointerY - anchorY) / height) * resizeFromCenterScale
481
- : 1;
482
- const keepAspectRatio = shouldMaintainAspectRatio ||
483
- targetElements.some((item) => item.latest.angle !== 0 ||
484
- isTextElement(item.latest) ||
485
- isInGroup(item.latest));
486
- if (keepAspectRatio) {
487
- scaleX = scale;
488
- scaleY = scale;
630
+ let nextWidth = handleDirection.includes("e") || handleDirection.includes("w")
631
+ ? Math.abs(pointerX - anchorX) * resizeFromCenterScale
632
+ : width;
633
+ let nextHeight = handleDirection.includes("n") || handleDirection.includes("s")
634
+ ? Math.abs(pointerY - anchorY) * resizeFromCenterScale
635
+ : height;
636
+ if (shouldMaintainAspectRatio) {
637
+ nextWidth = width * scale * Math.sign(pointerX - anchorX);
638
+ nextHeight = height * scale * Math.sign(pointerY - anchorY);
489
639
  }
490
640
  const flipConditionsMap = {
491
641
  ne: [pointerX < anchorX, pointerY > anchorY],
@@ -499,162 +649,244 @@ export const resizeMultipleElements = (originalElements, selectedElements, eleme
499
649
  n: [false, pointerY > anchorY],
500
650
  s: [false, pointerY < anchorY],
501
651
  };
502
- /**
503
- * to flip an element:
504
- * 1. determine over which axis is the element being flipped
505
- * (could be x, y, or both) indicated by `flipFactorX` & `flipFactorY`
506
- * 2. shift element's position by the amount of width or height (or both) or
507
- * mirror points in the case of linear & freedraw elemenets
508
- * 3. adjust element angle
509
- */
510
- const [flipFactorX, flipFactorY] = flipConditionsMap[direction].map((condition) => (condition ? -1 : 1));
511
- const isFlippedByX = flipFactorX < 0;
512
- const isFlippedByY = flipFactorY < 0;
513
- const elementsAndUpdates = [];
514
- for (const { orig, latest } of targetElements) {
515
- // bounded text elements are updated along with their container elements
516
- if (isTextElement(orig) && isBoundToContainer(orig)) {
517
- continue;
518
- }
519
- const width = orig.width * scaleX;
520
- const height = orig.height * scaleY;
521
- const angle = normalizeRadians((orig.angle * flipFactorX * flipFactorY));
522
- const isLinearOrFreeDraw = isLinearElement(orig) || isFreeDrawElement(orig);
523
- const offsetX = orig.x - anchorX;
524
- const offsetY = orig.y - anchorY;
525
- const shiftX = isFlippedByX && !isLinearOrFreeDraw ? width : 0;
526
- const shiftY = isFlippedByY && !isLinearOrFreeDraw ? height : 0;
527
- const x = anchorX + flipFactorX * (offsetX * scaleX + shiftX);
528
- const y = anchorY + flipFactorY * (offsetY * scaleY + shiftY);
529
- const rescaledPoints = rescalePointsInElement(orig, width * flipFactorX, height * flipFactorY, false);
530
- const update = {
531
- x,
532
- y,
533
- width,
534
- height,
535
- angle,
536
- ...rescaledPoints,
537
- };
538
- if (isImageElement(orig)) {
539
- update.scale = [orig.scale[0] * flipFactorX, orig.scale[1] * flipFactorY];
652
+ const [flipByX, flipByY] = flipConditionsMap[handleDirection].map((condition) => condition);
653
+ return {
654
+ originalBoundingBox,
655
+ nextWidth,
656
+ nextHeight,
657
+ flipByX,
658
+ flipByY,
659
+ };
660
+ };
661
+ export const resizeMultipleElements = (selectedElements, elementsMap, handleDirection, scene, originalElementsMap, { shouldMaintainAspectRatio = false, shouldResizeFromCenter = false, flipByX = false, flipByY = false, nextHeight, nextWidth, originalBoundingBox, } = {}) => {
662
+ // in the case of just flipping, there is no need to specify the next width and height
663
+ if (nextWidth === undefined &&
664
+ nextHeight === undefined &&
665
+ flipByX === undefined &&
666
+ flipByY === undefined) {
667
+ return;
668
+ }
669
+ // do not allow next width or height to be 0
670
+ if (nextHeight === 0 || nextWidth === 0) {
671
+ return;
672
+ }
673
+ if (!originalElementsMap) {
674
+ originalElementsMap = elementsMap;
675
+ }
676
+ const targetElements = selectedElements.reduce((acc, element) => {
677
+ const origElement = originalElementsMap.get(element.id);
678
+ if (origElement) {
679
+ acc.push({ orig: origElement, latest: element });
540
680
  }
541
- if (isTextElement(orig)) {
542
- const metrics = measureFontSizeFromWidth(orig, elementsMap, width);
543
- if (!metrics) {
544
- return;
681
+ return acc;
682
+ }, []);
683
+ let boundingBox;
684
+ if (originalBoundingBox) {
685
+ boundingBox = originalBoundingBox;
686
+ }
687
+ else {
688
+ const boundTextElements = targetElements.reduce((acc, { orig }) => {
689
+ if (!isLinearElement(orig)) {
690
+ return acc;
691
+ }
692
+ const textId = getBoundTextElementId(orig);
693
+ if (!textId) {
694
+ return acc;
695
+ }
696
+ const text = originalElementsMap.get(textId) ?? null;
697
+ if (!isBoundToContainer(text)) {
698
+ return acc;
545
699
  }
546
- update.fontSize = metrics.size;
700
+ return [
701
+ ...acc,
702
+ {
703
+ ...text,
704
+ ...LinearElementEditor.getBoundTextElementPosition(orig, text, elementsMap),
705
+ },
706
+ ];
707
+ }, []);
708
+ boundingBox = getCommonBoundingBox(targetElements.map(({ orig }) => orig).concat(boundTextElements));
709
+ }
710
+ const { minX, minY, maxX, maxY, midX, midY } = boundingBox;
711
+ const width = maxX - minX;
712
+ const height = maxY - minY;
713
+ if (nextWidth === undefined && nextHeight === undefined) {
714
+ nextWidth = width;
715
+ nextHeight = height;
716
+ }
717
+ if (shouldMaintainAspectRatio) {
718
+ if (nextWidth === undefined) {
719
+ nextWidth = nextHeight * (width / height);
720
+ }
721
+ else if (nextHeight === undefined) {
722
+ nextHeight = nextWidth * (height / width);
723
+ }
724
+ else if (Math.abs(nextWidth / nextHeight - width / height) > 0.001) {
725
+ nextWidth = nextHeight * (width / height);
726
+ }
727
+ }
728
+ if (nextWidth && nextHeight) {
729
+ let scaleX = handleDirection.includes("e") || handleDirection.includes("w")
730
+ ? Math.abs(nextWidth) / width
731
+ : 1;
732
+ let scaleY = handleDirection.includes("n") || handleDirection.includes("s")
733
+ ? Math.abs(nextHeight) / height
734
+ : 1;
735
+ let scale;
736
+ if (handleDirection.length === 1) {
737
+ scale =
738
+ handleDirection.includes("e") || handleDirection.includes("w")
739
+ ? scaleX
740
+ : scaleY;
547
741
  }
548
- const boundTextElement = originalElements.get(getBoundTextElementId(orig) ?? "");
549
- if (boundTextElement) {
550
- if (keepAspectRatio) {
551
- const newFontSize = boundTextElement.fontSize * scale;
552
- if (newFontSize < MIN_FONT_SIZE) {
742
+ else {
743
+ scale = Math.max(Math.abs(nextWidth) / width || 0, Math.abs(nextHeight) / height || 0);
744
+ }
745
+ const anchorsMap = {
746
+ ne: [minX, maxY],
747
+ se: [minX, minY],
748
+ sw: [maxX, minY],
749
+ nw: [maxX, maxY],
750
+ e: [minX, minY + height / 2],
751
+ w: [maxX, minY + height / 2],
752
+ n: [minX + width / 2, maxY],
753
+ s: [minX + width / 2, minY],
754
+ };
755
+ // anchor point must be on the opposite side of the dragged selection handle
756
+ // or be the center of the selection if shouldResizeFromCenter
757
+ const [anchorX, anchorY] = shouldResizeFromCenter
758
+ ? [midX, midY]
759
+ : anchorsMap[handleDirection];
760
+ const keepAspectRatio = shouldMaintainAspectRatio ||
761
+ targetElements.some((item) => item.latest.angle !== 0 ||
762
+ isTextElement(item.latest) ||
763
+ isInGroup(item.latest));
764
+ if (keepAspectRatio) {
765
+ scaleX = scale;
766
+ scaleY = scale;
767
+ }
768
+ /**
769
+ * to flip an element:
770
+ * 1. determine over which axis is the element being flipped
771
+ * (could be x, y, or both) indicated by `flipFactorX` & `flipFactorY`
772
+ * 2. shift element's position by the amount of width or height (or both) or
773
+ * mirror points in the case of linear & freedraw elemenets
774
+ * 3. adjust element angle
775
+ */
776
+ const [flipFactorX, flipFactorY] = [flipByX ? -1 : 1, flipByY ? -1 : 1];
777
+ const elementsAndUpdates = [];
778
+ for (const { orig, latest } of targetElements) {
779
+ // bounded text elements are updated along with their container elements
780
+ if (isTextElement(orig) && isBoundToContainer(orig)) {
781
+ continue;
782
+ }
783
+ const width = orig.width * scaleX;
784
+ const height = orig.height * scaleY;
785
+ const angle = normalizeRadians((orig.angle * flipFactorX * flipFactorY));
786
+ const isLinearOrFreeDraw = isLinearElement(orig) || isFreeDrawElement(orig);
787
+ const offsetX = orig.x - anchorX;
788
+ const offsetY = orig.y - anchorY;
789
+ const shiftX = flipByX && !isLinearOrFreeDraw ? width : 0;
790
+ const shiftY = flipByY && !isLinearOrFreeDraw ? height : 0;
791
+ const x = anchorX + flipFactorX * (offsetX * scaleX + shiftX);
792
+ const y = anchorY + flipFactorY * (offsetY * scaleY + shiftY);
793
+ const rescaledPoints = rescalePointsInElement(orig, width * flipFactorX, height * flipFactorY, false);
794
+ const update = {
795
+ x,
796
+ y,
797
+ width,
798
+ height,
799
+ angle,
800
+ ...rescaledPoints,
801
+ };
802
+ if (isElbowArrow(orig)) {
803
+ // Mirror fixed point binding for elbow arrows
804
+ // when resize goes into the negative direction
805
+ if (orig.startBinding) {
806
+ update.startBinding = {
807
+ ...orig.startBinding,
808
+ fixedPoint: [
809
+ flipByX
810
+ ? -orig.startBinding.fixedPoint[0] + 1
811
+ : orig.startBinding.fixedPoint[0],
812
+ flipByY
813
+ ? -orig.startBinding.fixedPoint[1] + 1
814
+ : orig.startBinding.fixedPoint[1],
815
+ ],
816
+ };
817
+ }
818
+ if (orig.endBinding) {
819
+ update.endBinding = {
820
+ ...orig.endBinding,
821
+ fixedPoint: [
822
+ flipByX
823
+ ? -orig.endBinding.fixedPoint[0] + 1
824
+ : orig.endBinding.fixedPoint[0],
825
+ flipByY
826
+ ? -orig.endBinding.fixedPoint[1] + 1
827
+ : orig.endBinding.fixedPoint[1],
828
+ ],
829
+ };
830
+ }
831
+ if (orig.fixedSegments && rescaledPoints.points) {
832
+ update.fixedSegments = orig.fixedSegments.map((segment) => ({
833
+ ...segment,
834
+ start: rescaledPoints.points[segment.index - 1],
835
+ end: rescaledPoints.points[segment.index],
836
+ }));
837
+ }
838
+ }
839
+ if (isImageElement(orig)) {
840
+ update.scale = [
841
+ orig.scale[0] * flipFactorX,
842
+ orig.scale[1] * flipFactorY,
843
+ ];
844
+ }
845
+ if (isTextElement(orig)) {
846
+ const metrics = measureFontSizeFromWidth(orig, elementsMap, width);
847
+ if (!metrics) {
553
848
  return;
554
849
  }
555
- update.boundTextFontSize = newFontSize;
850
+ update.fontSize = metrics.size;
556
851
  }
557
- else {
558
- update.boundTextFontSize = boundTextElement.fontSize;
852
+ const boundTextElement = originalElementsMap.get(getBoundTextElementId(orig) ?? "");
853
+ if (boundTextElement) {
854
+ if (keepAspectRatio) {
855
+ const newFontSize = boundTextElement.fontSize * scale;
856
+ if (newFontSize < MIN_FONT_SIZE) {
857
+ return;
858
+ }
859
+ update.boundTextFontSize = newFontSize;
860
+ }
861
+ else {
862
+ update.boundTextFontSize = boundTextElement.fontSize;
863
+ }
559
864
  }
865
+ elementsAndUpdates.push({
866
+ element: latest,
867
+ update,
868
+ });
560
869
  }
561
- elementsAndUpdates.push({
562
- element: latest,
563
- update,
564
- });
565
- }
566
- const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
567
- for (const { element, update: { boundTextFontSize, ...update }, } of elementsAndUpdates) {
568
- const { angle, width: newWidth, height: newHeight } = update;
569
- mutateElement(element, update, false);
570
- updateBoundElements(element, elementsMap, {
571
- simultaneouslyUpdated: elementsToUpdate,
572
- newSize: { width: newWidth, height: newHeight },
573
- });
574
- const boundTextElement = getBoundTextElement(element, elementsMap);
575
- if (boundTextElement && boundTextFontSize) {
576
- mutateElement(boundTextElement, {
577
- fontSize: boundTextFontSize,
578
- angle: isLinearElement(element) ? undefined : angle,
579
- }, false);
580
- handleBindTextResize(element, elementsMap, transformHandleType, true);
870
+ const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
871
+ for (const { element, update: { boundTextFontSize, ...update }, } of elementsAndUpdates) {
872
+ const { width, height, angle } = update;
873
+ mutateElement(element, update, false, {
874
+ // needed for the fixed binding point udpate to take effect
875
+ isDragging: true,
876
+ });
877
+ updateBoundElements(element, elementsMap, {
878
+ simultaneouslyUpdated: elementsToUpdate,
879
+ newSize: { width, height },
880
+ });
881
+ const boundTextElement = getBoundTextElement(element, elementsMap);
882
+ if (boundTextElement && boundTextFontSize) {
883
+ mutateElement(boundTextElement, {
884
+ fontSize: boundTextFontSize,
885
+ angle: isLinearElement(element) ? undefined : angle,
886
+ }, false);
887
+ handleBindTextResize(element, elementsMap, handleDirection, true);
888
+ }
581
889
  }
890
+ scene.triggerUpdate();
582
891
  }
583
- Scene.getScene(elementsAndUpdates[0].element)?.triggerUpdate();
584
- };
585
- const rotateMultipleElements = (originalElements, elements, elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle, centerX, centerY) => {
586
- let centerAngle = (5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
587
- if (shouldRotateWithDiscreteAngle) {
588
- centerAngle += SHIFT_LOCKING_ANGLE / 2;
589
- centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
590
- }
591
- elements
592
- .filter((element) => !isFrameLikeElement(element))
593
- .forEach((element) => {
594
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
595
- const cx = (x1 + x2) / 2;
596
- const cy = (y1 + y2) / 2;
597
- const origAngle = originalElements.get(element.id)?.angle ?? element.angle;
598
- const [rotatedCX, rotatedCY] = pointRotateRads(pointFrom(cx, cy), pointFrom(centerX, centerY), (centerAngle + origAngle - element.angle));
599
- if (isElbowArrow(element)) {
600
- const points = getArrowLocalFixedPoints(element, elementsMap);
601
- mutateElbowArrow(element, elementsMap, points);
602
- }
603
- else {
604
- mutateElement(element, {
605
- x: element.x + (rotatedCX - cx),
606
- y: element.y + (rotatedCY - cy),
607
- angle: normalizeRadians((centerAngle + origAngle)),
608
- }, false);
609
- }
610
- updateBoundElements(element, elementsMap, {
611
- simultaneouslyUpdated: elements,
612
- });
613
- const boundText = getBoundTextElement(element, elementsMap);
614
- if (boundText && !isArrowElement(element)) {
615
- mutateElement(boundText, {
616
- x: boundText.x + (rotatedCX - cx),
617
- y: boundText.y + (rotatedCY - cy),
618
- angle: normalizeRadians((centerAngle + origAngle)),
619
- }, false);
620
- }
621
- });
622
- Scene.getScene(elements[0])?.triggerUpdate();
623
- };
624
- export const getResizeOffsetXY = (transformHandleType, selectedElements, elementsMap, x, y) => {
625
- const [x1, y1, x2, y2] = selectedElements.length === 1
626
- ? getElementAbsoluteCoords(selectedElements[0], elementsMap)
627
- : getCommonBounds(selectedElements);
628
- const cx = (x1 + x2) / 2;
629
- const cy = (y1 + y2) / 2;
630
- const angle = (selectedElements.length === 1 ? selectedElements[0].angle : 0);
631
- [x, y] = pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), -angle);
632
- switch (transformHandleType) {
633
- case "n":
634
- return pointRotateRads(pointFrom(x - (x1 + x2) / 2, y - y1), pointFrom(0, 0), angle);
635
- case "s":
636
- return pointRotateRads(pointFrom(x - (x1 + x2) / 2, y - y2), pointFrom(0, 0), angle);
637
- case "w":
638
- return pointRotateRads(pointFrom(x - x1, y - (y1 + y2) / 2), pointFrom(0, 0), angle);
639
- case "e":
640
- return pointRotateRads(pointFrom(x - x2, y - (y1 + y2) / 2), pointFrom(0, 0), angle);
641
- case "nw":
642
- return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle);
643
- case "ne":
644
- return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle);
645
- case "sw":
646
- return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle);
647
- case "se":
648
- return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle);
649
- default:
650
- return [0, 0];
651
- }
652
- };
653
- export const getResizeArrowDirection = (transformHandleType, element) => {
654
- const [, [px, py]] = element.points;
655
- const isResizeEnd = (transformHandleType === "nw" && (px < 0 || py < 0)) ||
656
- (transformHandleType === "ne" && px >= 0) ||
657
- (transformHandleType === "sw" && px <= 0) ||
658
- (transformHandleType === "se" && (px > 0 || py > 0));
659
- return isResizeEnd ? "end" : "origin";
660
892
  };